datera 0.1.0 → 0.2.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/CHANGELOG.md +67 -0
- package/README.md +18 -7
- package/dist/api/formats.d.ts +4 -0
- package/dist/api/formats.js +9 -1
- package/dist/api/inspect.js +2 -1
- package/dist/cells.d.ts +5 -0
- package/dist/cells.js +24 -0
- package/dist/cli.js +2 -0
- package/dist/commands.js +9 -2
- package/dist/config/check.js +4 -0
- package/dist/config/ioSchema.d.ts +38 -0
- package/dist/config/ioSchema.js +30 -0
- package/dist/config/load.js +34 -6
- package/dist/config/schema.d.ts +38 -0
- package/dist/env.d.ts +0 -1
- package/dist/env.js +0 -9
- package/dist/filters/combine.js +7 -6
- package/dist/filters/dedupe.js +2 -1
- package/dist/filters/fillEmpty.js +5 -4
- package/dist/filters/merge.js +4 -3
- package/dist/filters/mergeStrategies.js +12 -11
- package/dist/filters/mergeUnkeyed.js +2 -3
- package/dist/filters/validate.js +3 -2
- package/dist/filters/validateTypes.d.ts +0 -5
- package/dist/io/csv/csvFormat.d.ts +1 -0
- package/dist/io/csv/csvFormat.js +8 -0
- package/dist/io/csv/csvSink.d.ts +1 -0
- package/dist/io/csv/csvSink.js +9 -5
- package/dist/io/csv/csvSource.js +2 -2
- package/dist/io/excel/excelSink.js +3 -2
- package/dist/io/excel/excelSource.js +2 -1
- package/dist/io/factory.js +57 -6
- package/dist/io/json/jsonSource.js +6 -5
- package/dist/io/mysql/client.d.ts +1 -0
- package/dist/io/mysql/client.js +1 -0
- package/dist/io/mysql/mysqlSink.d.ts +20 -0
- package/dist/io/mysql/mysqlSink.js +89 -0
- package/dist/io/parquet/parquetSink.js +2 -1
- package/dist/io/parquet/parquetSource.js +3 -4
- package/dist/io/postgres/postgresSink.d.ts +16 -0
- package/dist/io/postgres/postgresSink.js +82 -0
- package/dist/io/postgres/postgresSource.d.ts +1 -1
- package/dist/io/safety.d.ts +1 -1
- package/dist/io/safety.js +105 -6
- package/dist/io/servers.d.ts +14 -0
- package/dist/io/servers.js +72 -0
- package/dist/io/sheets/client.js +2 -1
- package/dist/io/sheets/sheetsSource.js +2 -1
- package/dist/io/sql/cell.js +2 -1
- package/dist/io/sql/target.d.ts +8 -0
- package/dist/io/sql/target.js +14 -0
- package/dist/io/sql/writer.d.ts +46 -0
- package/dist/io/sql/writer.js +264 -0
- package/dist/io/sqlite/sqlite.d.ts +3 -0
- package/dist/io/sqlite/sqlite.js +11 -0
- package/dist/io/sqlite/sqliteSink.d.ts +17 -0
- package/dist/io/sqlite/sqliteSink.js +69 -0
- package/dist/io/sqlite/sqliteSource.js +2 -9
- package/dist/io/sqlserver/sqlServerSink.d.ts +20 -0
- package/dist/io/sqlserver/sqlServerSink.js +107 -0
- package/dist/io/xml/xmlParse.js +4 -2
- package/dist/io/xml/xmlSource.js +7 -6
- package/dist/pipeline/runEtl.js +2 -1
- package/package.json +10 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Mudanças
|
|
2
|
+
|
|
3
|
+
Tudo que muda de uma versão pra outra do Datera fica aqui, da mais nova pra mais antiga. Vale pro pacote do npm, pro comando `datera` e pros dois apps, que saem sempre com o mesmo número.
|
|
4
|
+
|
|
5
|
+
Os números seguem o [versionamento semântico](https://semver.org/lang/pt-BR/):
|
|
6
|
+
|
|
7
|
+
- o último número sobe quando é só correção (`0.1.0` → `0.1.1`)
|
|
8
|
+
- o do meio sobe quando entra coisa nova sem quebrar o que já funcionava (`0.1.1` → `0.2.0`)
|
|
9
|
+
- o primeiro sobe quando alguma coisa deixa de funcionar do jeito antigo e precisa de ajuste de quem usa (`0.9.0` → `1.0.0`)
|
|
10
|
+
|
|
11
|
+
## [Não lançado]
|
|
12
|
+
|
|
13
|
+
## [0.2.0] - 2026-09-29
|
|
14
|
+
|
|
15
|
+
### Adicionado
|
|
16
|
+
|
|
17
|
+
- este CHANGELOG, e o `npm run release:prepare -- <versão>`, que troca a versão nos três `package.json` e fecha a seção do CHANGELOG de uma vez
|
|
18
|
+
- a action da aba Releases agora usa a seção do CHANGELOG como texto da release, e não deixa publicar uma versão sem ela
|
|
19
|
+
- MySQL, PostgreSQL, SQL Server e SQLite como destino: o resultado vai pra uma tabela nova e as pendências pra outra (`_pendencias` no fim do nome, ou o `pendingTable`), tudo numa transação
|
|
20
|
+
- o Datera anota as tabelas que ele cria em `datera_tabelas` e nunca apaga uma tabela que não foi ele que criou, nem a tabela de onde ele lê
|
|
21
|
+
- variáveis `DEST_DB_*` no `.env` pro banco de destino. Se for o mesmo banco da fonte, o que faltar é copiado dela
|
|
22
|
+
- `--output-table` no `convert`, `dedupe` e `merge`, e as opções `table` e `pendingTable` no `writeRows` e no `convert`
|
|
23
|
+
- no app, banco de dados e arquivo SQLite aparecem como destino no passo a passo, com a opção de salvar no mesmo servidor da fonte, e o Datera Dev ganhou os trechos de destino dos quatro bancos
|
|
24
|
+
- testes de tela dos dois apps com Playwright (`npm run test:telas`), nos temas claro e escuro, rodando no CI com as fotos de cada tela
|
|
25
|
+
|
|
26
|
+
### Mudou
|
|
27
|
+
|
|
28
|
+
- os testes usam só os dados da escola de música inventada, sem "clientes" nem telefone
|
|
29
|
+
- o `config.json.example` virou uma config de verdade, que já roda lendo o CSV de exemplo
|
|
30
|
+
|
|
31
|
+
### Segurança
|
|
32
|
+
|
|
33
|
+
- o CSV desarma célula de texto que o Excel leria como fórmula (começando com `=`, `+`, `-` ou `@`), colocando um `'` na frente. Dá pra desligar com `escapeFormulas: false`
|
|
34
|
+
- no app, a primeira exportação de cada configuração mostra de onde ela lê e onde grava, e só continua se você confirmar
|
|
35
|
+
- a senha da fonte só é reaproveitada no destino quando os dois são o mesmo servidor
|
|
36
|
+
- `ssl` no MySQL, pra conexão criptografada
|
|
37
|
+
- os instaladores desligam os recursos do Electron que deixariam usar o app como um Node.js escondido
|
|
38
|
+
- nome de tabela ou coluna com caractere invisível é recusado antes de gravar no banco
|
|
39
|
+
|
|
40
|
+
### Removido
|
|
41
|
+
|
|
42
|
+
- os scripts `test:config` e `test:connection`, que faziam o mesmo que o `datera validate` e o `--dry-run`
|
|
43
|
+
|
|
44
|
+
## [0.1.0] - 2026-09-29
|
|
45
|
+
|
|
46
|
+
Primeira versão publicada no npm e na aba Releases.
|
|
47
|
+
|
|
48
|
+
### Adicionado
|
|
49
|
+
|
|
50
|
+
- lê de MySQL, PostgreSQL, SQL Server, SQLite, Google Sheets, Excel, CSV, JSON, XML e Parquet, e escreve no Google Sheets, Excel, CSV, JSON, XML e Parquet
|
|
51
|
+
- modos `raw`, `dedupe` e `merge`, com normalizadores (`trim`, `lowercase`, `digitsOnly`, `alphanumeric`) e os seus próprios
|
|
52
|
+
- preparação (`fillEmpty`, `combineColumns`, `distribute`) e validação com a lista de pendências e o motivo de cada linha
|
|
53
|
+
- app Datera, pra quem não programa, com passo a passo, regras em frases, prévia, histórico e tema escuro
|
|
54
|
+
- app Datera Dev, com editor da config, erros marcados na linha, paleta de comandos e teste de normalizador
|
|
55
|
+
- instaladores pra Windows gerados sozinhos a cada tag de versão
|
|
56
|
+
- comando `datera` com `run`, `validate`, `init`, `convert`, `dedupe`, `merge`, `columns`, `preview`, `normalizers` e `normalize`
|
|
57
|
+
- funções pra usar no código: `readRows`, `writeRows`, `convert`, `clean`, `fillEmpty`, `combineColumns`, `validate`, `dedupe`, `merge`, `describeColumns`, `defineConfig` e `onStep` pra acompanhar o progresso
|
|
58
|
+
- `dedupeKeyNormalizer`, pra comparar repetidos depois de passar por um normalizador
|
|
59
|
+
- os pacotes de Excel, Parquet, Google Sheets e dos bancos são opcionais: só instala quem for usar
|
|
60
|
+
|
|
61
|
+
### Segurança
|
|
62
|
+
|
|
63
|
+
- arquivos de código citados na config só rodam depois de confirmados no app
|
|
64
|
+
- a senha do banco fica no `.env`, e o app só lê do `.env` as variáveis que conhece
|
|
65
|
+
- as telas não navegam pra fora do app nem abrem janelas novas
|
|
66
|
+
- o leitor de XML recusa `<!DOCTYPE>`
|
|
67
|
+
- entrada e saída nunca podem ser o mesmo arquivo, mesmo escrito de outro jeito
|
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="https://github.com/alessandravieiradev-blip/datera/actions/workflows/ci.yml"><img src="https://github.com/alessandravieiradev-blip/datera/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/datera"><img src="https://img.shields.io/npm/v/datera" alt="Versão no npm"></a>
|
|
9
10
|
</p>
|
|
10
11
|
|
|
11
12
|
<p align="center">
|
|
@@ -27,12 +28,14 @@ flowchart LR
|
|
|
27
28
|
end
|
|
28
29
|
subgraph destinos["Escreve em"]
|
|
29
30
|
direction TB
|
|
31
|
+
B2["Bancos<br/>MySQL · PostgreSQL<br/>SQL Server · SQLite"]
|
|
30
32
|
P2["Planilhas<br/>Google Sheets · Excel"]
|
|
31
33
|
A2["Arquivos<br/>CSV · JSON<br/>XML · Parquet"]
|
|
32
34
|
end
|
|
33
35
|
B --> D((Datera))
|
|
34
36
|
P1 --> D
|
|
35
37
|
A1 --> D
|
|
38
|
+
D --> B2
|
|
36
39
|
D --> P2
|
|
37
40
|
D --> A2
|
|
38
41
|
classDef datera fill:#2563EB,stroke:#2563EB,color:#ffffff
|
|
@@ -49,11 +52,11 @@ Então o Datera funciona assim: as regras ficam num arquivo de configuração (o
|
|
|
49
52
|
|
|
50
53
|
Por isso ele tem três jeitos de usar, todos com o mesmo motor e a mesma configuração:
|
|
51
54
|
|
|
52
|
-
| Jeito
|
|
53
|
-
|
|
|
54
|
-
| **Datera**
|
|
55
|
-
| **Datera Dev**
|
|
56
|
-
| **Terminal**
|
|
55
|
+
| Jeito | Pra quem | O que tem |
|
|
56
|
+
| --------------------- | ---------------------------------------------- | ------------------------------------------------------------------------- |
|
|
57
|
+
| **Datera** | quem cuida dos dados mas não programa | app com tela, passo a passo, regras em frases, gráficos e histórico |
|
|
58
|
+
| **Datera Dev** | quem programa | app escuro com editor da config, prévia na hora, log e paleta de comandos |
|
|
59
|
+
| **Terminal e código** | quem quer automatizar ou usar em outro projeto | o comando `datera` e funções pra usar no código, pelo npm |
|
|
57
60
|
|
|
58
61
|
## Como funciona
|
|
59
62
|
|
|
@@ -77,6 +80,10 @@ Hoje ele consegue:
|
|
|
77
80
|
- decidir o que fazer com as linhas que não têm chave, sem elas sumirem nem se misturarem
|
|
78
81
|
- espalhar valores em várias colunas sem jogar nenhum fora
|
|
79
82
|
- ler e escrever em formatos diferentes sem mudar nada das regras
|
|
83
|
+
- gravar o resultado numa tabela nova de banco, sem nunca mexer numa tabela que não foi ele que criou
|
|
84
|
+
- converter um arquivo de um formato pra outro, tirar repetidos ou juntar cadastros com um comando só, sem montar config
|
|
85
|
+
- mostrar um raio-x das colunas de um arquivo (quantas vazias, quantos valores diferentes) antes de montar as regras
|
|
86
|
+
- ser usado dentro de outro projeto em JavaScript ou TypeScript, com as mesmas regras da config
|
|
80
87
|
|
|
81
88
|
## Escolha o seu caminho
|
|
82
89
|
|
|
@@ -98,6 +105,10 @@ Você quer usar o app com tela: instalar, escolher de onde vêm os dados, montar
|
|
|
98
105
|
|
|
99
106
|
Você quer rodar pelo terminal, usar o Datera Dev, escrever a config na mão, criar normalizadores ou chamar o Datera de dentro de outro código.
|
|
100
107
|
|
|
108
|
+
```bash
|
|
109
|
+
npm install -g datera
|
|
110
|
+
```
|
|
111
|
+
|
|
101
112
|
<a href="docs/devs.md"><img src="https://img.shields.io/badge/Abrir%20o%20guia%20para%20devs-16181D?style=for-the-badge" alt="Abrir o guia para devs"></a>
|
|
102
113
|
<a href="docs/api.md"><img src="https://img.shields.io/badge/Comandos%20e%20API-475569?style=for-the-badge" alt="Comandos e API"></a>
|
|
103
114
|
|
|
@@ -121,11 +132,11 @@ Você quer rodar pelo terminal, usar o Datera Dev, escrever a config na mão, cr
|
|
|
121
132
|
|
|
122
133
|
## Contribuir e licença
|
|
123
134
|
|
|
124
|
-
Se quiser ajudar, o jeito de rodar, testar e mandar mudanças está no [CONTRIBUTING.md](CONTRIBUTING.md). O código é aberto, sob a [licença MIT](LICENSE).
|
|
135
|
+
Se quiser ajudar, o jeito de rodar, testar e mandar mudanças está no [CONTRIBUTING.md](CONTRIBUTING.md). O código é aberto, sob a [licença MIT](LICENSE), e o que mudou em cada versão está no [CHANGELOG](CHANGELOG.md). Quem participa segue o [código de conduta](CODE_OF_CONDUCT.md), e falha de segurança se avisa pelo caminho do [SECURITY.md](SECURITY.md).
|
|
125
136
|
|
|
126
137
|
## Próximos passos
|
|
127
138
|
|
|
128
|
-
A ideia é
|
|
139
|
+
A ideia é que qualquer pessoa consiga usar o Datera: quem não programa, pelo instalador de um clique, e quem programa, pelo pacote no npm, no terminal ou no código.
|
|
129
140
|
|
|
130
141
|
O que eu quero fazer fica nas [issues](https://github.com/alessandravieiradev-blip/datera/issues), agrupadas em etapas nos [milestones](https://github.com/alessandravieiradev-blip/datera/milestones). Lá dá pra ver o que já foi feito e o que falta, e tudo se atualiza sozinho conforme eu vou fazendo.
|
|
131
142
|
|
package/dist/api/formats.d.ts
CHANGED
|
@@ -7,10 +7,14 @@ export interface ReadOptions {
|
|
|
7
7
|
delimiter?: string | undefined;
|
|
8
8
|
encoding?: "utf-8" | "latin1" | undefined;
|
|
9
9
|
}
|
|
10
|
+
export declare const DEFAULT_TABLE = "dados";
|
|
10
11
|
export interface WriteOptions {
|
|
12
|
+
table?: string | undefined;
|
|
13
|
+
pendingTable?: string | undefined;
|
|
11
14
|
sheet?: string | undefined;
|
|
12
15
|
delimiter?: string | undefined;
|
|
13
16
|
bom?: boolean | undefined;
|
|
17
|
+
escapeFormulas?: boolean | undefined;
|
|
14
18
|
root?: string | undefined;
|
|
15
19
|
record?: string | undefined;
|
|
16
20
|
}
|
package/dist/api/formats.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_TABLE = void 0;
|
|
6
7
|
exports.detectFormat = detectFormat;
|
|
7
8
|
exports.sourceFromPath = sourceFromPath;
|
|
8
9
|
exports.destinationFromPath = destinationFromPath;
|
|
@@ -19,6 +20,7 @@ const EXTENSIONS = {
|
|
|
19
20
|
".sqlite": "sqlite",
|
|
20
21
|
".sqlite3": "sqlite",
|
|
21
22
|
};
|
|
23
|
+
exports.DEFAULT_TABLE = "dados";
|
|
22
24
|
function defined(value) {
|
|
23
25
|
return Object.fromEntries(Object.entries(value).filter(([, inner]) => inner !== undefined));
|
|
24
26
|
}
|
|
@@ -82,6 +84,7 @@ function destinationFromPath(filePath, options = {}) {
|
|
|
82
84
|
? "\t"
|
|
83
85
|
: undefined),
|
|
84
86
|
bom: options.bom,
|
|
87
|
+
escapeFormulas: options.escapeFormulas,
|
|
85
88
|
});
|
|
86
89
|
case "json":
|
|
87
90
|
return { type: "json", path: filePath };
|
|
@@ -101,6 +104,11 @@ function destinationFromPath(filePath, options = {}) {
|
|
|
101
104
|
case "parquet":
|
|
102
105
|
return { type: "parquet", path: filePath };
|
|
103
106
|
case "sqlite":
|
|
104
|
-
|
|
107
|
+
return defined({
|
|
108
|
+
type: "sqlite",
|
|
109
|
+
path: filePath,
|
|
110
|
+
table: options.table?.trim() || exports.DEFAULT_TABLE,
|
|
111
|
+
pendingTable: options.pendingTable,
|
|
112
|
+
});
|
|
105
113
|
}
|
|
106
114
|
}
|
package/dist/api/inspect.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.normalize = normalize;
|
|
|
6
6
|
const header_1 = require("../io/header");
|
|
7
7
|
const normalizers_1 = require("../normalizers");
|
|
8
8
|
const registry_1 = require("../normalizers/registry");
|
|
9
|
+
const cells_1 = require("../cells");
|
|
9
10
|
const EXAMPLES = 3;
|
|
10
11
|
function isEmpty(value) {
|
|
11
12
|
return value === null || value === undefined || value === "";
|
|
@@ -21,7 +22,7 @@ function kindOf(values) {
|
|
|
21
22
|
function describeColumns(rows) {
|
|
22
23
|
return (0, header_1.buildHeader)(rows).map((name) => {
|
|
23
24
|
const present = rows
|
|
24
|
-
.map((row) => row
|
|
25
|
+
.map((row) => (0, cells_1.cellOf)(row, name))
|
|
25
26
|
.filter((value) => !isEmpty(value));
|
|
26
27
|
const distinct = new Set(present.map((value) => String(value)));
|
|
27
28
|
return {
|
package/dist/cells.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { TableRow } from "./types";
|
|
2
|
+
export type Cell = TableRow[string];
|
|
3
|
+
export declare function hasCell(row: TableRow, column: string): boolean;
|
|
4
|
+
export declare function cellOf(row: TableRow, column: string): Cell | undefined;
|
|
5
|
+
export declare function setCell(row: TableRow, column: string, value: Cell): void;
|
package/dist/cells.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasCell = hasCell;
|
|
4
|
+
exports.cellOf = cellOf;
|
|
5
|
+
exports.setCell = setCell;
|
|
6
|
+
const own = Object.prototype.hasOwnProperty;
|
|
7
|
+
function hasCell(row, column) {
|
|
8
|
+
return own.call(row, column);
|
|
9
|
+
}
|
|
10
|
+
function cellOf(row, column) {
|
|
11
|
+
return own.call(row, column) ? row[column] : undefined;
|
|
12
|
+
}
|
|
13
|
+
function setCell(row, column, value) {
|
|
14
|
+
if (column === "__proto__") {
|
|
15
|
+
Object.defineProperty(row, column, {
|
|
16
|
+
value,
|
|
17
|
+
writable: true,
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
row[column] = value;
|
|
24
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ function withReadFlags(command) {
|
|
|
25
25
|
}
|
|
26
26
|
function withWriteFlags(command) {
|
|
27
27
|
return command
|
|
28
|
+
.option("--output-table <tabela>", 'Tabela onde gravar, quando a saída é SQLite (padrão: "dados").')
|
|
28
29
|
.option("--output-sheet <aba>", "Nome da aba, quando a saída é Excel.")
|
|
29
30
|
.option("--output-delimiter <separador>", "Separador do CSV de saída.")
|
|
30
31
|
.option("--root <nome>", "Elemento de fora, quando a saída é XML.")
|
|
@@ -41,6 +42,7 @@ function readOptions(flags) {
|
|
|
41
42
|
}
|
|
42
43
|
function writeOptions(flags) {
|
|
43
44
|
return {
|
|
45
|
+
table: flags.outputTable,
|
|
44
46
|
sheet: flags.outputSheet,
|
|
45
47
|
delimiter: flags.outputDelimiter,
|
|
46
48
|
root: flags.root,
|
package/dist/commands.js
CHANGED
|
@@ -11,6 +11,13 @@ const config_1 = require("./config");
|
|
|
11
11
|
const api_1 = require("./api");
|
|
12
12
|
const safety_1 = require("./io/safety");
|
|
13
13
|
const pipeline_1 = require("./pipeline");
|
|
14
|
+
function assertSafeOutput(cli) {
|
|
15
|
+
(0, safety_1.assertSourceIsSafe)({
|
|
16
|
+
mode: "raw",
|
|
17
|
+
source: (0, api_1.sourceFromPath)(cli.input, cli.read),
|
|
18
|
+
destination: (0, api_1.destinationFromPath)(cli.output, cli.write),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
14
21
|
function plural(count, one, many) {
|
|
15
22
|
return `${count} ${count === 1 ? one : many}`;
|
|
16
23
|
}
|
|
@@ -112,7 +119,7 @@ async function runCommand(cli, logger) {
|
|
|
112
119
|
return 0;
|
|
113
120
|
}
|
|
114
121
|
case "dedupe": {
|
|
115
|
-
(
|
|
122
|
+
assertSafeOutput(cli);
|
|
116
123
|
const rows = await (0, api_1.readRows)(cli.input, cli.read);
|
|
117
124
|
const result = (0, api_1.dedupe)(rows, cli.by, {
|
|
118
125
|
keep: cli.keep,
|
|
@@ -123,7 +130,7 @@ async function runCommand(cli, logger) {
|
|
|
123
130
|
return 0;
|
|
124
131
|
}
|
|
125
132
|
case "merge": {
|
|
126
|
-
(
|
|
133
|
+
assertSafeOutput(cli);
|
|
127
134
|
const rows = await (0, api_1.readRows)(cli.input, cli.read);
|
|
128
135
|
const result = (0, api_1.merge)(rows, cli.key, {
|
|
129
136
|
normalizer: cli.normalizer,
|
package/dist/config/check.js
CHANGED
|
@@ -10,6 +10,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const messages_1 = require("./messages");
|
|
12
12
|
const schema_1 = require("./schema");
|
|
13
|
+
const safety_1 = require("../io/safety");
|
|
13
14
|
function pathText(parts) {
|
|
14
15
|
return parts
|
|
15
16
|
.filter((part) => typeof part === "string" || typeof part === "number")
|
|
@@ -74,6 +75,9 @@ function checkConfigText(text, folder) {
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
const problems = missingFiles(result.data, folder);
|
|
78
|
+
const tables = (0, safety_1.tableProblem)(result.data);
|
|
79
|
+
if (tables !== null)
|
|
80
|
+
problems.push({ where: "destination.table", message: tables });
|
|
77
81
|
return { ok: problems.length === 0, problems };
|
|
78
82
|
}
|
|
79
83
|
function checkConfigFile(configPath) {
|
|
@@ -7,6 +7,7 @@ export declare const sourceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
7
7
|
database: z.ZodOptional<z.ZodString>;
|
|
8
8
|
table: z.ZodOptional<z.ZodString>;
|
|
9
9
|
type: z.ZodLiteral<"mysql">;
|
|
10
|
+
ssl: z.ZodOptional<z.ZodBoolean>;
|
|
10
11
|
}, z.core.$strip>, z.ZodObject<{
|
|
11
12
|
host: z.ZodOptional<z.ZodString>;
|
|
12
13
|
port: z.ZodOptional<z.ZodNumber>;
|
|
@@ -64,6 +65,42 @@ export declare const sourceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
64
65
|
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
65
66
|
}, z.core.$strip>], "type">;
|
|
66
67
|
export declare const destinationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
68
|
+
host: z.ZodOptional<z.ZodString>;
|
|
69
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
user: z.ZodOptional<z.ZodString>;
|
|
71
|
+
password: z.ZodOptional<z.ZodString>;
|
|
72
|
+
database: z.ZodOptional<z.ZodString>;
|
|
73
|
+
table: z.ZodOptional<z.ZodString>;
|
|
74
|
+
pendingTable: z.ZodOptional<z.ZodString>;
|
|
75
|
+
type: z.ZodLiteral<"mysql">;
|
|
76
|
+
ssl: z.ZodOptional<z.ZodBoolean>;
|
|
77
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
78
|
+
host: z.ZodOptional<z.ZodString>;
|
|
79
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
80
|
+
user: z.ZodOptional<z.ZodString>;
|
|
81
|
+
password: z.ZodOptional<z.ZodString>;
|
|
82
|
+
database: z.ZodOptional<z.ZodString>;
|
|
83
|
+
table: z.ZodOptional<z.ZodString>;
|
|
84
|
+
pendingTable: z.ZodOptional<z.ZodString>;
|
|
85
|
+
type: z.ZodLiteral<"postgres">;
|
|
86
|
+
ssl: z.ZodOptional<z.ZodBoolean>;
|
|
87
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
88
|
+
host: z.ZodOptional<z.ZodString>;
|
|
89
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
90
|
+
user: z.ZodOptional<z.ZodString>;
|
|
91
|
+
password: z.ZodOptional<z.ZodString>;
|
|
92
|
+
database: z.ZodOptional<z.ZodString>;
|
|
93
|
+
table: z.ZodOptional<z.ZodString>;
|
|
94
|
+
pendingTable: z.ZodOptional<z.ZodString>;
|
|
95
|
+
type: z.ZodLiteral<"sqlserver">;
|
|
96
|
+
encrypt: z.ZodOptional<z.ZodBoolean>;
|
|
97
|
+
trustServerCertificate: z.ZodOptional<z.ZodBoolean>;
|
|
98
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
99
|
+
pendingTable: z.ZodOptional<z.ZodString>;
|
|
100
|
+
type: z.ZodLiteral<"sqlite">;
|
|
101
|
+
path: z.ZodString;
|
|
102
|
+
table: z.ZodString;
|
|
103
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
67
104
|
type: z.ZodLiteral<"sheets">;
|
|
68
105
|
spreadsheetId: z.ZodOptional<z.ZodString>;
|
|
69
106
|
credentialsPath: z.ZodOptional<z.ZodString>;
|
|
@@ -73,6 +110,7 @@ export declare const destinationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
73
110
|
path: z.ZodString;
|
|
74
111
|
delimiter: z.ZodOptional<z.ZodString>;
|
|
75
112
|
bom: z.ZodOptional<z.ZodBoolean>;
|
|
113
|
+
escapeFormulas: z.ZodOptional<z.ZodBoolean>;
|
|
76
114
|
}, z.core.$strip>, z.ZodObject<{
|
|
77
115
|
type: z.ZodLiteral<"json">;
|
|
78
116
|
path: z.ZodString;
|
package/dist/config/ioSchema.js
CHANGED
|
@@ -17,6 +17,7 @@ exports.sourceSchema = zod_1.z.discriminatedUnion("type", [
|
|
|
17
17
|
zod_1.z.object({
|
|
18
18
|
type: zod_1.z.literal("mysql"),
|
|
19
19
|
...serverFields,
|
|
20
|
+
ssl: zod_1.z.boolean().optional(),
|
|
20
21
|
}),
|
|
21
22
|
zod_1.z.object({
|
|
22
23
|
type: zod_1.z.literal("postgres"),
|
|
@@ -71,7 +72,35 @@ exports.sourceSchema = zod_1.z.discriminatedUnion("type", [
|
|
|
71
72
|
options: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
72
73
|
}),
|
|
73
74
|
]);
|
|
75
|
+
const tableTargetFields = {
|
|
76
|
+
pendingTable: zod_1.z.string().trim().min(1).optional(),
|
|
77
|
+
};
|
|
74
78
|
exports.destinationSchema = zod_1.z.discriminatedUnion("type", [
|
|
79
|
+
zod_1.z.object({
|
|
80
|
+
type: zod_1.z.literal("mysql"),
|
|
81
|
+
...serverFields,
|
|
82
|
+
...tableTargetFields,
|
|
83
|
+
ssl: zod_1.z.boolean().optional(),
|
|
84
|
+
}),
|
|
85
|
+
zod_1.z.object({
|
|
86
|
+
type: zod_1.z.literal("postgres"),
|
|
87
|
+
...serverFields,
|
|
88
|
+
...tableTargetFields,
|
|
89
|
+
ssl: zod_1.z.boolean().optional(),
|
|
90
|
+
}),
|
|
91
|
+
zod_1.z.object({
|
|
92
|
+
type: zod_1.z.literal("sqlserver"),
|
|
93
|
+
...serverFields,
|
|
94
|
+
...tableTargetFields,
|
|
95
|
+
encrypt: zod_1.z.boolean().optional(),
|
|
96
|
+
trustServerCertificate: zod_1.z.boolean().optional(),
|
|
97
|
+
}),
|
|
98
|
+
zod_1.z.object({
|
|
99
|
+
type: zod_1.z.literal("sqlite"),
|
|
100
|
+
path: zod_1.z.string(),
|
|
101
|
+
table: zod_1.z.string().trim().min(1),
|
|
102
|
+
...tableTargetFields,
|
|
103
|
+
}),
|
|
75
104
|
zod_1.z.object({
|
|
76
105
|
type: zod_1.z.literal("sheets"),
|
|
77
106
|
spreadsheetId: zod_1.z.string().optional(),
|
|
@@ -83,6 +112,7 @@ exports.destinationSchema = zod_1.z.discriminatedUnion("type", [
|
|
|
83
112
|
path: zod_1.z.string(),
|
|
84
113
|
delimiter: zod_1.z.string().min(1).optional(),
|
|
85
114
|
bom: zod_1.z.boolean().optional(),
|
|
115
|
+
escapeFormulas: zod_1.z.boolean().optional(),
|
|
86
116
|
}),
|
|
87
117
|
zod_1.z.object({
|
|
88
118
|
type: zod_1.z.literal("json"),
|
package/dist/config/load.js
CHANGED
|
@@ -15,10 +15,17 @@ function loadJsonConfig(filePath) {
|
|
|
15
15
|
const parsedData = JSON.parse(content);
|
|
16
16
|
return schema_1.etlConfigSchema.parse(parsedData);
|
|
17
17
|
}
|
|
18
|
+
function envPassword(prefix) {
|
|
19
|
+
const password = (0, env_1.getOptionalEnv)(`${prefix}_PASSWORD`);
|
|
20
|
+
if (password === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
if ((0, env_1.getOptionalEnv)(`${prefix}_HOST`) === undefined) {
|
|
23
|
+
throw new Error(`Por segurança, a senha do .env (${prefix}_PASSWORD) só é usada quando o .env também diz qual é o servidor (${prefix}_HOST). Assim uma config não consegue mandar a sua senha pra outro endereço. Coloque ${prefix}_HOST=<endereço do servidor> no .env, do lado da senha.`);
|
|
24
|
+
}
|
|
25
|
+
return password;
|
|
26
|
+
}
|
|
18
27
|
function withDatabaseEnv(source) {
|
|
19
|
-
if (source
|
|
20
|
-
source?.type !== "postgres" &&
|
|
21
|
-
source?.type !== "sqlserver")
|
|
28
|
+
if (!isServer(source))
|
|
22
29
|
return source;
|
|
23
30
|
const port = (0, env_1.getOptionalEnv)("DB_PORT");
|
|
24
31
|
return {
|
|
@@ -26,11 +33,30 @@ function withDatabaseEnv(source) {
|
|
|
26
33
|
host: (0, env_1.getOptionalEnv)("DB_HOST") ?? source.host,
|
|
27
34
|
port: port !== undefined ? Number(port) : source.port,
|
|
28
35
|
user: (0, env_1.getOptionalEnv)("DB_USER") ?? source.user,
|
|
29
|
-
password: (
|
|
36
|
+
password: envPassword("DB") ?? source.password,
|
|
30
37
|
database: (0, env_1.getOptionalEnv)("DB_NAME") ?? source.database,
|
|
31
38
|
table: (0, env_1.getOptionalEnv)("DB_TABLE") ?? source.table,
|
|
32
39
|
};
|
|
33
40
|
}
|
|
41
|
+
function isServer(side) {
|
|
42
|
+
return (side?.type === "mysql" ||
|
|
43
|
+
side?.type === "postgres" ||
|
|
44
|
+
side?.type === "sqlserver");
|
|
45
|
+
}
|
|
46
|
+
function withDestinationDatabaseEnv(destination) {
|
|
47
|
+
if (!isServer(destination))
|
|
48
|
+
return destination;
|
|
49
|
+
const port = (0, env_1.getOptionalEnv)("DEST_DB_PORT");
|
|
50
|
+
return {
|
|
51
|
+
...destination,
|
|
52
|
+
host: (0, env_1.getOptionalEnv)("DEST_DB_HOST") ?? destination.host,
|
|
53
|
+
port: port !== undefined ? Number(port) : destination.port,
|
|
54
|
+
user: (0, env_1.getOptionalEnv)("DEST_DB_USER") ?? destination.user,
|
|
55
|
+
password: envPassword("DEST_DB") ?? destination.password,
|
|
56
|
+
database: (0, env_1.getOptionalEnv)("DEST_DB_NAME") ?? destination.database,
|
|
57
|
+
table: (0, env_1.getOptionalEnv)("DEST_DB_TABLE") ?? destination.table,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
34
60
|
function withSheetsEnv(destination) {
|
|
35
61
|
if (destination?.type !== "sheets")
|
|
36
62
|
return destination;
|
|
@@ -47,7 +73,7 @@ function withEnv(jsonConfig) {
|
|
|
47
73
|
const finalConfig = {
|
|
48
74
|
...jsonConfig,
|
|
49
75
|
source: withDatabaseEnv(jsonConfig.source),
|
|
50
|
-
destination: withSheetsEnv(jsonConfig.destination),
|
|
76
|
+
destination: withDestinationDatabaseEnv(withSheetsEnv(jsonConfig.destination)),
|
|
51
77
|
tableName: (0, env_1.getOptionalEnv)("DB_TABLE") ?? jsonConfig.tableName,
|
|
52
78
|
spreadsheetId: (0, env_1.getOptionalEnv)("GOOGLE_SPREADSHEET_ID") ?? jsonConfig.spreadsheetId,
|
|
53
79
|
credentialsPath: (0, env_1.getOptionalEnv)("GOOGLE_SERVICE_ACCOUNT_KEY_PATH") ??
|
|
@@ -55,7 +81,9 @@ function withEnv(jsonConfig) {
|
|
|
55
81
|
dbHost: (0, env_1.getOptionalEnv)("DB_HOST") ?? jsonConfig.dbHost,
|
|
56
82
|
dbPort: dbPort !== undefined ? Number(dbPort) : jsonConfig.dbPort,
|
|
57
83
|
dbUser: (0, env_1.getOptionalEnv)("DB_USER") ?? jsonConfig.dbUser,
|
|
58
|
-
dbPassword:
|
|
84
|
+
dbPassword: jsonConfig.source === undefined
|
|
85
|
+
? (envPassword("DB") ?? jsonConfig.dbPassword)
|
|
86
|
+
: jsonConfig.dbPassword,
|
|
59
87
|
dbName: (0, env_1.getOptionalEnv)("DB_NAME") ?? jsonConfig.dbName,
|
|
60
88
|
};
|
|
61
89
|
return schema_1.etlConfigSchema.parse(finalConfig);
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const etlConfigSchema: z.ZodObject<{
|
|
|
8
8
|
database: z.ZodOptional<z.ZodString>;
|
|
9
9
|
table: z.ZodOptional<z.ZodString>;
|
|
10
10
|
type: z.ZodLiteral<"mysql">;
|
|
11
|
+
ssl: z.ZodOptional<z.ZodBoolean>;
|
|
11
12
|
}, z.core.$strip>, z.ZodObject<{
|
|
12
13
|
host: z.ZodOptional<z.ZodString>;
|
|
13
14
|
port: z.ZodOptional<z.ZodNumber>;
|
|
@@ -65,6 +66,42 @@ export declare const etlConfigSchema: z.ZodObject<{
|
|
|
65
66
|
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
66
67
|
}, z.core.$strip>], "type">>;
|
|
67
68
|
destination: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
69
|
+
host: z.ZodOptional<z.ZodString>;
|
|
70
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
71
|
+
user: z.ZodOptional<z.ZodString>;
|
|
72
|
+
password: z.ZodOptional<z.ZodString>;
|
|
73
|
+
database: z.ZodOptional<z.ZodString>;
|
|
74
|
+
table: z.ZodOptional<z.ZodString>;
|
|
75
|
+
pendingTable: z.ZodOptional<z.ZodString>;
|
|
76
|
+
type: z.ZodLiteral<"mysql">;
|
|
77
|
+
ssl: z.ZodOptional<z.ZodBoolean>;
|
|
78
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
79
|
+
host: z.ZodOptional<z.ZodString>;
|
|
80
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
user: z.ZodOptional<z.ZodString>;
|
|
82
|
+
password: z.ZodOptional<z.ZodString>;
|
|
83
|
+
database: z.ZodOptional<z.ZodString>;
|
|
84
|
+
table: z.ZodOptional<z.ZodString>;
|
|
85
|
+
pendingTable: z.ZodOptional<z.ZodString>;
|
|
86
|
+
type: z.ZodLiteral<"postgres">;
|
|
87
|
+
ssl: z.ZodOptional<z.ZodBoolean>;
|
|
88
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
89
|
+
host: z.ZodOptional<z.ZodString>;
|
|
90
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
91
|
+
user: z.ZodOptional<z.ZodString>;
|
|
92
|
+
password: z.ZodOptional<z.ZodString>;
|
|
93
|
+
database: z.ZodOptional<z.ZodString>;
|
|
94
|
+
table: z.ZodOptional<z.ZodString>;
|
|
95
|
+
pendingTable: z.ZodOptional<z.ZodString>;
|
|
96
|
+
type: z.ZodLiteral<"sqlserver">;
|
|
97
|
+
encrypt: z.ZodOptional<z.ZodBoolean>;
|
|
98
|
+
trustServerCertificate: z.ZodOptional<z.ZodBoolean>;
|
|
99
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
100
|
+
pendingTable: z.ZodOptional<z.ZodString>;
|
|
101
|
+
type: z.ZodLiteral<"sqlite">;
|
|
102
|
+
path: z.ZodString;
|
|
103
|
+
table: z.ZodString;
|
|
104
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
68
105
|
type: z.ZodLiteral<"sheets">;
|
|
69
106
|
spreadsheetId: z.ZodOptional<z.ZodString>;
|
|
70
107
|
credentialsPath: z.ZodOptional<z.ZodString>;
|
|
@@ -74,6 +111,7 @@ export declare const etlConfigSchema: z.ZodObject<{
|
|
|
74
111
|
path: z.ZodString;
|
|
75
112
|
delimiter: z.ZodOptional<z.ZodString>;
|
|
76
113
|
bom: z.ZodOptional<z.ZodBoolean>;
|
|
114
|
+
escapeFormulas: z.ZodOptional<z.ZodBoolean>;
|
|
77
115
|
}, z.core.$strip>, z.ZodObject<{
|
|
78
116
|
type: z.ZodLiteral<"json">;
|
|
79
117
|
path: z.ZodString;
|
package/dist/env.d.ts
CHANGED
package/dist/env.js
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireEnv = requireEnv;
|
|
4
3
|
exports.getOptionalEnv = getOptionalEnv;
|
|
5
|
-
function requireEnv(name) {
|
|
6
|
-
const value = process.env[name];
|
|
7
|
-
if (!value) {
|
|
8
|
-
throw new Error(`Variável de ambiente ausente: ${name}`);
|
|
9
|
-
}
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
4
|
function getOptionalEnv(name) {
|
|
13
5
|
return process.env[name];
|
|
14
6
|
}
|
|
15
|
-
;
|
package/dist/filters/combine.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CombineFilter = void 0;
|
|
4
|
+
const cells_1 = require("../cells");
|
|
4
5
|
class CombineFilter {
|
|
5
6
|
combinations;
|
|
6
7
|
constructor(combinations) {
|
|
@@ -31,24 +32,24 @@ class CombineFilter {
|
|
|
31
32
|
for (const [column, value] of Object.entries(row)) {
|
|
32
33
|
const into = anchorOf.get(column);
|
|
33
34
|
if (into !== undefined)
|
|
34
|
-
result
|
|
35
|
+
(0, cells_1.setCell)(result, into, combined.get(into) ?? null);
|
|
35
36
|
if (combined.has(column)) {
|
|
36
|
-
result
|
|
37
|
+
(0, cells_1.setCell)(result, column, combined.get(column) ?? null);
|
|
37
38
|
}
|
|
38
39
|
else if (!sourcesToRemove.has(column)) {
|
|
39
|
-
result
|
|
40
|
+
(0, cells_1.setCell)(result, column, value);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
for (const [into, value] of combined) {
|
|
43
|
-
if (!(
|
|
44
|
-
result
|
|
44
|
+
if (!(0, cells_1.hasCell)(result, into))
|
|
45
|
+
(0, cells_1.setCell)(result, into, value);
|
|
45
46
|
}
|
|
46
47
|
return result;
|
|
47
48
|
}
|
|
48
49
|
combine(row, { columns, separator = " " }) {
|
|
49
50
|
const values = [];
|
|
50
51
|
for (const column of columns) {
|
|
51
|
-
const raw = row
|
|
52
|
+
const raw = (0, cells_1.cellOf)(row, column);
|
|
52
53
|
const text = raw === null || raw === undefined ? "" : String(raw).trim();
|
|
53
54
|
if (text === "")
|
|
54
55
|
return null;
|
package/dist/filters/dedupe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DedupeFilter = void 0;
|
|
4
|
+
const cells_1 = require("../cells");
|
|
4
5
|
class DedupeFilter {
|
|
5
6
|
column;
|
|
6
7
|
strategy;
|
|
@@ -11,7 +12,7 @@ class DedupeFilter {
|
|
|
11
12
|
this.normalizer = normalizer;
|
|
12
13
|
}
|
|
13
14
|
keyOf(row) {
|
|
14
|
-
const raw = row
|
|
15
|
+
const raw = (0, cells_1.cellOf)(row, this.column);
|
|
15
16
|
if (raw === null || raw === undefined)
|
|
16
17
|
return undefined;
|
|
17
18
|
if (!this.normalizer)
|