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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MARK_PREFIX = exports.OLD_SUFFIX = exports.NEW_SUFFIX = exports.OWNED_TABLES = void 0;
|
|
7
|
+
exports.newToken = newToken;
|
|
8
|
+
exports.quoteName = quoteName;
|
|
9
|
+
exports.tableParts = tableParts;
|
|
10
|
+
exports.quoteTableName = quoteTableName;
|
|
11
|
+
exports.nameRoom = nameRoom;
|
|
12
|
+
exports.checkTableName = checkTableName;
|
|
13
|
+
exports.checkColumns = checkColumns;
|
|
14
|
+
exports.columnKind = columnKind;
|
|
15
|
+
exports.writeTable = writeTable;
|
|
16
|
+
const header_1 = require("../header");
|
|
17
|
+
const cells_1 = require("../../cells");
|
|
18
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
19
|
+
exports.OWNED_TABLES = "datera_tabelas";
|
|
20
|
+
exports.NEW_SUFFIX = "__datera_novo";
|
|
21
|
+
exports.OLD_SUFFIX = "__datera_velho";
|
|
22
|
+
exports.MARK_PREFIX = "datera:";
|
|
23
|
+
function newToken() {
|
|
24
|
+
return `${exports.MARK_PREFIX}${crypto_1.default.randomUUID()}`;
|
|
25
|
+
}
|
|
26
|
+
function quoteName(dialect, name) {
|
|
27
|
+
const { open, close } = dialect;
|
|
28
|
+
return `${open}${name.split(close).join(close + close)}${close}`;
|
|
29
|
+
}
|
|
30
|
+
function tableParts(dialect, table) {
|
|
31
|
+
const trimmed = table.trim();
|
|
32
|
+
const parts = dialect.schemas
|
|
33
|
+
? trimmed.split(".").map((part) => part.trim())
|
|
34
|
+
: [trimmed];
|
|
35
|
+
if (parts.length > 2 || parts.some((part) => part === "")) {
|
|
36
|
+
throw new Error(`Nome de tabela inválido: "${table}".`);
|
|
37
|
+
}
|
|
38
|
+
return parts;
|
|
39
|
+
}
|
|
40
|
+
function quoteTableName(dialect, table) {
|
|
41
|
+
return tableParts(dialect, table)
|
|
42
|
+
.map((part) => quoteName(dialect, part))
|
|
43
|
+
.join(".");
|
|
44
|
+
}
|
|
45
|
+
function nameRoom(dialect) {
|
|
46
|
+
return dialect.transactionalDdl ? 0 : exports.OLD_SUFFIX.length;
|
|
47
|
+
}
|
|
48
|
+
function checkName(dialect, name, what) {
|
|
49
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) {
|
|
50
|
+
throw new Error(`${what} "${name.replace(/[\u0000-\u001f\u007f]/g, "?")}" tem um caractere invisível no nome (tipo quebra de linha ou tab). Tire ele antes de gravar no banco.`);
|
|
51
|
+
}
|
|
52
|
+
if (dialect.nameLength(name) > dialect.maxNameLength) {
|
|
53
|
+
throw new Error(`${what} "${name}" tem um nome comprido demais pro ${dialect.label}, que aceita até ${dialect.maxNameLength} caracteres. Use um nome mais curto.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function checkTableName(dialect, table, extra = 0) {
|
|
57
|
+
const parts = tableParts(dialect, table);
|
|
58
|
+
const last = parts[parts.length - 1];
|
|
59
|
+
if (dialect.nameLength(last) + extra > dialect.maxNameLength) {
|
|
60
|
+
throw new Error(`A tabela "${table}" tem um nome comprido demais pro ${dialect.label}. Use um nome de até ${dialect.maxNameLength - extra} caracteres.`);
|
|
61
|
+
}
|
|
62
|
+
for (const part of parts)
|
|
63
|
+
checkName(dialect, part, "A tabela");
|
|
64
|
+
}
|
|
65
|
+
function checkColumns(dialect, header) {
|
|
66
|
+
const seen = new Map();
|
|
67
|
+
for (const column of header) {
|
|
68
|
+
if (column.trim() === "") {
|
|
69
|
+
throw new Error("Tem uma coluna sem nome. Dê um nome pra ela antes de gravar no banco.");
|
|
70
|
+
}
|
|
71
|
+
checkName(dialect, column, "A coluna");
|
|
72
|
+
const key = column.toLowerCase();
|
|
73
|
+
const other = seen.get(key);
|
|
74
|
+
if (other !== undefined) {
|
|
75
|
+
throw new Error(`As colunas "${other}" e "${column}" só mudam nas maiúsculas, e o ${dialect.label} pode achar que são a mesma. Troque o nome de uma delas.`);
|
|
76
|
+
}
|
|
77
|
+
seen.set(key, column);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function columnKind(rows, column) {
|
|
81
|
+
let kind = null;
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
const value = (0, cells_1.cellOf)(row, column);
|
|
84
|
+
if (value === null || value === undefined || value === "")
|
|
85
|
+
continue;
|
|
86
|
+
if (typeof value !== "number")
|
|
87
|
+
return "text";
|
|
88
|
+
kind =
|
|
89
|
+
kind === "number" || !Number.isSafeInteger(value)
|
|
90
|
+
? "number"
|
|
91
|
+
: "integer";
|
|
92
|
+
}
|
|
93
|
+
return kind ?? "text";
|
|
94
|
+
}
|
|
95
|
+
function valueFor(kind, value) {
|
|
96
|
+
if (value === null || value === undefined || value === "")
|
|
97
|
+
return null;
|
|
98
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
99
|
+
return null;
|
|
100
|
+
return kind === "text" ? String(value) : value;
|
|
101
|
+
}
|
|
102
|
+
function createTableSql(dialect, table, header, kinds, token) {
|
|
103
|
+
const columns = header.map((column, index) => `${quoteName(dialect, column)} ${dialect.types[kinds[index]]}`);
|
|
104
|
+
const mark = dialect.tableMark(token);
|
|
105
|
+
return `CREATE TABLE ${quoteTableName(dialect, table)} (${mark.inside}${columns.join(", ")})${dialect.tableSuffix}${mark.after}`;
|
|
106
|
+
}
|
|
107
|
+
async function insertRows(session, dialect, table, header, kinds, rows) {
|
|
108
|
+
const perInsert = Math.max(1, Math.min(dialect.maxRowsPerInsert, Math.floor(dialect.maxParams / header.length)));
|
|
109
|
+
const columns = header.map((column) => quoteName(dialect, column));
|
|
110
|
+
const target = quoteTableName(dialect, table);
|
|
111
|
+
for (let start = 0; start < rows.length; start += perInsert) {
|
|
112
|
+
const batch = rows.slice(start, start + perInsert);
|
|
113
|
+
const params = [];
|
|
114
|
+
const tuples = batch.map((row) => {
|
|
115
|
+
const marks = header.map((column, index) => {
|
|
116
|
+
params.push(valueFor(kinds[index], (0, cells_1.cellOf)(row, column)));
|
|
117
|
+
return dialect.param(params.length);
|
|
118
|
+
});
|
|
119
|
+
return `(${marks.join(", ")})`;
|
|
120
|
+
});
|
|
121
|
+
await session.run(`INSERT INTO ${target} (${columns.join(", ")}) VALUES ${tuples.join(", ")}`, params);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function ownedTablesName(dialect) {
|
|
125
|
+
return quoteName(dialect, exports.OWNED_TABLES);
|
|
126
|
+
}
|
|
127
|
+
async function ensureOwnedTables(session, dialect) {
|
|
128
|
+
if (await dialect.tableExists(session, exports.OWNED_TABLES)) {
|
|
129
|
+
if (dialect.ownsTable &&
|
|
130
|
+
!(await dialect.ownsTable(session, exports.OWNED_TABLES))) {
|
|
131
|
+
throw new Error(`A tabela "${exports.OWNED_TABLES}", onde o Datera anota as tabelas que ele criou, foi criada por outro usuário do ${dialect.label}. Por segurança, ele não confia nela. Use outro banco ou schema, ou peça pra quem cuida do banco apagar essa tabela.`);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await session.run(`CREATE TABLE ${ownedTablesName(dialect)} (${quoteName(dialect, "tabela")} ${dialect.keyType} PRIMARY KEY, ${quoteName(dialect, "identificador")} ${dialect.types.text}, ${quoteName(dialect, "atualizada_em")} ${dialect.types.text})${dialect.tableSuffix}`);
|
|
136
|
+
}
|
|
137
|
+
async function isOwned(session, dialect, table) {
|
|
138
|
+
const found = await session.all(`SELECT ${quoteName(dialect, "identificador")} AS ${quoteName(dialect, "identificador")} FROM ${ownedTablesName(dialect)} WHERE ${quoteName(dialect, "tabela")} = ${dialect.param(1)}`, [table]);
|
|
139
|
+
const saved = found[0]?.identificador;
|
|
140
|
+
if (typeof saved !== "string" || saved === "")
|
|
141
|
+
return false;
|
|
142
|
+
const current = await dialect.tableId(session, table);
|
|
143
|
+
return current !== null && current === saved;
|
|
144
|
+
}
|
|
145
|
+
async function markOwned(session, dialect, table, id) {
|
|
146
|
+
await unmarkOwned(session, dialect, table);
|
|
147
|
+
await session.run(`INSERT INTO ${ownedTablesName(dialect)} (${quoteName(dialect, "tabela")}, ${quoteName(dialect, "identificador")}, ${quoteName(dialect, "atualizada_em")}) VALUES (${dialect.param(1)}, ${dialect.param(2)}, ${dialect.param(3)})`, [table, id, new Date().toISOString()]);
|
|
148
|
+
}
|
|
149
|
+
async function unmarkOwned(session, dialect, table) {
|
|
150
|
+
await session.run(`DELETE FROM ${ownedTablesName(dialect)} WHERE ${quoteName(dialect, "tabela")} = ${dialect.param(1)}`, [table]);
|
|
151
|
+
}
|
|
152
|
+
async function markCurrent(session, dialect, table) {
|
|
153
|
+
const id = await dialect.tableId(session, table);
|
|
154
|
+
if (id === null) {
|
|
155
|
+
throw new Error(`Não consegui confirmar que a tabela "${table}" foi criada no ${dialect.label}.`);
|
|
156
|
+
}
|
|
157
|
+
await markOwned(session, dialect, table, id);
|
|
158
|
+
}
|
|
159
|
+
function notOwned(dialect, table) {
|
|
160
|
+
return new Error(`A tabela "${table}" já existe no ${dialect.label} e não foi o Datera que criou (ou foi apagada e criada de novo por outra pessoa). Pra não apagar nada seu, ele só grava em tabela nova ou em tabela que ele mesmo criou antes. Escolha outro nome.`);
|
|
161
|
+
}
|
|
162
|
+
async function inTransaction(session, work) {
|
|
163
|
+
await session.begin();
|
|
164
|
+
try {
|
|
165
|
+
await work();
|
|
166
|
+
await session.commit();
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
await session.rollback().catch(() => undefined);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function replaceInTransaction(session, dialect, table, rows, header, kinds) {
|
|
174
|
+
let created = true;
|
|
175
|
+
await inTransaction(session, async () => {
|
|
176
|
+
await ensureOwnedTables(session, dialect);
|
|
177
|
+
const exists = await dialect.tableExists(session, table);
|
|
178
|
+
if (exists && !(await isOwned(session, dialect, table))) {
|
|
179
|
+
throw notOwned(dialect, table);
|
|
180
|
+
}
|
|
181
|
+
if (header.length === 0) {
|
|
182
|
+
created = exists;
|
|
183
|
+
if (exists) {
|
|
184
|
+
await session.run(`DELETE FROM ${quoteTableName(dialect, table)}`);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (exists) {
|
|
189
|
+
await session.run(`DROP TABLE ${quoteTableName(dialect, table)}`);
|
|
190
|
+
}
|
|
191
|
+
await session.run(createTableSql(dialect, table, header, kinds, newToken()));
|
|
192
|
+
await insertRows(session, dialect, table, header, kinds, rows);
|
|
193
|
+
await markCurrent(session, dialect, table);
|
|
194
|
+
});
|
|
195
|
+
return created;
|
|
196
|
+
}
|
|
197
|
+
async function dropOwned(session, dialect, table) {
|
|
198
|
+
if (!(await dialect.tableExists(session, table)))
|
|
199
|
+
return;
|
|
200
|
+
if (!(await isOwned(session, dialect, table))) {
|
|
201
|
+
throw notOwned(dialect, table);
|
|
202
|
+
}
|
|
203
|
+
await session.run(`DROP TABLE ${quoteTableName(dialect, table)}`);
|
|
204
|
+
await inTransaction(session, () => unmarkOwned(session, dialect, table));
|
|
205
|
+
}
|
|
206
|
+
async function replaceBySwap(session, dialect, table, rows, header, kinds) {
|
|
207
|
+
checkTableName(dialect, table, nameRoom(dialect));
|
|
208
|
+
await ensureOwnedTables(session, dialect);
|
|
209
|
+
const exists = await dialect.tableExists(session, table);
|
|
210
|
+
if (exists && !(await isOwned(session, dialect, table))) {
|
|
211
|
+
throw notOwned(dialect, table);
|
|
212
|
+
}
|
|
213
|
+
if (header.length === 0) {
|
|
214
|
+
if (!exists)
|
|
215
|
+
return false;
|
|
216
|
+
await inTransaction(session, async () => {
|
|
217
|
+
await session.run(`DELETE FROM ${quoteTableName(dialect, table)}`);
|
|
218
|
+
});
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
const fresh = `${table}${exports.NEW_SUFFIX}`;
|
|
222
|
+
const old = `${table}${exports.OLD_SUFFIX}`;
|
|
223
|
+
await dropOwned(session, dialect, fresh);
|
|
224
|
+
await dropOwned(session, dialect, old);
|
|
225
|
+
const token = newToken();
|
|
226
|
+
await inTransaction(session, () => markOwned(session, dialect, fresh, token));
|
|
227
|
+
try {
|
|
228
|
+
await session.run(createTableSql(dialect, fresh, header, kinds, token));
|
|
229
|
+
const oldId = exists ? await dialect.tableId(session, table) : null;
|
|
230
|
+
await inTransaction(session, async () => {
|
|
231
|
+
await insertRows(session, dialect, fresh, header, kinds, rows);
|
|
232
|
+
if (oldId !== null)
|
|
233
|
+
await markOwned(session, dialect, old, oldId);
|
|
234
|
+
});
|
|
235
|
+
const target = quoteTableName(dialect, table);
|
|
236
|
+
const freshName = quoteTableName(dialect, fresh);
|
|
237
|
+
await session.run(exists
|
|
238
|
+
? `RENAME TABLE ${target} TO ${quoteTableName(dialect, old)}, ${freshName} TO ${target}`
|
|
239
|
+
: `RENAME TABLE ${freshName} TO ${target}`);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
await dropOwned(session, dialect, fresh).catch(() => undefined);
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
await inTransaction(session, async () => {
|
|
246
|
+
await unmarkOwned(session, dialect, fresh);
|
|
247
|
+
await markOwned(session, dialect, table, token);
|
|
248
|
+
});
|
|
249
|
+
await dropOwned(session, dialect, old);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
async function writeTable(session, dialect, table, rows, logger) {
|
|
253
|
+
const name = table.trim();
|
|
254
|
+
checkTableName(dialect, name);
|
|
255
|
+
const header = (0, header_1.buildHeader)(rows);
|
|
256
|
+
checkColumns(dialect, header);
|
|
257
|
+
const kinds = header.map((column) => columnKind(rows, column));
|
|
258
|
+
const created = dialect.transactionalDdl
|
|
259
|
+
? await replaceInTransaction(session, dialect, name, rows, header, kinds)
|
|
260
|
+
: await replaceBySwap(session, dialect, name, rows, header, kinds);
|
|
261
|
+
logger.info(created
|
|
262
|
+
? `${rows.length} ${rows.length === 1 ? "linha gravada" : "linhas gravadas"} na tabela "${name}" (${dialect.label}).`
|
|
263
|
+
: `Nenhuma linha pra tabela "${name}" (${dialect.label}), então ela não foi criada.`);
|
|
264
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadSqlite = loadSqlite;
|
|
4
|
+
function loadSqlite() {
|
|
5
|
+
try {
|
|
6
|
+
return require("node:sqlite");
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
throw new Error("Essa versão do Node não sabe usar SQLite. Atualize pro Node 22.13 ou mais novo.");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { TableRow } from "../../types";
|
|
3
|
+
import { Sink, SinkWriteOptions } from "../types";
|
|
4
|
+
import { Logger } from "../../logger";
|
|
5
|
+
import { SqlDialect, SqlSession } from "../sql/writer";
|
|
6
|
+
import { TableTarget } from "../sql/target";
|
|
7
|
+
export interface SqliteSinkOptions extends TableTarget {
|
|
8
|
+
path: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const SQLITE_DIALECT: SqlDialect;
|
|
11
|
+
export declare function sqliteSession(database: DatabaseSync): SqlSession;
|
|
12
|
+
export declare class SqliteSink implements Sink {
|
|
13
|
+
private readonly options;
|
|
14
|
+
private readonly logger;
|
|
15
|
+
constructor(options: SqliteSinkOptions, logger?: Logger);
|
|
16
|
+
write(rows: TableRow[], options?: SinkWriteOptions): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SqliteSink = exports.SQLITE_DIALECT = void 0;
|
|
7
|
+
exports.sqliteSession = sqliteSession;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const logger_1 = require("../../logger");
|
|
11
|
+
const writer_1 = require("../sql/writer");
|
|
12
|
+
const target_1 = require("../sql/target");
|
|
13
|
+
const sqlite_1 = require("./sqlite");
|
|
14
|
+
exports.SQLITE_DIALECT = {
|
|
15
|
+
label: "SQLite",
|
|
16
|
+
open: '"',
|
|
17
|
+
close: '"',
|
|
18
|
+
schemas: false,
|
|
19
|
+
maxNameLength: 1000,
|
|
20
|
+
nameLength: (name) => name.length,
|
|
21
|
+
maxParams: 999,
|
|
22
|
+
maxRowsPerInsert: 500,
|
|
23
|
+
transactionalDdl: true,
|
|
24
|
+
types: { integer: "INTEGER", number: "REAL", text: "TEXT" },
|
|
25
|
+
keyType: "TEXT",
|
|
26
|
+
tableSuffix: "",
|
|
27
|
+
param: () => "?",
|
|
28
|
+
tableMark: (token) => ({ inside: `/* ${token} */ `, after: "" }),
|
|
29
|
+
tableId: async (session, table) => {
|
|
30
|
+
const [found] = await session.all("SELECT sql FROM sqlite_master WHERE type = 'table' AND lower(name) = lower(?)", [table.trim()]);
|
|
31
|
+
const sql = typeof found?.sql === "string" ? found.sql : "";
|
|
32
|
+
return sql.match(/\/\* (datera:[0-9a-f-]+) \*\//)?.[1] ?? null;
|
|
33
|
+
},
|
|
34
|
+
tableExists: async (session, table) => (await session.all("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND lower(name) = lower(?)", [table.trim()])).length > 0,
|
|
35
|
+
};
|
|
36
|
+
function sqliteSession(database) {
|
|
37
|
+
const params = (values = []) => values;
|
|
38
|
+
return {
|
|
39
|
+
run: async (sql, values) => {
|
|
40
|
+
database.prepare(sql).run(...params(values));
|
|
41
|
+
},
|
|
42
|
+
all: async (sql, values) => database.prepare(sql).all(...params(values)),
|
|
43
|
+
begin: async () => database.exec("BEGIN IMMEDIATE"),
|
|
44
|
+
commit: async () => database.exec("COMMIT"),
|
|
45
|
+
rollback: async () => database.exec("ROLLBACK"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
class SqliteSink {
|
|
49
|
+
options;
|
|
50
|
+
logger;
|
|
51
|
+
constructor(options, logger = logger_1.consoleLogger) {
|
|
52
|
+
this.options = options;
|
|
53
|
+
this.logger = logger;
|
|
54
|
+
}
|
|
55
|
+
async write(rows, options = {}) {
|
|
56
|
+
const folder = path_1.default.dirname(this.options.path);
|
|
57
|
+
if (folder)
|
|
58
|
+
fs_1.default.mkdirSync(folder, { recursive: true });
|
|
59
|
+
const { DatabaseSync } = (0, sqlite_1.loadSqlite)();
|
|
60
|
+
const database = new DatabaseSync(this.options.path);
|
|
61
|
+
try {
|
|
62
|
+
await (0, writer_1.writeTable)(sqliteSession(database), exports.SQLITE_DIALECT, (0, target_1.tableFor)(this.options, options), rows, this.logger);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
database.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.SqliteSink = SqliteSink;
|
|
@@ -7,14 +7,7 @@ exports.SqliteSource = void 0;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const names_1 = require("../sql/names");
|
|
9
9
|
const cell_1 = require("../sql/cell");
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
return require("node:sqlite");
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
throw new Error("Essa versão do Node não sabe ler SQLite. Atualize pro Node 22.13 ou mais novo.");
|
|
16
|
-
}
|
|
17
|
-
}
|
|
10
|
+
const sqlite_1 = require("./sqlite");
|
|
18
11
|
class SqliteSource {
|
|
19
12
|
options;
|
|
20
13
|
constructor(options) {
|
|
@@ -24,7 +17,7 @@ class SqliteSource {
|
|
|
24
17
|
if (!fs_1.default.existsSync(this.options.path)) {
|
|
25
18
|
throw new Error(`Arquivo não encontrado: ${this.options.path}`);
|
|
26
19
|
}
|
|
27
|
-
const { DatabaseSync } = loadSqlite();
|
|
20
|
+
const { DatabaseSync } = (0, sqlite_1.loadSqlite)();
|
|
28
21
|
const database = new DatabaseSync(this.options.path, {
|
|
29
22
|
readOnly: true,
|
|
30
23
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TableRow } from "../../types";
|
|
2
|
+
import { Sink, SinkWriteOptions } from "../types";
|
|
3
|
+
import { Logger } from "../../logger";
|
|
4
|
+
import { SqlDialect, SqlSession } from "../sql/writer";
|
|
5
|
+
import { TableTarget } from "../sql/target";
|
|
6
|
+
import { SqlServerConnection } from "./sqlServerSource";
|
|
7
|
+
export declare const SQLSERVER_DIALECT: SqlDialect;
|
|
8
|
+
export interface SqlServerSession extends SqlSession {
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export type SqlServerSessionFactory = (connection: SqlServerConnection) => Promise<SqlServerSession>;
|
|
12
|
+
export declare const createSqlServerSession: SqlServerSessionFactory;
|
|
13
|
+
export declare class SqlServerSink implements Sink {
|
|
14
|
+
private readonly connection;
|
|
15
|
+
private readonly target;
|
|
16
|
+
private readonly logger;
|
|
17
|
+
private readonly createSession;
|
|
18
|
+
constructor(connection: SqlServerConnection, target: TableTarget, logger?: Logger, createSession?: SqlServerSessionFactory);
|
|
19
|
+
write(rows: TableRow[], options?: SinkWriteOptions): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SqlServerSink = exports.createSqlServerSession = exports.SQLSERVER_DIALECT = void 0;
|
|
4
|
+
const logger_1 = require("../../logger");
|
|
5
|
+
const optional_1 = require("../optional");
|
|
6
|
+
const writer_1 = require("../sql/writer");
|
|
7
|
+
const target_1 = require("../sql/target");
|
|
8
|
+
exports.SQLSERVER_DIALECT = {
|
|
9
|
+
label: "SQL Server",
|
|
10
|
+
open: "[",
|
|
11
|
+
close: "]",
|
|
12
|
+
schemas: true,
|
|
13
|
+
maxNameLength: 128,
|
|
14
|
+
nameLength: (name) => name.length,
|
|
15
|
+
maxParams: 2000,
|
|
16
|
+
maxRowsPerInsert: 1000,
|
|
17
|
+
transactionalDdl: true,
|
|
18
|
+
types: { integer: "BIGINT", number: "FLOAT", text: "NVARCHAR(MAX)" },
|
|
19
|
+
keyType: "NVARCHAR(450) COLLATE Latin1_General_BIN2",
|
|
20
|
+
tableSuffix: "",
|
|
21
|
+
param: (index) => `@p${index}`,
|
|
22
|
+
tableMark: () => ({ inside: "", after: "" }),
|
|
23
|
+
tableId: async (session, table) => {
|
|
24
|
+
const [found] = await session.all("SELECT CAST(OBJECT_ID(@p1, 'U') AS NVARCHAR(20)) AS id", [(0, writer_1.quoteTableName)(exports.SQLSERVER_DIALECT, table)]);
|
|
25
|
+
return typeof found?.id === "string" ? found.id : null;
|
|
26
|
+
},
|
|
27
|
+
tableExists: async (session, table) => {
|
|
28
|
+
const [found] = await session.all("SELECT OBJECT_ID(@p1) AS id", [
|
|
29
|
+
(0, writer_1.quoteTableName)(exports.SQLSERVER_DIALECT, table),
|
|
30
|
+
]);
|
|
31
|
+
return found?.id !== null && found?.id !== undefined;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
function typeOf(mssql, value) {
|
|
35
|
+
if (typeof value === "number") {
|
|
36
|
+
return Number.isSafeInteger(value) ? mssql.BigInt : mssql.Float;
|
|
37
|
+
}
|
|
38
|
+
return mssql.NVarChar(mssql.MAX);
|
|
39
|
+
}
|
|
40
|
+
const createSqlServerSession = async (connection) => {
|
|
41
|
+
const mssql = (0, optional_1.requireOptional)(() => require("mssql"), ["mssql"], "SQL Server");
|
|
42
|
+
const pool = new mssql.ConnectionPool({
|
|
43
|
+
server: connection.host,
|
|
44
|
+
port: connection.port,
|
|
45
|
+
user: connection.user,
|
|
46
|
+
...(connection.password !== undefined
|
|
47
|
+
? { password: connection.password }
|
|
48
|
+
: {}),
|
|
49
|
+
database: connection.database,
|
|
50
|
+
options: {
|
|
51
|
+
encrypt: connection.encrypt ?? true,
|
|
52
|
+
trustServerCertificate: connection.trustServerCertificate ?? false,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
await pool.connect();
|
|
56
|
+
let transaction;
|
|
57
|
+
const request = (sql, values = []) => {
|
|
58
|
+
const next = new mssql.Request(transaction ?? pool);
|
|
59
|
+
values.forEach((value, index) => next.input(`p${index + 1}`, typeOf(mssql, value), value));
|
|
60
|
+
return next.query(sql);
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
run: async (sql, values) => {
|
|
64
|
+
await request(sql, values);
|
|
65
|
+
},
|
|
66
|
+
all: async (sql, values) => (await request(sql, values)).recordset ?? [],
|
|
67
|
+
begin: async () => {
|
|
68
|
+
transaction = new mssql.Transaction(pool);
|
|
69
|
+
await transaction.begin();
|
|
70
|
+
},
|
|
71
|
+
commit: async () => {
|
|
72
|
+
await transaction?.commit();
|
|
73
|
+
transaction = undefined;
|
|
74
|
+
},
|
|
75
|
+
rollback: async () => {
|
|
76
|
+
const current = transaction;
|
|
77
|
+
transaction = undefined;
|
|
78
|
+
await current?.rollback();
|
|
79
|
+
},
|
|
80
|
+
close: async () => {
|
|
81
|
+
await pool.close();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
exports.createSqlServerSession = createSqlServerSession;
|
|
86
|
+
class SqlServerSink {
|
|
87
|
+
connection;
|
|
88
|
+
target;
|
|
89
|
+
logger;
|
|
90
|
+
createSession;
|
|
91
|
+
constructor(connection, target, logger = logger_1.consoleLogger, createSession = exports.createSqlServerSession) {
|
|
92
|
+
this.connection = connection;
|
|
93
|
+
this.target = target;
|
|
94
|
+
this.logger = logger;
|
|
95
|
+
this.createSession = createSession;
|
|
96
|
+
}
|
|
97
|
+
async write(rows, options = {}) {
|
|
98
|
+
const session = await this.createSession(this.connection);
|
|
99
|
+
try {
|
|
100
|
+
await (0, writer_1.writeTable)(session, exports.SQLSERVER_DIALECT, (0, target_1.tableFor)(this.target, options), rows, this.logger);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await session.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.SqlServerSink = SqlServerSink;
|
package/dist/io/xml/xmlParse.js
CHANGED
|
@@ -78,7 +78,9 @@ function decode(value, reader, at) {
|
|
|
78
78
|
return value.replace(/&([^;&\s]*);?/g, (whole, entity) => {
|
|
79
79
|
if (!whole.endsWith(";"))
|
|
80
80
|
reader.fail("Tem um & solto no texto", at);
|
|
81
|
-
const named = NAMED_ENTITIES
|
|
81
|
+
const named = Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, entity)
|
|
82
|
+
? NAMED_ENTITIES[entity]
|
|
83
|
+
: undefined;
|
|
82
84
|
if (named !== undefined)
|
|
83
85
|
return named;
|
|
84
86
|
const code = entity.startsWith("#x")
|
|
@@ -110,7 +112,7 @@ function readElement(reader) {
|
|
|
110
112
|
reader.skip("<");
|
|
111
113
|
const element = {
|
|
112
114
|
name: reader.name(),
|
|
113
|
-
attributes:
|
|
115
|
+
attributes: Object.create(null),
|
|
114
116
|
children: [],
|
|
115
117
|
text: "",
|
|
116
118
|
};
|
package/dist/io/xml/xmlSource.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.XmlSource = void 0;
|
|
4
4
|
const files_1 = require("../files");
|
|
5
5
|
const xmlParse_1 = require("./xmlParse");
|
|
6
|
+
const cells_1 = require("../../cells");
|
|
6
7
|
function join(prefix, name) {
|
|
7
8
|
return prefix ? `${prefix}.${name}` : name;
|
|
8
9
|
}
|
|
@@ -21,13 +22,13 @@ function groupByName(elements) {
|
|
|
21
22
|
}
|
|
22
23
|
function flatten(element, prefix, row) {
|
|
23
24
|
for (const [name, value] of Object.entries(element.attributes)) {
|
|
24
|
-
row
|
|
25
|
+
(0, cells_1.setCell)(row, join(prefix, name), value === "" ? null : value);
|
|
25
26
|
}
|
|
26
27
|
if (prefix && element.children.length === 0) {
|
|
27
28
|
if (element.text !== "")
|
|
28
|
-
row
|
|
29
|
+
(0, cells_1.setCell)(row, prefix, element.text);
|
|
29
30
|
else if (isSimple(element))
|
|
30
|
-
row
|
|
31
|
+
(0, cells_1.setCell)(row, prefix, null);
|
|
31
32
|
}
|
|
32
33
|
for (const [name, group] of groupByName(element.children)) {
|
|
33
34
|
const key = join(prefix, name);
|
|
@@ -36,14 +37,14 @@ function flatten(element, prefix, row) {
|
|
|
36
37
|
flatten(first, key, row);
|
|
37
38
|
}
|
|
38
39
|
else if (group.every(isSimple)) {
|
|
39
|
-
row
|
|
40
|
+
(0, cells_1.setCell)(row, key, group.map((item) => item.text).join(", "));
|
|
40
41
|
}
|
|
41
42
|
else {
|
|
42
|
-
row
|
|
43
|
+
(0, cells_1.setCell)(row, key, JSON.stringify(group.map((item) => {
|
|
43
44
|
const inner = {};
|
|
44
45
|
flatten(item, "", inner);
|
|
45
46
|
return inner;
|
|
46
|
-
}));
|
|
47
|
+
})));
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
}
|
package/dist/pipeline/runEtl.js
CHANGED
|
@@ -10,11 +10,12 @@ const validate_1 = require("../filters/validate");
|
|
|
10
10
|
const normalizers_1 = require("../normalizers");
|
|
11
11
|
const logger_1 = require("../logger");
|
|
12
12
|
const steps_1 = require("./steps");
|
|
13
|
+
const cells_1 = require("../cells");
|
|
13
14
|
exports.DEFAULT_PREVIEW_SIZE = 5;
|
|
14
15
|
function countPendingReasons(pending, reasonColumn = validate_1.DEFAULT_REASON_COLUMN) {
|
|
15
16
|
const counts = new Map();
|
|
16
17
|
for (const row of pending) {
|
|
17
|
-
const text = row
|
|
18
|
+
const text = (0, cells_1.cellOf)(row, reasonColumn);
|
|
18
19
|
if (text === null || text === undefined)
|
|
19
20
|
continue;
|
|
20
21
|
for (const part of String(text).split(";")) {
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datera",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Limpa, unifica e organiza dados bagunçados de bancos de dados, planilhas, CSV, JSON, XML e Parquet, e
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Limpa, unifica e organiza dados bagunçados de bancos de dados, planilhas, CSV, JSON, XML e Parquet, e grava em bancos de dados, Google Sheets, Excel, CSV, JSON, XML ou Parquet.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
|
8
8
|
"datera": "dist/main.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"CHANGELOG.md"
|
|
12
13
|
],
|
|
13
14
|
"workspaces": [
|
|
14
15
|
"apps/*"
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
"scripts": {
|
|
17
18
|
"build": "tsc -p tsconfig.build.json",
|
|
18
19
|
"prepublishOnly": "npm run build && npm test",
|
|
20
|
+
"release:prepare": "tsx scripts/prepareRelease.ts",
|
|
19
21
|
"start": "tsx src/main.ts",
|
|
20
22
|
"validate": "tsx src/main.ts validate",
|
|
21
23
|
"example": "tsx src/main.ts --config examples/config.csv.json",
|
|
@@ -26,8 +28,9 @@
|
|
|
26
28
|
"test": "vitest run",
|
|
27
29
|
"test:watch": "vitest",
|
|
28
30
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"
|
|
30
|
-
"test:
|
|
31
|
+
"typecheck:telas": "tsc -p e2e",
|
|
32
|
+
"test:telas": "playwright test",
|
|
33
|
+
"test:telas:ver": "playwright test --headed --project=claro --workers=1",
|
|
31
34
|
"test:sheet": "tsx scripts/testSheetFake.ts",
|
|
32
35
|
"prepare": "husky"
|
|
33
36
|
},
|
|
@@ -38,6 +41,7 @@
|
|
|
38
41
|
"xml",
|
|
39
42
|
"parquet",
|
|
40
43
|
"sqlite",
|
|
44
|
+
"sqlserver",
|
|
41
45
|
"postgres",
|
|
42
46
|
"mysql",
|
|
43
47
|
"dedupe",
|
|
@@ -61,6 +65,7 @@
|
|
|
61
65
|
"@commitlint/cli": "^21.2.3",
|
|
62
66
|
"@commitlint/config-conventional": "^21.2.3",
|
|
63
67
|
"@googleapis/sheets": "^18.0.1",
|
|
68
|
+
"@playwright/test": "^1.63.0",
|
|
64
69
|
"@types/node": "^22.20.3",
|
|
65
70
|
"exceljs": "^4.4.0",
|
|
66
71
|
"husky": "^9.1.7",
|