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
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PostgresSink = exports.POSTGRES_DIALECT = void 0;
4
+ exports.postgresSession = postgresSession;
5
+ const logger_1 = require("../../logger");
6
+ const writer_1 = require("../sql/writer");
7
+ const target_1 = require("../sql/target");
8
+ const postgresSource_1 = require("./postgresSource");
9
+ exports.POSTGRES_DIALECT = {
10
+ label: "PostgreSQL",
11
+ open: '"',
12
+ close: '"',
13
+ schemas: true,
14
+ maxNameLength: 63,
15
+ nameLength: (name) => Buffer.byteLength(name, "utf8"),
16
+ maxParams: 65000,
17
+ maxRowsPerInsert: 1000,
18
+ transactionalDdl: true,
19
+ types: {
20
+ integer: "BIGINT",
21
+ number: "DOUBLE PRECISION",
22
+ text: "TEXT",
23
+ },
24
+ keyType: "TEXT",
25
+ tableSuffix: "",
26
+ param: (index) => `$${index}`,
27
+ tableMark: () => ({ inside: "", after: "" }),
28
+ tableId: async (session, table) => {
29
+ const [found] = await session.all("SELECT to_regclass($1)::oid::text AS id", [(0, writer_1.quoteTableName)(exports.POSTGRES_DIALECT, table)]);
30
+ return typeof found?.id === "string" ? found.id : null;
31
+ },
32
+ ownsTable: async (session, table) => {
33
+ const [found] = await session.all("SELECT pg_get_userbyid(c.relowner) = current_user AS dono FROM pg_class c WHERE c.oid = to_regclass($1)", [(0, writer_1.quoteTableName)(exports.POSTGRES_DIALECT, table)]);
34
+ return found?.dono === true;
35
+ },
36
+ tableExists: async (session, table) => {
37
+ const [found] = await session.all("SELECT to_regclass($1) AS nome", [
38
+ (0, writer_1.quoteTableName)(exports.POSTGRES_DIALECT, table),
39
+ ]);
40
+ return found?.nome !== null && found?.nome !== undefined;
41
+ },
42
+ };
43
+ function postgresSession(client) {
44
+ return {
45
+ run: async (sql, values) => {
46
+ await client.query(sql, values);
47
+ },
48
+ all: async (sql, values) => (await client.query(sql, values)).rows,
49
+ begin: async () => {
50
+ await client.query("BEGIN");
51
+ },
52
+ commit: async () => {
53
+ await client.query("COMMIT");
54
+ },
55
+ rollback: async () => {
56
+ await client.query("ROLLBACK");
57
+ },
58
+ };
59
+ }
60
+ class PostgresSink {
61
+ connection;
62
+ target;
63
+ logger;
64
+ createClient;
65
+ constructor(connection, target, logger = logger_1.consoleLogger, createClient = postgresSource_1.createPostgresClient) {
66
+ this.connection = connection;
67
+ this.target = target;
68
+ this.logger = logger;
69
+ this.createClient = createClient;
70
+ }
71
+ async write(rows, options = {}) {
72
+ const client = this.createClient(this.connection);
73
+ await client.connect();
74
+ try {
75
+ await (0, writer_1.writeTable)(postgresSession(client), exports.POSTGRES_DIALECT, (0, target_1.tableFor)(this.target, options), rows, this.logger);
76
+ }
77
+ finally {
78
+ await client.end();
79
+ }
80
+ }
81
+ }
82
+ exports.PostgresSink = PostgresSink;
@@ -10,7 +10,7 @@ export interface PostgresConnection {
10
10
  }
