datera 0.1.0 → 0.2.1

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +18 -7
  3. package/dist/api/formats.d.ts +4 -0
  4. package/dist/api/formats.js +9 -1
  5. package/dist/api/inspect.js +2 -1
  6. package/dist/cells.d.ts +5 -0
  7. package/dist/cells.js +24 -0
  8. package/dist/cli.js +2 -0
  9. package/dist/commands.js +9 -2
  10. package/dist/config/check.js +4 -0
  11. package/dist/config/ioSchema.d.ts +38 -0
  12. package/dist/config/ioSchema.js +30 -0
  13. package/dist/config/load.js +34 -6
  14. package/dist/config/schema.d.ts +38 -0
  15. package/dist/env.d.ts +0 -1
  16. package/dist/env.js +0 -9
  17. package/dist/filters/combine.js +7 -6
  18. package/dist/filters/dedupe.js +2 -1
  19. package/dist/filters/fillEmpty.js +5 -4
  20. package/dist/filters/merge.js +4 -3
  21. package/dist/filters/mergeStrategies.js +12 -11
  22. package/dist/filters/mergeUnkeyed.js +2 -3
  23. package/dist/filters/validate.js +3 -2
  24. package/dist/filters/validateTypes.d.ts +0 -5
  25. package/dist/io/csv/csvFormat.d.ts +1 -0
  26. package/dist/io/csv/csvFormat.js +8 -0
  27. package/dist/io/csv/csvSink.d.ts +1 -0
  28. package/dist/io/csv/csvSink.js +9 -5
  29. package/dist/io/csv/csvSource.js +2 -2
  30. package/dist/io/excel/excelSink.js +3 -2
  31. package/dist/io/excel/excelSource.js +2 -1
  32. package/dist/io/factory.js +57 -6
  33. package/dist/io/json/jsonSource.js +6 -5
  34. package/dist/io/mysql/client.d.ts +1 -0
  35. package/dist/io/mysql/client.js +1 -0
  36. package/dist/io/mysql/mysqlSink.d.ts +20 -0
  37. package/dist/io/mysql/mysqlSink.js +89 -0
  38. package/dist/io/parquet/parquetSink.js +2 -1
  39. package/dist/io/parquet/parquetSource.js +3 -4
  40. package/dist/io/postgres/postgresSink.d.ts +16 -0
  41. package/dist/io/postgres/postgresSink.js +82 -0
  42. package/dist/io/postgres/postgresSource.d.ts +1 -1
  43. package/dist/io/safety.d.ts +1 -1
  44. package/dist/io/safety.js +105 -6
  45. package/dist/io/servers.d.ts +14 -0
  46. package/dist/io/servers.js +72 -0
  47. package/dist/io/sheets/client.js +2 -1
  48. package/dist/io/sheets/sheetsSource.js +2 -1
  49. package/dist/io/sql/cell.js +2 -1
  50. package/dist/io/sql/target.d.ts +8 -0
  51. package/dist/io/sql/target.js +14 -0
  52. package/dist/io/sql/writer.d.ts +46 -0
  53. package/dist/io/sql/writer.js +264 -0
  54. package/dist/io/sqlite/sqlite.d.ts +3 -0
  55. package/dist/io/sqlite/sqlite.js +11 -0
  56. package/dist/io/sqlite/sqliteSink.d.ts +17 -0
  57. package/dist/io/sqlite/sqliteSink.js +69 -0
  58. package/dist/io/sqlite/sqliteSource.js +2 -9
  59. package/dist/io/sqlserver/sqlServerSink.d.ts +20 -0
  60. package/dist/io/sqlserver/sqlServerSink.js +107 -0
  61. package/dist/io/xml/xmlParse.js +4 -2
  62. package/dist/io/xml/xmlSource.js +7 -6
  63. package/dist/pipeline/runEtl.js +2 -1
  64. package/package.json +10 -5
