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/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
- async function runConfig(cli, logger) {
25
- const configPath = path_1.default.resolve(cli.config);
26
- if (!fs_1.default.existsSync(configPath)) {
27
- throw new Error(`Não achei a config ${cli.config}. Se era pra ser um comando, veja a lista com "datera --help".`);
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
- const folder = path_1.default.dirname(configPath);
30
- const input = cli.input === undefined ? undefined : path_1.default.resolve(cli.input);
31
- const output = cli.output === undefined ? undefined : path_1.default.resolve(cli.output);
32
- dotenv_1.default.config({ path: path_1.default.join(folder, ".env"), quiet: true });
33
- const raw = JSON.parse(fs_1.default.readFileSync(configPath, "utf-8"));
34
- if (input)
35
- raw.source = (0, api_1.sourceFromPath)(input);
36
- if (output)
37
- raw.destination = (0, api_1.destinationFromPath)(output);
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>;
@@ -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),
@@ -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>>;
@@ -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
- export interface ExcelSourceOptions {
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
- const header = new Map();
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;
@@ -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>;
@@ -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
- constructor(sheets: sheets_v4.Sheets, spreadsheetId: string, sheet?: string | undefined);
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
- constructor(sheets, spreadsheetId, sheet) {
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 read() {
23
- const [header, ...lines] = await (0, client_1.readSheet)(this.sheets, this.spreadsheetId, this.sheet);
24
- if (!header)
25
- return [];
26
- const columns = header.map((cell) => {
27
- const name = fromSheetCell(cell);
28
- return name === null ? null : String(name).trim();
29
- });
30
- const rows = [];
31
- for (const values of lines) {
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 rows;
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[];