11
11
  export interface PostgresClient {
12
12
  connect(): Promise<unknown>;
13
- query(text: string): Promise<{
13
+ query(text: string, values?: unknown[]): Promise<{
14
14
  rows: Record<string, unknown>[];
15
15
  }>;
16
16
  end(): Promise<unknown>;
@@ -1,4 +1,4 @@
1
1
  import { EtlConfig } from "../config";
2
2
  export declare function isSameFile(a: string, b: string): boolean;
3
- export declare function assertDifferentFiles(input: string, output: string): void;
3
+ export declare function tableProblem(config: EtlConfig): string | null;
4
4
  export declare function assertSourceIsSafe(config: EtlConfig): void;
package/dist/io/safety.js CHANGED
@@ -4,11 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isSameFile = isSameFile;
7
- exports.assertDifferentFiles = assertDifferentFiles;
7
+ exports.tableProblem = tableProblem;
8
8
  exports.assertSourceIsSafe = assertSourceIsSafe;
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const validate_1 = require("../filters/validate");
12
+ const writer_1 = require("./sql/writer");
13
+ const sqliteSink_1 = require("./sqlite/sqliteSink");
14
+ const mysqlSink_1 = require("./mysql/mysqlSink");
15
+ const postgresSink_1 = require("./postgres/postgresSink");
16
+ const sqlServerSink_1 = require("./sqlserver/sqlServerSink");
17
+ const target_1 = require("./sql/target");
18
+ const servers_1 = require("./servers");
12
19
  function sameName(a, b) {
13
20
  return a.trim().toLowerCase() === b.trim().toLowerCase();
14
21
  }
@@ -61,12 +68,10 @@ function comparablePath(file) {
61
68
  function isSameFile(a, b) {
62
69
  return comparablePath(a) === comparablePath(b);
63
70
  }
64
- function assertDifferentFiles(input, output) {
65
- if (isSameFile(input, output)) {
66
- throw new Error(`A entrada e a saída são o mesmo arquivo (${input}). A saída é apagada antes de gravar, então escolha outro arquivo pra ela.`);
67
- }
68
- }
69
71
  function assertFilesAreSafe(config) {
72
+ if (config.source?.type === "sqlite" &&
73
+ config.destination?.type === "sqlite")
74
+ return;
70
75
  const sourcePath = config.source ? filePath(config.source) : undefined;
71
76
  const destinationPath = config.destination
72
77
  ? filePath(config.destination)
@@ -77,7 +82,101 @@ function assertFilesAreSafe(config) {
77
82
  throw new Error(`A fonte e o destino são o mesmo arquivo (${sourcePath}). O destino é apagado antes de gravar, então escolha outro arquivo pra saída.`);
78
83
  }
79
84
  }
85
+ function destinationTable(config) {
86
+ const destination = config.destination;
87
+ switch (destination?.type) {
88
+ case "sqlite":
89
+ case "mysql":
90
+ case "postgres":
91
+ case "sqlserver":
92
+ return destination.table?.trim() || undefined;
93
+ default:
94
+ return undefined;
95
+ }
96
+ }
97
+ function sameDatabase(config) {
98
+ const { source, destination } = config;
99
+ if (source?.type === "sqlite" && destination?.type === "sqlite") {
100
+ return isSameFile(source.path, destination.path);
101
+ }
102
+ const from = (0, servers_1.serverOf)(config, "source");
103
+ const to = (0, servers_1.serverOf)(config, "destination");
104
+ if (!from || !to || from.type !== to.type)
105
+ return false;
106
+ return (sameName(from.host ?? "", to.host ?? "") &&
107
+ from.port === to.port &&
108
+ sameName(from.database ?? "", to.database ?? ""));
109
+ }
110
+ function sourceTable(config) {
111
+ const source = config.source;
112
+ if (source?.type === "sqlite")
113
+ return source.table;
114
+ return (0, servers_1.serverOf)(config, "source")?.table;
115
+ }
116
+ const DIALECTS = {
117
+ sqlite: sqliteSink_1.SQLITE_DIALECT,
118
+ mysql: mysqlSink_1.MYSQL_DIALECT,
119
+ postgres: postgresSink_1.POSTGRES_DIALECT,
120
+ sqlserver: sqlServerSink_1.SQLSERVER_DIALECT,
121
+ };
122
+ function nameProblem(type, targets) {
123
+ const dialect = DIALECTS[type];
124
+ if (!dialect)
125
+ return null;
126
+ for (const target of targets) {
127
+ try {
128
+ (0, writer_1.checkTableName)(dialect, target, (0, writer_1.nameRoom)(dialect));
129
+ }
130
+ catch (error) {
131
+ return error instanceof Error ? error.message : String(error);
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ function tableProblem(config) {
137
+ const table = destinationTable(config);
138
+ const destination = config.destination;
139
+ if (table === undefined || destination === undefined)
140
+ return null;
141
+ const pending = config.validation
142
+ ? (0, target_1.pendingTableOf)({
143
+ table,
144
+ pendingTable: "pendingTable" in destination
145
+ ? destination.pendingTable
146
+ : undefined,
147
+ })
148
+ : undefined;
149
+ const targets = pending === undefined ? [table] : [table, pending];
150
+ const names = nameProblem(destination.type, targets);
151
+ if (names !== null)
152
+ return names;
153
+ const lastPart = (target) => target.split(".").pop()?.trim().toLowerCase() ?? "";
154
+ if (targets.some((target) => {
155
+ const last = lastPart(target);
156
+ return (last === writer_1.OWNED_TABLES ||
157
+ last.endsWith(writer_1.NEW_SUFFIX) ||
158
+ last.endsWith(writer_1.OLD_SUFFIX));
159
+ })) {
160
+ return `"${writer_1.OWNED_TABLES}" e os nomes terminados em ${writer_1.NEW_SUFFIX} ou ${writer_1.OLD_SUFFIX} são usados pelo próprio Datera. Escolha outro nome.`;
161
+ }
162
+ if (pending !== undefined && sameName(pending, table)) {
163
+ return `A tabela das pendências precisa ter um nome diferente da tabela do resultado ("${table}").`;
164
+ }
165
+ const from = sourceTable(config);
166
+ if (from !== undefined && sameDatabase(config)) {
167
+ if (targets.some((target) => sameName(target, from))) {
168
+ return `A tabela "${from}" é de onde o Datera lê, então ele não pode gravar nela. Escolha outro nome pra tabela do resultado ou das pendências.`;
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+ function assertTablesAreSafe(config) {
174
+ const problem = tableProblem(config);
175
+ if (problem !== null)
176
+ throw new Error(problem);
177
+ }
80
178
  function assertSourceIsSafe(config) {
81
179
  assertSheetsAreSafe(config);
82
180
  assertFilesAreSafe(config);
181
+ assertTablesAreSafe(config);
83
182
  }
@@ -0,0 +1,14 @@
1
+ import { EtlConfig } from "../config";
2
+ export type ServerType = "mysql" | "postgres" | "sqlserver";
3
+ export declare const DEFAULT_PORTS: Record<ServerType, number>;
4
+ export interface ServerPlace {
5
+ type: ServerType;
6
+ host: string | undefined;
7
+ port: number;
8
+ user: string | undefined;
9
+ password: string | undefined;
10
+ database: string | undefined;
11
+ table: string | undefined;
12
+ }
13
+ export declare function serverOf(config: EtlConfig, side: "source" | "destination"): ServerPlace | undefined;
14
+ export declare function destinationSharesSource(config: EtlConfig): boolean;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_PORTS = void 0;
4
+ exports.serverOf = serverOf;
5
+ exports.destinationSharesSource = destinationSharesSource;
6
+ exports.DEFAULT_PORTS = {
7
+ mysql: 3306,
8
+ postgres: 5432,
9
+ sqlserver: 1433,
10
+ };
11
+ function sourceServer(config) {
12
+ const source = config.source ?? { type: "mysql" };
13
+ if (source.type !== "mysql" &&
14
+ source.type !== "postgres" &&
15
+ source.type !== "sqlserver")
16
+ return undefined;
17
+ const legacy = source.type === "mysql";
18
+ return {
19
+ type: source.type,
20
+ host: source.host ?? (legacy ? config.dbHost : undefined),
21
+ port: source.port ??
22
+ (legacy ? config.dbPort : undefined) ??
23
+ exports.DEFAULT_PORTS[source.type],
24
+ user: source.user ?? (legacy ? config.dbUser : undefined),
25
+ password: source.password ?? (legacy ? config.dbPassword : undefined),
26
+ database: source.database ?? (legacy ? config.dbName : undefined),
27
+ table: source.table ?? (legacy ? config.tableName : undefined),
28
+ };
29
+ }
30
+ function sameHost(a, b) {
31
+ return (a ?? "").trim().toLowerCase() === (b ?? "").trim().toLowerCase();
32
+ }
33
+ function isSameServer(source, destination) {
34
+ if (source.type !== destination.type)
35
+ return false;
36
+ if (destination.host === undefined)
37
+ return destination.port === undefined;
38
+ return (sameHost(source.host, destination.host) &&
39
+ (destination.port ?? exports.DEFAULT_PORTS[destination.type]) === source.port);
40
+ }
41
+ function serverOf(config, side) {
42
+ const source = sourceServer(config);
43
+ if (side === "source")
44
+ return source;
45
+ const destination = config.destination;
46
+ if (destination?.type !== "mysql" &&
47
+ destination?.type !== "postgres" &&
48
+ destination?.type !== "sqlserver")
49
+ return undefined;
50
+ const same = source !== undefined && isSameServer(source, destination)
51
+ ? source
52
+ : undefined;
53
+ return {
54
+ type: destination.type,
55
+ host: destination.host ?? same?.host,
56
+ port: destination.port ?? same?.port ?? exports.DEFAULT_PORTS[destination.type],
57
+ user: destination.user ?? same?.user,
58
+ password: destination.password ?? same?.password,
59
+ database: destination.database ?? same?.database,
60
+ table: destination.table,
61
+ };
62
+ }
63
+ function destinationSharesSource(config) {
64
+ const source = sourceServer(config);
65
+ const destination = config.destination;
66
+ if (source === undefined ||
67
+ (destination?.type !== "mysql" &&
68
+ destination?.type !== "postgres" &&
69
+ destination?.type !== "sqlserver"))
70
+ return false;
71
+ return isSameServer(source, destination);
72
+ }
@@ -6,6 +6,7 @@ exports.writeData = writeData;
6
6
  const optional_1 = require("../optional");
7
7
  const header_1 = require("../header");
8
8
  const logger_1 = require("../../logger");
9
+ const cells_1 = require("../../cells");
9
10
  function createSheetsClient(credentialsPath) {
10
11
  const { auth, sheets: sheetsApi } = (0, optional_1.requireOptional)(() => require("@googleapis/sheets"), ["@googleapis/sheets"], "Google Sheets");
11
12
  const credentials = new auth.GoogleAuth({
@@ -127,7 +128,7 @@ async function writeData(sheets, spreadsheetId, data, options = {}) {
127
128
  return;
128
129
  }
129
130
  const header = (0, header_1.buildHeader)(data);
130
- const rows = data.map((row) => header.map((column) => row[column] ?? ""));
131
+ const rows = data.map((row) => header.map((column) => (0, cells_1.cellOf)(row, column) ?? ""));
131
132
  await sheets.spreadsheets.values.update({
132
133
  spreadsheetId,
133
134
  range: `${range}!A1`,
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SheetsSource = void 0;
4
4
  const client_1 = require("./client");
5
+ const cells_1 = require("../../cells");
5
6
  function fromSheetCell(value) {
6
7
  if (value === null || value === undefined || value === "")
7
8
  return null;
@@ -34,7 +35,7 @@ class SheetsSource {
34
35
  if (column === null)
35
36
  return;
36
37
  const value = fromSheetCell(values[index]);
37
- row[column] = value;
38
+ (0, cells_1.setCell)(row, column, value);
38
39
  if (value !== null)
39
40
  hasValue = true;
40
41
  });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toCell = toCell;
4
4
  exports.toRow = toRow;
5
+ const cells_1 = require("../../cells");
5
6
  function isoDate(date) {
6
7
  if (Number.isNaN(date.getTime()))
7
8
  return "";
@@ -32,7 +33,7 @@ function toCell(value) {
32
33
  function toRow(record) {
33
34
  const row = {};
34
35
  for (const [column, value] of Object.entries(record)) {
35
- row[column] = toCell(value);
36
+ (0, cells_1.setCell)(row, column, toCell(value));
36
37
  }
37
38
  return row;
38
39
  }
@@ -0,0 +1,8 @@
1
+ import { SinkWriteOptions } from "../types";
2
+ export declare const PENDING_SUFFIX = "_pendencias";
3
+ export interface TableTarget {
4
+ table: string;
5
+ pendingTable?: string | undefined;
6
+ }
7
+ export declare function pendingTableOf(target: TableTarget): string;
8
+ export declare function tableFor(target: TableTarget, options: SinkWriteOptions): string;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PENDING_SUFFIX = void 0;
4
+ exports.pendingTableOf = pendingTableOf;
5
+ exports.tableFor = tableFor;
6
+ exports.PENDING_SUFFIX = "_pendencias";
7
+ function pendingTableOf(target) {
8
+ return (target.pendingTable?.trim() || `${target.table.trim()}${exports.PENDING_SUFFIX}`);
9
+ }
10
+ function tableFor(target, options) {
11
+ return options.name === undefined
12
+ ? target.table.trim()
13
+ : pendingTableOf(target);
14
+ }
@@ -0,0 +1,46 @@
1
+ import { TableRow } from "../../types";
2
+ import { Logger } from "../../logger";
3
+ export type ColumnKind = "integer" | "number" | "text";
4
+ export type SqlValue = string | number | null;
5
+ export interface SqlSession {
6
+ run(sql: string, params?: SqlValue[]): Promise<void>;
7
+ all(sql: string, params?: SqlValue[]): Promise<Record<string, unknown>[]>;
8
+ begin(): Promise<void>;
9
+ commit(): Promise<void>;
10
+ rollback(): Promise<void>;
11
+ }
12
+ export interface SqlDialect {
13
+ label: string;
14
+ open: string;
15
+ close: string;
16
+ schemas: boolean;
17
+ maxNameLength: number;
18
+ nameLength(name: string): number;
19
+ maxParams: number;
20
+ maxRowsPerInsert: number;
21
+ transactionalDdl: boolean;
22
+ types: Record<ColumnKind, string>;
23
+ keyType: string;
24
+ tableSuffix: string;
25
+ param(index: number): string;
26
+ tableExists(session: SqlSession, table: string): Promise<boolean>;
27
+ tableMark(token: string): {
28
+ inside: string;
29
+ after: string;
30
+ };
31
+ tableId(session: SqlSession, table: string): Promise<string | null>;
32
+ ownsTable?(session: SqlSession, table: string): Promise<boolean>;
33
+ }
34
+ export declare const OWNED_TABLES = "datera_tabelas";
35
+ export declare const NEW_SUFFIX = "__datera_novo";
36
+ export declare const OLD_SUFFIX = "__datera_velho";
37
+ export declare const MARK_PREFIX = "datera:";
38
+ export declare function newToken(): string;
39
+ export declare function quoteName(dialect: SqlDialect, name: string): string;
40
+ export declare function tableParts(dialect: SqlDialect, table: string): string[];
41
+ export declare function quoteTableName(dialect: SqlDialect, table: string): string;
42
+ export declare function nameRoom(dialect: SqlDialect): number;
43
+ export declare function checkTableName(dialect: SqlDialect, table: string, extra?: number): void;
44
+ export declare function checkColumns(dialect: SqlDialect, header: string[]): void;
45
+ export declare function columnKind(rows: TableRow[], column: string): ColumnKind;
46
+ export declare function writeTable(session: SqlSession, dialect: SqlDialect, table: string, rows: TableRow[], logger: Logger): Promise<void>;