package/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
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.1] - 2026-09-30
14
+
15
+ ### Corrigido
16
+
17
+ - no Windows, o app recusava arquivo de código numa subpasta que ainda não existia, quando a pasta da configuração tinha um nome curto do Windows (tipo `RUNNER~1`) ou passava por um atalho
18
+ - as actions do GitHub foram pra versões que rodam no Node 24
19
+
20
+ ## [0.2.0] - 2026-09-29
21
+
22
+ ### Adicionado
23
+
24
+ - 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
25
+ - 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
26
+ - 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
27
+ - 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ê
28
+ - variáveis `DEST_DB_*` no `.env` pro banco de destino. Se for o mesmo banco da fonte, o que faltar é copiado dela
29
+ - `--output-table` no `convert`, `dedupe` e `merge`, e as opções `table` e `pendingTable` no `writeRows` e no `convert`
30
+ - 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
31
+ - 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
32
+
33
+ ### Mudou
34
+
35
+ - os testes usam só os dados da escola de música inventada, sem "clientes" nem telefone
36
+ - o `config.json.example` virou uma config de verdade, que já roda lendo o CSV de exemplo
37
+
38
+ ### Segurança
39
+
40
+ - 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`
41
+ - no app, a primeira exportação de cada configuração mostra de onde ela lê e onde grava, e só continua se você confirmar
42
+ - a senha da fonte só é reaproveitada no destino quando os dois são o mesmo servidor
43
+ - `ssl` no MySQL, pra conexão criptografada
44
+ - os instaladores desligam os recursos do Electron que deixariam usar o app como um Node.js escondido
45
+ - nome de tabela ou coluna com caractere invisível é recusado antes de gravar no banco
46
+
47
+ ### Removido
48
+
49
+ - os scripts `test:config` e `test:connection`, que faziam o mesmo que o `datera validate` e o `--dry-run`
50
+
51
+ ## [0.1.0] - 2026-09-29
52
+
53
+ Primeira versão publicada no npm e na aba Releases.
54
+
55
+ ### Adicionado
56
+
57
+ - 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
58
+ - modos `raw`, `dedupe` e `merge`, com normalizadores (`trim`, `lowercase`, `digitsOnly`, `alphanumeric`) e os seus próprios
59
+ - preparação (`fillEmpty`, `combineColumns`, `distribute`) e validação com a lista de pendências e o motivo de cada linha
60
+ - app Datera, pra quem não programa, com passo a passo, regras em frases, prévia, histórico e tema escuro
61
+ - app Datera Dev, com editor da config, erros marcados na linha, paleta de comandos e teste de normalizador
62
+ - instaladores pra Windows gerados sozinhos a cada tag de versão
63
+ - comando `datera` com `run`, `validate`, `init`, `convert`, `dedupe`, `merge`, `columns`, `preview`, `normalizers` e `normalize`
64
+ - funções pra usar no código: `readRows`, `writeRows`, `convert`, `clean`, `fillEmpty`, `combineColumns`, `validate`, `dedupe`, `merge`, `describeColumns`, `defineConfig` e `onStep` pra acompanhar o progresso
65
+ - `dedupeKeyNormalizer`, pra comparar repetidos depois de passar por um normalizador
66
+ - os pacotes de Excel, Parquet, Google Sheets e dos bancos são opcionais: só instala quem for usar
67
+
68
+ ### Segurança
69
+
70
+ - arquivos de código citados na config só rodam depois de confirmados no app
71
+ - a senha do banco fica no `.env`, e o app só lê do `.env` as variáveis que conhece
72
+ - as telas não navegam pra fora do app nem abrem janelas novas
73
+ - o leitor de XML recusa `<!DOCTYPE>`
74
+ - 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 | Pra quem | O que tem |
53
- | -------------- | --------------------------------------------- | ------------------------------------------------------------------------- |
54
- | **Datera** | quem cuida dos dados mas não programa | app com tela, passo a passo, regras em frases, gráficos e histórico |
55
- | **Datera Dev** | quem programa | app escuro com editor da config, prévia na hora, log e paleta de comandos |
56
- | **Terminal** | quem quer automatizar ou usar em outro código | o comando `datera`, `--dry-run` e a função `runEtl` |
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 é chegar num instalador de um clique pra quem não programa e numa biblioteca no npm pra quem programa.
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
 
@@ -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
  }
@@ -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
- throw new Error("Ainda não dá pra gravar em SQLite. Escolha outro formato pra saída.");
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
  }
@@ -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[name])
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 {
@@ -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
- (0, safety_1.assertDifferentFiles)(cli.input, cli.output);
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
- (0, safety_1.assertDifferentFiles)(cli.input, cli.output);
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,
@@ -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;
@@ -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"),
@@ -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?.type !== "mysql" &&
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: (0, env_1.getOptionalEnv)("DB_PASSWORD") ?? source.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: (0, env_1.getOptionalEnv)("DB_PASSWORD") ?? jsonConfig.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);
@@ -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
@@ -1,2 +1 @@
1
- export declare function requireEnv(name: string): string;
2
1
  export declare function getOptionalEnv(name: string): string | undefined;
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
- ;
@@ -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[into] = combined.get(into) ?? null;
35
+ (0, cells_1.setCell)(result, into, combined.get(into) ?? null);
35
36
  if (combined.has(column)) {
36
- result[column] = combined.get(column) ?? null;
37
+ (0, cells_1.setCell)(result, column, combined.get(column) ?? null);
37
38
  }
38
39
  else if (!sourcesToRemove.has(column)) {
39
- result[column] = value;
40
+ (0, cells_1.setCell)(result, column, value);
40
41
  }
41
42
  }
42
43
  for (const [into, value] of combined) {
43
- if (!(into in result))
44
- result[into] = value;
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[column];
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;
@@ -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[this.column];
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)