datera 0.2.1 → 0.3.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 +9 -0
- package/README.md +1 -0
- package/dist/api/clean.d.ts +7 -0
- package/dist/api/clean.js +16 -0
- package/dist/api/files.d.ts +10 -0
- package/dist/api/files.js +145 -0
- package/dist/api/formats.d.ts +4 -0
- package/dist/api/formats.js +5 -1
- package/dist/api/index.d.ts +8 -3
- package/dist/api/index.js +12 -1
- package/dist/api/rows.d.ts +13 -0
- package/dist/api/rows.js +82 -2
- package/dist/cli.d.ts +32 -0
- package/dist/cli.js +69 -9
- package/dist/commands.js +88 -14
- package/dist/config/ioSchema.d.ts +4 -0
- package/dist/config/ioSchema.js +6 -0
- package/dist/config/prepareSchema.d.ts +4 -0
- package/dist/config/prepareSchema.js +8 -1
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +8 -0
- package/dist/filters/align.d.ts +11 -0
- package/dist/filters/align.js +102 -0
- package/dist/io/excel/excelSource.d.ts +3 -1
- package/dist/io/excel/excelSource.js +40 -22
- package/dist/io/factory.js +4 -1
- package/dist/io/safety.js +3 -0
- package/dist/io/sheets/client.d.ts +1 -0
- package/dist/io/sheets/client.js +9 -3
- package/dist/io/sheets/sheetsSource.d.ts +4 -1
- package/dist/io/sheets/sheetsSource.js +46 -23
- package/dist/io/tabs.d.ts +11 -0
- package/dist/io/tabs.js +23 -0
- package/dist/io/types.d.ts +2 -0
- package/dist/pipeline/steps.js +8 -0
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -21,21 +21,29 @@ function assertSafeOutput(cli) {
|
|
|
21
21
|
function plural(count, one, many) {
|
|
22
22
|
return `${count} ${count === 1 ? one : many}`;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
function isBatch(cli) {
|
|
25
|
+
if (cli.outDir !== undefined || cli.to !== undefined)
|
|
26
|
+
return true;
|
|
27
|
+
if (cli.input === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
return (/[*?]/.test(cli.input) ||
|
|
30
|
+
(fs_1.default.existsSync(cli.input) && fs_1.default.statSync(cli.input).isDirectory()));
|
|
31
|
+
}
|
|
32
|
+
function sameType(base, next) {
|
|
33
|
+
if (typeof base === "object" &&
|
|
34
|
+
base !== null &&
|
|
35
|
+
base.type === next.type) {
|
|
36
|
+
return { ...base, path: next.path };
|
|
28
37
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
process.chdir(folder);
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
function configTarget(raw) {
|
|
41
|
+
const destination = raw.destination;
|
|
42
|
+
return typeof destination?.path === "string"
|
|
43
|
+
? path_1.default.extname(destination.path)
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
async function runOnce(raw, cli, logger) {
|
|
39
47
|
const config = (0, config_1.parseConfig)(raw);
|
|
40
48
|
const report = await (0, pipeline_1.runEtl)(config, {
|
|
41
49
|
mode: cli.mode === undefined ? undefined : (0, pipeline_1.parseMode)(cli.mode),
|
|
@@ -49,6 +57,53 @@ async function runConfig(cli, logger) {
|
|
|
49
57
|
logger.info("");
|
|
50
58
|
for (const line of (0, pipeline_1.formatReport)(report))
|
|
51
59
|
logger.info(line);
|
|
60
|
+
return report;
|
|
61
|
+
}
|
|
62
|
+
async function runConfig(cli, logger) {
|
|
63
|
+
const configPath = path_1.default.resolve(cli.config);
|
|
64
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
65
|
+
throw new Error(`Não achei a config ${cli.config}. Se era pra ser um comando, veja a lista com "datera --help".`);
|
|
66
|
+
}
|
|
67
|
+
const folder = path_1.default.dirname(configPath);
|
|
68
|
+
dotenv_1.default.config({ path: path_1.default.join(folder, ".env"), quiet: true });
|
|
69
|
+
const raw = JSON.parse(fs_1.default.readFileSync(configPath, "utf-8"));
|
|
70
|
+
if (isBatch(cli)) {
|
|
71
|
+
if (cli.input === undefined) {
|
|
72
|
+
throw new Error('Com --out-dir ou --to, diga os arquivos com --input (tipo --input "matriculas/*.xlsx").');
|
|
73
|
+
}
|
|
74
|
+
if (cli.output !== undefined) {
|
|
75
|
+
throw new Error("Com vários arquivos, use --out-dir em vez de --output, porque cada arquivo ganha o seu resultado.");
|
|
76
|
+
}
|
|
77
|
+
const target = cli.to ?? configTarget(raw);
|
|
78
|
+
if (target === undefined) {
|
|
79
|
+
throw new Error("O destino da config não é um arquivo, então diga o formato de cada resultado com --to (tipo --to xlsx).");
|
|
80
|
+
}
|
|
81
|
+
const plan = (0, api_1.planOutputs)((0, api_1.findFiles)(path_1.default.resolve(cli.input)), target, cli.outDir === undefined ? undefined : path_1.default.resolve(cli.outDir));
|
|
82
|
+
process.chdir(folder);
|
|
83
|
+
let rows = 0;
|
|
84
|
+
for (const [index, item] of plan.entries()) {
|
|
85
|
+
logger.info("");
|
|
86
|
+
logger.info(`Arquivo ${index + 1} de ${plan.length}: ${item.input}`);
|
|
87
|
+
const report = await runOnce({
|
|
88
|
+
...raw,
|
|
89
|
+
source: sameType(raw.source, (0, api_1.sourceFromPath)(item.input, (raw.source ?? {}))),
|
|
90
|
+
destination: sameType(raw.destination, (0, api_1.destinationFromPath)(item.output, (raw.destination ?? {}))),
|
|
91
|
+
}, cli, logger);
|
|
92
|
+
rows += report.rowsOut;
|
|
93
|
+
}
|
|
94
|
+
logger.info("");
|
|
95
|
+
logger.info(`${plural(plan.length, "arquivo processado", "arquivos processados")}, ${plural(rows, "linha no resultado", "linhas no resultado")} ao todo.`);
|
|
96
|
+
logger.info(cli.dryRun ? "Teste concluído." : "ETL concluído com sucesso!");
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
const input = cli.input === undefined ? undefined : path_1.default.resolve(cli.input);
|
|
100
|
+
const output = cli.output === undefined ? undefined : path_1.default.resolve(cli.output);
|
|
101
|
+
if (input)
|
|
102
|
+
raw.source = (0, api_1.sourceFromPath)(input);
|
|
103
|
+
if (output)
|
|
104
|
+
raw.destination = (0, api_1.destinationFromPath)(output);
|
|
105
|
+
process.chdir(folder);
|
|
106
|
+
const report = await runOnce(raw, cli, logger);
|
|
52
107
|
logger.info(report.written ? "ETL concluído com sucesso!" : "Teste concluído.");
|
|
53
108
|
return 0;
|
|
54
109
|
}
|
|
@@ -118,6 +173,22 @@ async function runCommand(cli, logger) {
|
|
|
118
173
|
logger.info(`${plural(count, "linha convertida", "linhas convertidas")} de ${cli.input} pra ${cli.output}.`);
|
|
119
174
|
return 0;
|
|
120
175
|
}
|
|
176
|
+
case "convert-many": {
|
|
177
|
+
const results = await (0, api_1.convertMany)(cli.inputs, {
|
|
178
|
+
to: cli.to,
|
|
179
|
+
outDir: cli.outDir,
|
|
180
|
+
read: cli.read,
|
|
181
|
+
write: cli.write,
|
|
182
|
+
});
|
|
183
|
+
for (const item of results) {
|
|
184
|
+
const from = item.sheet === undefined
|
|
185
|
+
? item.input
|
|
186
|
+
: `${item.input} (aba ${item.sheet})`;
|
|
187
|
+
logger.info(`${from} → ${item.output} (${plural(item.rows, "linha", "linhas")})`);
|
|
188
|
+
}
|
|
189
|
+
logger.info(`${plural(results.length, "arquivo convertido", "arquivos convertidos")}.`);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
121
192
|
case "dedupe": {
|
|
122
193
|
assertSafeOutput(cli);
|
|
123
194
|
const rows = await (0, api_1.readRows)(cli.input, cli.read);
|
|
@@ -145,6 +216,9 @@ async function runCommand(cli, logger) {
|
|
|
145
216
|
case "columns": {
|
|
146
217
|
const rows = await (0, api_1.readRows)(cli.input, cli.read);
|
|
147
218
|
logger.info(`${cli.input}: ${plural(rows.length, "linha", "linhas")}.`);
|
|
219
|
+
for (const group of (0, api_1.findSimilarColumns)(rows)) {
|
|
220
|
+
logger.warn(`As colunas ${group.map((name) => `"${name}"`).join(", ")} parecem a mesma. Pra juntar, use --align-columns.`);
|
|
221
|
+
}
|
|
148
222
|
console.table((0, api_1.describeColumns)(rows).map((column) => ({
|
|
149
223
|
coluna: column.name,
|
|
150
224
|
preenchidas: column.filled,
|
|
@@ -51,10 +51,14 @@ export declare const sourceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
51
51
|
type: z.ZodLiteral<"parquet">;
|
|
52
52
|
path: z.ZodString;
|
|
53
53
|
}, z.core.$strip>, z.ZodObject<{
|
|
54
|
+
allSheets: z.ZodOptional<z.ZodBoolean>;
|
|
55
|
+
sheetColumn: z.ZodOptional<z.ZodString>;
|
|
54
56
|
type: z.ZodLiteral<"excel">;
|
|
55
57
|
path: z.ZodString;
|
|
56
58
|
sheet: z.ZodOptional<z.ZodString>;
|
|
57
59
|
}, z.core.$strip>, z.ZodObject<{
|
|
60
|
+
allSheets: z.ZodOptional<z.ZodBoolean>;
|
|
61
|
+
sheetColumn: z.ZodOptional<z.ZodString>;
|
|
58
62
|
type: z.ZodLiteral<"sheets">;
|
|
59
63
|
spreadsheetId: z.ZodString;
|
|
60
64
|
credentialsPath: z.ZodOptional<z.ZodString>;
|
package/dist/config/ioSchema.js
CHANGED
|
@@ -13,6 +13,10 @@ const serverFields = {
|
|
|
13
13
|
database: zod_1.z.string().optional(),
|
|
14
14
|
table: zod_1.z.string().optional(),
|
|
15
15
|
};
|
|
16
|
+
const tabFields = {
|
|
17
|
+
allSheets: zod_1.z.boolean().optional(),
|
|
18
|
+
sheetColumn: zod_1.z.string().trim().min(1).optional(),
|
|
19
|
+
};
|
|
16
20
|
exports.sourceSchema = zod_1.z.discriminatedUnion("type", [
|
|
17
21
|
zod_1.z.object({
|
|
18
22
|
type: zod_1.z.literal("mysql"),
|
|
@@ -59,12 +63,14 @@ exports.sourceSchema = zod_1.z.discriminatedUnion("type", [
|
|
|
59
63
|
type: zod_1.z.literal("excel"),
|
|
60
64
|
path: zod_1.z.string(),
|
|
61
65
|
sheet: zod_1.z.string().min(1).optional(),
|
|
66
|
+
...tabFields,
|
|
62
67
|
}),
|
|
63
68
|
zod_1.z.object({
|
|
64
69
|
type: zod_1.z.literal("sheets"),
|
|
65
70
|
spreadsheetId: zod_1.z.string().min(1),
|
|
66
71
|
credentialsPath: zod_1.z.string().optional(),
|
|
67
72
|
sheet: zod_1.z.string().min(1).optional(),
|
|
73
|
+
...tabFields,
|
|
68
74
|
}),
|
|
69
75
|
zod_1.z.object({
|
|
70
76
|
type: zod_1.z.literal("custom"),
|
|
@@ -4,6 +4,10 @@ export declare const fillEmptySchema: z.ZodObject<{
|
|
|
4
4
|
fallbackColumns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
5
5
|
default: z.ZodOptional<z.ZodString>;
|
|
6
6
|
}, z.core.$strip>;
|
|
7
|
+
export declare const alignColumnsSchema: z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
8
|
+
auto: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
rename: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
10
|
+
}, z.core.$strip>]>;
|
|
7
11
|
export declare const combineColumnsSchema: z.ZodObject<{
|
|
8
12
|
into: z.ZodString;
|
|
9
13
|
columns: z.ZodArray<z.ZodString>;
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.combineColumnsSchema = exports.fillEmptySchema = void 0;
|
|
3
|
+
exports.combineColumnsSchema = exports.alignColumnsSchema = exports.fillEmptySchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
exports.fillEmptySchema = zod_1.z.object({
|
|
6
6
|
column: zod_1.z.string(),
|
|
7
7
|
fallbackColumns: zod_1.z.array(zod_1.z.string()).optional(),
|
|
8
8
|
default: zod_1.z.string().optional(),
|
|
9
9
|
});
|
|
10
|
+
exports.alignColumnsSchema = zod_1.z.union([
|
|
11
|
+
zod_1.z.boolean(),
|
|
12
|
+
zod_1.z.object({
|
|
13
|
+
auto: zod_1.z.boolean().optional(),
|
|
14
|
+
rename: zod_1.z.record(zod_1.z.string(), zod_1.z.string().trim().min(1)).optional(),
|
|
15
|
+
}),
|
|
16
|
+
]);
|
|
10
17
|
exports.combineColumnsSchema = zod_1.z.object({
|
|
11
18
|
into: zod_1.z.string(),
|
|
12
19
|
columns: zod_1.z.array(zod_1.z.string()).min(1),
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -52,10 +52,14 @@ export declare const etlConfigSchema: z.ZodObject<{
|
|
|
52
52
|
type: z.ZodLiteral<"parquet">;
|
|
53
53
|
path: z.ZodString;
|
|
54
54
|
}, z.core.$strip>, z.ZodObject<{
|
|
55
|
+
allSheets: z.ZodOptional<z.ZodBoolean>;
|
|
56
|
+
sheetColumn: z.ZodOptional<z.ZodString>;
|
|
55
57
|
type: z.ZodLiteral<"excel">;
|
|
56
58
|
path: z.ZodString;
|
|
57
59
|
sheet: z.ZodOptional<z.ZodString>;
|
|
58
60
|
}, z.core.$strip>, z.ZodObject<{
|
|
61
|
+
allSheets: z.ZodOptional<z.ZodBoolean>;
|
|
62
|
+
sheetColumn: z.ZodOptional<z.ZodString>;
|
|
59
63
|
type: z.ZodLiteral<"sheets">;
|
|
60
64
|
spreadsheetId: z.ZodString;
|
|
61
65
|
credentialsPath: z.ZodOptional<z.ZodString>;
|
|
@@ -192,6 +196,10 @@ export declare const etlConfigSchema: z.ZodObject<{
|
|
|
192
196
|
mergeKeyNormalizer: z.ZodOptional<z.ZodString>;
|
|
193
197
|
normalizerModules: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
194
198
|
adapterModules: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
199
|
+
alignColumns: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
200
|
+
auto: z.ZodOptional<z.ZodBoolean>;
|
|
201
|
+
rename: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
202
|
+
}, z.core.$strip>]>>;
|
|
195
203
|
fillEmpty: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
196
204
|
column: z.ZodString;
|
|
197
205
|
fallbackColumns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
package/dist/config/schema.js
CHANGED
|
@@ -29,6 +29,7 @@ exports.etlConfigSchema = zod_1.z
|
|
|
29
29
|
mergeKeyNormalizer: zod_1.z.string().optional(),
|
|
30
30
|
normalizerModules: zod_1.z.array(zod_1.z.string()).optional(),
|
|
31
31
|
adapterModules: zod_1.z.array(zod_1.z.string()).optional(),
|
|
32
|
+
alignColumns: prepareSchema_1.alignColumnsSchema.optional(),
|
|
32
33
|
fillEmpty: zod_1.z.array(prepareSchema_1.fillEmptySchema).optional(),
|
|
33
34
|
combineColumns: zod_1.z.array(prepareSchema_1.combineColumnsSchema).optional(),
|
|
34
35
|
validation: validationSchema_1.validationSchema.optional(),
|
|
@@ -36,6 +37,13 @@ exports.etlConfigSchema = zod_1.z
|
|
|
36
37
|
.refine((config) => config.source !== undefined || config.tableName !== undefined, {
|
|
37
38
|
message: 'Faltou dizer de onde ler: coloque "source" ou os campos antigos do MySQL.',
|
|
38
39
|
path: ["source"],
|
|
40
|
+
})
|
|
41
|
+
.refine((config) => !((config.source?.type === "excel" ||
|
|
42
|
+
config.source?.type === "sheets") &&
|
|
43
|
+
config.source.allSheets === true &&
|
|
44
|
+
config.source.sheet !== undefined), {
|
|
45
|
+
message: 'Use "sheet" pra ler uma aba só ou "allSheets": true pra ler todas, mas não os dois juntos.',
|
|
46
|
+
path: ["source", "allSheets"],
|
|
39
47
|
})
|
|
40
48
|
.refine((config) => config.destination !== undefined ||
|
|
41
49
|
config.spreadsheetId !== undefined, {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TableRow } from "../types";
|
|
2
|
+
export interface AlignOptions {
|
|
3
|
+
auto?: boolean | undefined;
|
|
4
|
+
rename?: Record<string, string> | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare const ALIGN_SEPARATOR = " | ";
|
|
7
|
+
export declare function columnKey(name: string): string;
|
|
8
|
+
export declare function columnMapping(names: string[], options?: AlignOptions): Map<string, string>;
|
|
9
|
+
export declare function similarColumns(names: string[]): string[][];
|
|
10
|
+
export declare function alignRows(rows: TableRow[], options?: AlignOptions): TableRow[];
|
|
11
|
+
export declare function alignOptionsOf(value: boolean | AlignOptions | undefined): AlignOptions | undefined;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ALIGN_SEPARATOR = void 0;
|
|
4
|
+
exports.columnKey = columnKey;
|
|
5
|
+
exports.columnMapping = columnMapping;
|
|
6
|
+
exports.similarColumns = similarColumns;
|
|
7
|
+
exports.alignRows = alignRows;
|
|
8
|
+
exports.alignOptionsOf = alignOptionsOf;
|
|
9
|
+
const cells_1 = require("../cells");
|
|
10
|
+
exports.ALIGN_SEPARATOR = " | ";
|
|
11
|
+
function columnKey(name) {
|
|
12
|
+
const key = name
|
|
13
|
+
.normalize("NFD")
|
|
14
|
+
.replace(/[̀-ͯ]/g, "")
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9]/g, "");
|
|
17
|
+
return key === "" ? `=${name}` : key;
|
|
18
|
+
}
|
|
19
|
+
function columnNames(rows) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
for (const name of Object.keys(row))
|
|
23
|
+
seen.add(name);
|
|
24
|
+
}
|
|
25
|
+
return [...seen];
|
|
26
|
+
}
|
|
27
|
+
function columnMapping(names, options = {}) {
|
|
28
|
+
const auto = options.auto ?? true;
|
|
29
|
+
const renameByKey = new Map();
|
|
30
|
+
for (const [from, to] of Object.entries(options.rename ?? {})) {
|
|
31
|
+
const target = to.trim();
|
|
32
|
+
const key = columnKey(from);
|
|
33
|
+
const other = renameByKey.get(key);
|
|
34
|
+
if (other !== undefined && other !== target) {
|
|
35
|
+
throw new Error(`A coluna "${from}" está no rename duas vezes, uma indo pra "${other}" e outra pra "${target}". Deixe só uma.`);
|
|
36
|
+
}
|
|
37
|
+
renameByKey.set(key, target);
|
|
38
|
+
}
|
|
39
|
+
const canonical = new Map();
|
|
40
|
+
for (const target of renameByKey.values()) {
|
|
41
|
+
const key = columnKey(target);
|
|
42
|
+
if (!canonical.has(key))
|
|
43
|
+
canonical.set(key, target);
|
|
44
|
+
}
|
|
45
|
+
const mapping = new Map();
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const renamed = renameByKey.get(columnKey(name));
|
|
48
|
+
if (renamed !== undefined) {
|
|
49
|
+
mapping.set(name, auto ? (canonical.get(columnKey(renamed)) ?? renamed) : renamed);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!auto) {
|
|
53
|
+
mapping.set(name, name);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const key = columnKey(name);
|
|
57
|
+
const first = canonical.get(key);
|
|
58
|
+
if (first === undefined)
|
|
59
|
+
canonical.set(key, name);
|
|
60
|
+
mapping.set(name, first ?? name);
|
|
61
|
+
}
|
|
62
|
+
return mapping;
|
|
63
|
+
}
|
|
64
|
+
function similarColumns(names) {
|
|
65
|
+
const groups = new Map();
|
|
66
|
+
for (const name of names) {
|
|
67
|
+
const key = columnKey(name);
|
|
68
|
+
groups.set(key, [...(groups.get(key) ?? []), name]);
|
|
69
|
+
}
|
|
70
|
+
return [...groups.values()].filter((group) => group.length > 1);
|
|
71
|
+
}
|
|
72
|
+
function isEmpty(value) {
|
|
73
|
+
return value === null || value === undefined || value === "";
|
|
74
|
+
}
|
|
75
|
+
function alignRows(rows, options = {}) {
|
|
76
|
+
const mapping = columnMapping(columnNames(rows), options);
|
|
77
|
+
if ([...mapping].every(([from, to]) => from === to))
|
|
78
|
+
return rows;
|
|
79
|
+
return rows.map((row) => {
|
|
80
|
+
const next = {};
|
|
81
|
+
for (const [name, value] of Object.entries(row)) {
|
|
82
|
+
const target = mapping.get(name) ?? name;
|
|
83
|
+
if (!(0, cells_1.hasCell)(next, target)) {
|
|
84
|
+
(0, cells_1.setCell)(next, target, value);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const current = (0, cells_1.cellOf)(next, target);
|
|
88
|
+
if (isEmpty(current)) {
|
|
89
|
+
(0, cells_1.setCell)(next, target, value);
|
|
90
|
+
}
|
|
91
|
+
else if (!isEmpty(value) && String(value) !== String(current)) {
|
|
92
|
+
(0, cells_1.setCell)(next, target, `${String(current)}${exports.ALIGN_SEPARATOR}${String(value)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return next;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function alignOptionsOf(value) {
|
|
99
|
+
if (value === undefined || value === false)
|
|
100
|
+
return undefined;
|
|
101
|
+
return value === true ? {} : value;
|
|
102
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { TableRow } from "../../types";
|
|
2
2
|
import { Source } from "../types";
|
|
3
|
-
|
|
3
|
+
import { SheetRows, TabOptions } from "../tabs";
|
|
4
|
+
export interface ExcelSourceOptions extends TabOptions {
|
|
4
5
|
path: string;
|
|
5
6
|
sheet?: string | undefined;
|
|
6
7
|
}
|
|
7
8
|
export declare class ExcelSource implements Source {
|
|
8
9
|
private readonly options;
|
|
9
10
|
constructor(options: ExcelSourceOptions);
|
|
11
|
+
readTabs(): Promise<SheetRows[]>;
|
|
10
12
|
read(): Promise<TableRow[]>;
|
|
11
13
|
}
|
|
@@ -4,12 +4,51 @@ exports.ExcelSource = void 0;
|
|
|
4
4
|
const workbook_1 = require("./workbook");
|
|
5
5
|
const excelCell_1 = require("./excelCell");
|
|
6
6
|
const cells_1 = require("../../cells");
|
|
7
|
+
const tabs_1 = require("../tabs");
|
|
8
|
+
function readWorksheet(worksheet) {
|
|
9
|
+
const header = new Map();
|
|
10
|
+
worksheet.getRow(1).eachCell((cell, column) => {
|
|
11
|
+
const name = (0, excelCell_1.fromExcelCell)(cell.value);
|
|
12
|
+
if (name !== null)
|
|
13
|
+
header.set(column, String(name).trim());
|
|
14
|
+
});
|
|
15
|
+
const rows = [];
|
|
16
|
+
worksheet.eachRow((excelRow, rowNumber) => {
|
|
17
|
+
if (rowNumber === 1)
|
|
18
|
+
return;
|
|
19
|
+
const row = {};
|
|
20
|
+
let hasValue = false;
|
|
21
|
+
for (const [column, name] of header) {
|
|
22
|
+
const value = (0, excelCell_1.fromExcelCell)(excelRow.getCell(column).value);
|
|
23
|
+
(0, cells_1.setCell)(row, name, value);
|
|
24
|
+
if (value !== null)
|
|
25
|
+
hasValue = true;
|
|
26
|
+
}
|
|
27
|
+
if (hasValue)
|
|
28
|
+
rows.push(row);
|
|
29
|
+
});
|
|
30
|
+
return rows;
|
|
31
|
+
}
|
|
7
32
|
class ExcelSource {
|
|
8
33
|
options;
|
|
9
34
|
constructor(options) {
|
|
10
35
|
this.options = options;
|
|
11
36
|
}
|
|
37
|
+
async readTabs() {
|
|
38
|
+
const workbook = await (0, workbook_1.openWorkbook)(this.options.path);
|
|
39
|
+
const visible = workbook.worksheets.filter((worksheet) => worksheet.state === undefined || worksheet.state === "visible");
|
|
40
|
+
if (visible.length === 0) {
|
|
41
|
+
throw new Error(`O arquivo ${this.options.path} não tem nenhuma aba visível.`);
|
|
42
|
+
}
|
|
43
|
+
return visible.map((worksheet) => ({
|
|
44
|
+
sheet: worksheet.name,
|
|
45
|
+
rows: readWorksheet(worksheet),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
12
48
|
async read() {
|
|
49
|
+
if (this.options.allSheets) {
|
|
50
|
+
return (0, tabs_1.joinTabs)(await this.readTabs(), this.options.sheetColumn ?? tabs_1.DEFAULT_SHEET_COLUMN, this.options.path);
|
|
51
|
+
}
|
|
13
52
|
const workbook = await (0, workbook_1.openWorkbook)(this.options.path);
|
|
14
53
|
const sheetName = this.options.sheet;
|
|
15
54
|
const worksheet = sheetName === undefined
|
|
@@ -21,28 +60,7 @@ class ExcelSource {
|
|
|
21
60
|
? `O arquivo ${this.options.path} não tem nenhuma aba.`
|
|
22
61
|
: `Aba "${sheetName}" não encontrada em ${this.options.path}. Abas: ${names}`);
|
|
23
62
|
}
|
|
24
|
-
|
|
25
|
-
worksheet.getRow(1).eachCell((cell, column) => {
|
|
26
|
-
const name = (0, excelCell_1.fromExcelCell)(cell.value);
|
|
27
|
-
if (name !== null)
|
|
28
|
-
header.set(column, String(name).trim());
|
|
29
|
-
});
|
|
30
|
-
const rows = [];
|
|
31
|
-
worksheet.eachRow((excelRow, rowNumber) => {
|
|
32
|
-
if (rowNumber === 1)
|
|
33
|
-
return;
|
|
34
|
-
const row = {};
|
|
35
|
-
let hasValue = false;
|
|
36
|
-
for (const [column, name] of header) {
|
|
37
|
-
const value = (0, excelCell_1.fromExcelCell)(excelRow.getCell(column).value);
|
|
38
|
-
(0, cells_1.setCell)(row, name, value);
|
|
39
|
-
if (value !== null)
|
|
40
|
-
hasValue = true;
|
|
41
|
-
}
|
|
42
|
-
if (hasValue)
|
|
43
|
-
rows.push(row);
|
|
44
|
-
});
|
|
45
|
-
return rows;
|
|
63
|
+
return readWorksheet(worksheet);
|
|
46
64
|
}
|
|
47
65
|
}
|
|
48
66
|
exports.ExcelSource = ExcelSource;
|
package/dist/io/factory.js
CHANGED
|
@@ -83,7 +83,10 @@ function createSource(config, logger = logger_1.consoleLogger) {
|
|
|
83
83
|
return new excelSource_1.ExcelSource(source);
|
|
84
84
|
case "sheets": {
|
|
85
85
|
const credentialsPath = required(source.credentialsPath ?? config.credentialsPath, "o caminho das credenciais do Google");
|
|
86
|
-
return new sheetsSource_1.SheetsSource((0, client_1.createSheetsClient)(credentialsPath), source.spreadsheetId, source.sheet
|
|
86
|
+
return new sheetsSource_1.SheetsSource((0, client_1.createSheetsClient)(credentialsPath), source.spreadsheetId, source.sheet, {
|
|
87
|
+
allSheets: source.allSheets,
|
|
88
|
+
sheetColumn: source.sheetColumn,
|
|
89
|
+
});
|
|
87
90
|
}
|
|
88
91
|
case "custom":
|
|
89
92
|
return (0, registry_1.createCustomSource)(source.adapter, source.options ?? {}, logger);
|
package/dist/io/safety.js
CHANGED
|
@@ -32,6 +32,9 @@ function assertSheetsAreSafe(config) {
|
|
|
32
32
|
const destinationId = destination.spreadsheetId ?? config.spreadsheetId;
|
|
33
33
|
if (source.spreadsheetId !== destinationId)
|
|
34
34
|
return;
|
|
35
|
+
if (source.allSheets) {
|
|
36
|
+
throw new Error("A fonte lê todas as abas, então o resultado precisa ir pra outra planilha. Senão ele leria também a aba do resultado.");
|
|
37
|
+
}
|
|
35
38
|
if (source.sheet === undefined) {
|
|
36
39
|
throw new Error('A fonte e o destino são a mesma planilha. Coloque "sheet" na fonte com o nome da aba dos dados originais, senão ele pode ler e apagar a mesma aba.');
|
|
37
40
|
}
|
|
@@ -6,5 +6,6 @@ export interface WriteOptions {
|
|
|
6
6
|
sheetName?: string | undefined;
|
|
7
7
|
logger?: Logger | undefined;
|
|
8
8
|
}
|
|
9
|
+
export declare function visibleSheetTitles(sheets: sheets_v4.Sheets, spreadsheetId: string): Promise<string[]>;
|
|
9
10
|
export declare function readSheet(sheets: sheets_v4.Sheets, spreadsheetId: string, sheetName?: string): Promise<unknown[][]>;
|
|
10
11
|
export declare function writeData(sheets: sheets_v4.Sheets, spreadsheetId: string, data: TableRow[], options?: WriteOptions): Promise<void>;
|
package/dist/io/sheets/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createSheetsClient = createSheetsClient;
|
|
4
|
+
exports.visibleSheetTitles = visibleSheetTitles;
|
|
4
5
|
exports.readSheet = readSheet;
|
|
5
6
|
exports.writeData = writeData;
|
|
6
7
|
const optional_1 = require("../optional");
|
|
@@ -21,17 +22,22 @@ function quoteSheetTitle(title) {
|
|
|
21
22
|
async function listSheets(sheets, spreadsheetId) {
|
|
22
23
|
const response = await sheets.spreadsheets.get({
|
|
23
24
|
spreadsheetId,
|
|
24
|
-
fields: "sheets.properties(sheetId,title)",
|
|
25
|
+
fields: "sheets.properties(sheetId,title,hidden)",
|
|
25
26
|
});
|
|
26
27
|
const found = [];
|
|
27
28
|
for (const sheet of response.data.sheets ?? []) {
|
|
28
|
-
const { title, sheetId } = sheet.properties ?? {};
|
|
29
|
+
const { title, sheetId, hidden } = sheet.properties ?? {};
|
|
29
30
|
if (title && sheetId !== null && sheetId !== undefined) {
|
|
30
|
-
found.push({ title, sheetId });
|
|
31
|
+
found.push({ title, sheetId, hidden: hidden === true });
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
return found;
|
|
34
35
|
}
|
|
36
|
+
async function visibleSheetTitles(sheets, spreadsheetId) {
|
|
37
|
+
return (await listSheets(sheets, spreadsheetId))
|
|
38
|
+
.filter((sheet) => !sheet.hidden)
|
|
39
|
+
.map((sheet) => sheet.title);
|
|
40
|
+
}
|
|
35
41
|
async function resolveSheet(sheets, spreadsheetId, sheetName, logger = logger_1.consoleLogger) {
|
|
36
42
|
const existing = await listSheets(sheets, spreadsheetId);
|
|
37
43
|
const found = sheetName === undefined
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { sheets_v4 } from "@googleapis/sheets";
|
|
2
2
|
import { TableRow } from "../../types";
|
|
3
3
|
import { Source } from "../types";
|
|
4
|
+
import { SheetRows, TabOptions } from "../tabs";
|
|
4
5
|
export declare class SheetsSource implements Source {
|
|
5
6
|
private readonly sheets;
|
|
6
7
|
private readonly spreadsheetId;
|
|
7
8
|
private readonly sheet?;
|
|
8
|
-
|
|
9
|
+
private readonly tabs;
|
|
10
|
+
constructor(sheets: sheets_v4.Sheets, spreadsheetId: string, sheet?: string | undefined, tabs?: TabOptions);
|
|
11
|
+
readTabs(): Promise<SheetRows[]>;
|
|
9
12
|
read(): Promise<TableRow[]>;
|
|
10
13
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SheetsSource = void 0;
|
|
4
4
|
const client_1 = require("./client");
|
|
5
5
|
const cells_1 = require("../../cells");
|
|
6
|
+
const tabs_1 = require("../tabs");
|
|
6
7
|
function fromSheetCell(value) {
|
|
7
8
|
if (value === null || value === undefined || value === "")
|
|
8
9
|
return null;
|
|
@@ -10,39 +11,61 @@ function fromSheetCell(value) {
|
|
|
10
11
|
return value;
|
|
11
12
|
return String(value);
|
|
12
13
|
}
|
|
14
|
+
function rowsFrom(values) {
|
|
15
|
+
const [header, ...lines] = values;
|
|
16
|
+
if (!header)
|
|
17
|
+
return [];
|
|
18
|
+
const columns = header.map((cell) => {
|
|
19
|
+
const name = fromSheetCell(cell);
|
|
20
|
+
return name === null ? null : String(name).trim();
|
|
21
|
+
});
|
|
22
|
+
const rows = [];
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const row = {};
|
|
25
|
+
let hasValue = false;
|
|
26
|
+
columns.forEach((column, index) => {
|
|
27
|
+
if (column === null)
|
|
28
|
+
return;
|
|
29
|
+
const value = fromSheetCell(line[index]);
|
|
30
|
+
(0, cells_1.setCell)(row, column, value);
|
|
31
|
+
if (value !== null)
|
|
32
|
+
hasValue = true;
|
|
33
|
+
});
|
|
34
|
+
if (hasValue)
|
|
35
|
+
rows.push(row);
|
|
36
|
+
}
|
|
37
|
+
return rows;
|
|
38
|
+
}
|
|
13
39
|
class SheetsSource {
|
|
14
40
|
sheets;
|
|
15
41
|
spreadsheetId;
|
|
16
42
|
sheet;
|
|
17
|
-
|
|
43
|
+
tabs;
|
|
44
|
+
constructor(sheets, spreadsheetId, sheet, tabs = {}) {
|
|
18
45
|
this.sheets = sheets;
|
|
19
46
|
this.spreadsheetId = spreadsheetId;
|
|
20
47
|
this.sheet = sheet;
|
|
48
|
+
this.tabs = tabs;
|
|
21
49
|
}
|
|
22
|
-
async
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const row = {};
|
|
33
|
-
let hasValue = false;
|
|
34
|
-
columns.forEach((column, index) => {
|
|
35
|
-
if (column === null)
|
|
36
|
-
return;
|
|
37
|
-
const value = fromSheetCell(values[index]);
|
|
38
|
-
(0, cells_1.setCell)(row, column, value);
|
|
39
|
-
if (value !== null)
|
|
40
|
-
hasValue = true;
|
|
50
|
+
async readTabs() {
|
|
51
|
+
const titles = await (0, client_1.visibleSheetTitles)(this.sheets, this.spreadsheetId);
|
|
52
|
+
if (titles.length === 0) {
|
|
53
|
+
throw new Error("A planilha não tem nenhuma aba visível pra ler.");
|
|
54
|
+
}
|
|
55
|
+
const tabs = [];
|
|
56
|
+
for (const title of titles) {
|
|
57
|
+
tabs.push({
|
|
58
|
+
sheet: title,
|
|
59
|
+
rows: rowsFrom(await (0, client_1.readSheet)(this.sheets, this.spreadsheetId, title)),
|
|
41
60
|
});
|
|
42
|
-
if (hasValue)
|
|
43
|
-
rows.push(row);
|
|
44
61
|
}
|
|
45
|
-
return
|
|
62
|
+
return tabs;
|
|
63
|
+
}
|
|
64
|
+
async read() {
|
|
65
|
+
if (this.tabs.allSheets) {
|
|
66
|
+
return (0, tabs_1.joinTabs)(await this.readTabs(), this.tabs.sheetColumn ?? tabs_1.DEFAULT_SHEET_COLUMN, "a planilha do Google");
|
|
67
|
+
}
|
|
68
|
+
return rowsFrom(await (0, client_1.readSheet)(this.sheets, this.spreadsheetId, this.sheet));
|
|
46
69
|
}
|
|
47
70
|
}
|
|
48
71
|
exports.SheetsSource = SheetsSource;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TableRow } from "../types";
|
|
2
|
+
export declare const DEFAULT_SHEET_COLUMN = "aba";
|
|
3
|
+
export interface SheetRows {
|
|
4
|
+
sheet: string;
|
|
5
|
+
rows: TableRow[];
|
|
6
|
+
}
|
|
7
|
+
export interface TabOptions {
|
|
8
|
+
allSheets?: boolean | undefined;
|
|
9
|
+
sheetColumn?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function joinTabs(tabs: SheetRows[], column: string, where: string): TableRow[];
|