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.
Files changed (64) hide show
  1. package/CHANGELOG.md +67 -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,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 | 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)