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/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@ Os números seguem o [versionamento semântico](https://semver.org/lang/pt-BR/):
|
|
|
10
10
|
|
|
11
11
|
## [Não lançado]
|
|
12
12
|
|
|
13
|
+
## [0.3.0] - 2026-09-30
|
|
14
|
+
|
|
15
|
+
### Adicionado
|
|
16
|
+
|
|
17
|
+
- converter vários arquivos de uma vez: `datera convert "matriculas/*.xlsx" --to csv --out-dir convertidos`, aceitando pasta, `*` no nome e lista de arquivos, e o `convertMany` na API (com `findFiles` e `planOutputs`)
|
|
18
|
+
- ler todas as abas de um Excel ou de uma planilha do Google de uma vez (`"allSheets": true`, `--all-sheets`), com uma coluna dizendo de qual aba veio cada linha. Com `--to`, cada aba vira um arquivo. Na API, o `readSheets` devolve aba por aba. No app, a opção **Ler todas as abas** no passo a passo, e no Datera Dev, os trechos na paleta
|
|
19
|
+
- alinhar colunas com nomes diferentes (`E-mail`, `email`, `Email`) com `"alignColumns": true`, e dizer na mão que uma coluna é outra com `rename`. No terminal, `--align-columns` e `--rename "Email do aluno=email"`, e o `datera columns` avisa quando tem colunas parecidas. Na API, o `alignColumns`, o `findSimilarColumns` e a opção nas regras do `clean` e no `readRows`, `convert` e `convertMany`. No Datera Dev, o trecho na paleta
|
|
20
|
+
- `datera run regras.json --input "matriculas/*.xlsx" --out-dir limpos` aplica as mesmas regras em cada arquivo, cada um com o seu resultado
|
|
21
|
+
|
|
13
22
|
## [0.2.1] - 2026-09-30
|
|
14
23
|
|
|
15
24
|
### Corrigido
|
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ Hoje ele consegue:
|
|
|
82
82
|
- ler e escrever em formatos diferentes sem mudar nada das regras
|
|
83
83
|
- gravar o resultado numa tabela nova de banco, sem nunca mexer numa tabela que não foi ele que criou
|
|
84
84
|
- converter um arquivo de um formato pra outro, tirar repetidos ou juntar cadastros com um comando só, sem montar config
|
|
85
|
+
- converter uma pasta inteira de uma vez, ou aplicar as mesmas regras em cada planilha do mês
|
|
85
86
|
- mostrar um raio-x das colunas de um arquivo (quantas vazias, quantos valores diferentes) antes de montar as regras
|
|
86
87
|
- ser usado dentro de outro projeto em JavaScript ou TypeScript, com as mesmas regras da config
|
|
87
88
|
|
package/dist/api/clean.d.ts
CHANGED
|
@@ -2,10 +2,15 @@ import { z } from "zod";
|
|
|
2
2
|
import { etlConfigSchema } from "../config";
|
|
3
3
|
import { mergeColumnSchema } from "../config/mergeSchema";
|
|
4
4
|
import { combineColumnsSchema, fillEmptySchema } from "../config/prepareSchema";
|
|
5
|
+
import { AlignOptions } from "../filters/align";
|
|
5
6
|
import { validationSchema } from "../config/validationSchema";
|
|
6
7
|
import { PendingReason, StepReport } from "../pipeline";
|
|
7
8
|
import { TableRow } from "../types";
|
|
8
9
|
export declare const rulesSchema: z.ZodObject<{
|
|
10
|
+
alignColumns: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
11
|
+
auto: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
rename: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
13
|
+
}, z.core.$strip>]>>;
|
|
9
14
|
fillEmpty: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
10
15
|
column: z.ZodString;
|
|
11
16
|
fallbackColumns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -114,6 +119,8 @@ type Validation = z.input<typeof validationSchema>;
|
|
|
114
119
|
type ValidationRule = Validation["rules"][number];
|
|
115
120
|
type MergeColumn = z.input<typeof mergeColumnSchema>;
|
|
116
121
|
export declare function fillEmpty(rows: TableRow[], rules: FillEmptyRule[]): TableRow[];
|
|
122
|
+
export declare function alignColumns(rows: TableRow[], options?: AlignOptions): TableRow[];
|
|
123
|
+
export declare function findSimilarColumns(rows: TableRow[]): string[][];
|
|
117
124
|
export declare function combineColumns(rows: TableRow[], rules: CombineRule[]): TableRow[];
|
|
118
125
|
export declare function validate(rows: TableRow[], rules: ValidationRule[] | Validation): {
|
|
119
126
|
valid: TableRow[];
|
package/dist/api/clean.js
CHANGED
|
@@ -5,6 +5,8 @@ exports.defineConfig = defineConfig;
|
|
|
5
5
|
exports.defineRules = defineRules;
|
|
6
6
|
exports.clean = clean;
|
|
7
7
|
exports.fillEmpty = fillEmpty;
|
|
8
|
+
exports.alignColumns = alignColumns;
|
|
9
|
+
exports.findSimilarColumns = findSimilarColumns;
|
|
8
10
|
exports.combineColumns = combineColumns;
|
|
9
11
|
exports.validate = validate;
|
|
10
12
|
exports.dedupe = dedupe;
|
|
@@ -13,12 +15,14 @@ exports.merge = merge;
|
|
|
13
15
|
const zod_1 = require("zod");
|
|
14
16
|
const mergeSchema_1 = require("../config/mergeSchema");
|
|
15
17
|
const prepareSchema_1 = require("../config/prepareSchema");
|
|
18
|
+
const align_1 = require("../filters/align");
|
|
16
19
|
const validationSchema_1 = require("../config/validationSchema");
|
|
17
20
|
const normalizers_1 = require("../normalizers");
|
|
18
21
|
const pipeline_1 = require("../pipeline");
|
|
19
22
|
const parse_1 = require("./parse");
|
|
20
23
|
exports.rulesSchema = zod_1.z
|
|
21
24
|
.object({
|
|
25
|
+
alignColumns: prepareSchema_1.alignColumnsSchema.optional(),
|
|
22
26
|
fillEmpty: zod_1.z.array(prepareSchema_1.fillEmptySchema).optional(),
|
|
23
27
|
combineColumns: zod_1.z.array(prepareSchema_1.combineColumnsSchema).optional(),
|
|
24
28
|
validation: validationSchema_1.validationSchema.optional(),
|
|
@@ -53,6 +57,7 @@ function configOf(rules) {
|
|
|
53
57
|
const mode = rules.merge ? "merge" : rules.dedupe ? "dedupe" : "raw";
|
|
54
58
|
const config = {
|
|
55
59
|
mode,
|
|
60
|
+
alignColumns: rules.alignColumns,
|
|
56
61
|
fillEmpty: rules.fillEmpty,
|
|
57
62
|
combineColumns: rules.combineColumns,
|
|
58
63
|
validation: rules.validation,
|
|
@@ -84,6 +89,17 @@ function clean(rows, rules = {}, options = {}) {
|
|
|
84
89
|
function fillEmpty(rows, rules) {
|
|
85
90
|
return clean(rows, { fillEmpty: rules }).rows;
|
|
86
91
|
}
|
|
92
|
+
function alignColumns(rows, options = {}) {
|
|
93
|
+
return clean(rows, { alignColumns: options }).rows;
|
|
94
|
+
}
|
|
95
|
+
function findSimilarColumns(rows) {
|
|
96
|
+
const names = new Set();
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
for (const name of Object.keys(row))
|
|
99
|
+
names.add(name);
|
|
100
|
+
}
|
|
101
|
+
return (0, align_1.similarColumns)([...names]);
|
|
102
|
+
}
|
|
87
103
|
function combineColumns(rows, rules) {
|
|
88
104
|
return clean(rows, { combineColumns: rules }).rows;
|
|
89
105
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface PlannedFile {
|
|
2
|
+
input: string;
|
|
3
|
+
output: string;
|
|
4
|
+
sheet?: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare function findFiles(inputs: string | string[]): string[];
|
|
7
|
+
export declare function extensionFor(target: string): string;
|
|
8
|
+
export declare function checkPlan(plan: PlannedFile[]): PlannedFile[];
|
|
9
|
+
export declare function planOutputs(files: string[], target: string, outDir?: string): PlannedFile[];
|
|
10
|
+
export declare function planSheetOutputs(input: string, sheets: string[], target: string, outDir?: string): PlannedFile[];
|
|
@@ -0,0 +1,145 @@
|
|
|
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.findFiles = findFiles;
|
|
7
|
+
exports.extensionFor = extensionFor;
|
|
8
|
+
exports.checkPlan = checkPlan;
|
|
9
|
+
exports.planOutputs = planOutputs;
|
|
10
|
+
exports.planSheetOutputs = planSheetOutputs;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const safety_1 = require("../io/safety");
|
|
14
|
+
const formats_1 = require("./formats");
|
|
15
|
+
const TARGETS = {
|
|
16
|
+
csv: ".csv",
|
|
17
|
+
tsv: ".tsv",
|
|
18
|
+
json: ".json",
|
|
19
|
+
xml: ".xml",
|
|
20
|
+
xlsx: ".xlsx",
|
|
21
|
+
excel: ".xlsx",
|
|
22
|
+
parquet: ".parquet",
|
|
23
|
+
db: ".db",
|
|
24
|
+
sqlite: ".db",
|
|
25
|
+
sqlite3: ".sqlite3",
|
|
26
|
+
};
|
|
27
|
+
function hasWildcard(text) {
|
|
28
|
+
return /[*?]/.test(text);
|
|
29
|
+
}
|
|
30
|
+
function patternToRegExp(pattern) {
|
|
31
|
+
const source = pattern
|
|
32
|
+
.split("")
|
|
33
|
+
.map((char) => {
|
|
34
|
+
if (char === "*")
|
|
35
|
+
return ".*";
|
|
36
|
+
if (char === "?")
|
|
37
|
+
return ".";
|
|
38
|
+
return char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
39
|
+
})
|
|
40
|
+
.join("");
|
|
41
|
+
const flags = process.platform === "win32" ? "i" : "";
|
|
42
|
+
return new RegExp(`^${source}$`, flags);
|
|
43
|
+
}
|
|
44
|
+
function isKnownFormat(file) {
|
|
45
|
+
try {
|
|
46
|
+
(0, formats_1.detectFormat)(file);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function filesIn(folder, accept) {
|
|
54
|
+
if (!fs_1.default.existsSync(folder) || !fs_1.default.statSync(folder).isDirectory()) {
|
|
55
|
+
throw new Error(`Não achei a pasta ${folder}.`);
|
|
56
|
+
}
|
|
57
|
+
return fs_1.default
|
|
58
|
+
.readdirSync(folder, { withFileTypes: true })
|
|
59
|
+
.filter((entry) => entry.isFile() && !entry.name.startsWith("."))
|
|
60
|
+
.map((entry) => entry.name)
|
|
61
|
+
.filter((name) => accept(name) && isKnownFormat(name))
|
|
62
|
+
.map((name) => path_1.default.join(folder, name));
|
|
63
|
+
}
|
|
64
|
+
function expand(input) {
|
|
65
|
+
if (hasWildcard(input)) {
|
|
66
|
+
const folder = path_1.default.dirname(input);
|
|
67
|
+
if (hasWildcard(folder)) {
|
|
68
|
+
throw new Error(`Em "${input}", o * só pode estar no nome do arquivo, não no nome da pasta.`);
|
|
69
|
+
}
|
|
70
|
+
const pattern = patternToRegExp(path_1.default.basename(input));
|
|
71
|
+
return filesIn(folder, (name) => pattern.test(name));
|
|
72
|
+
}
|
|
73
|
+
if (fs_1.default.existsSync(input) && fs_1.default.statSync(input).isDirectory()) {
|
|
74
|
+
return filesIn(input, () => true);
|
|
75
|
+
}
|
|
76
|
+
if (!fs_1.default.existsSync(input)) {
|
|
77
|
+
throw new Error(`Não achei o arquivo ${input}.`);
|
|
78
|
+
}
|
|
79
|
+
return [input];
|
|
80
|
+
}
|
|
81
|
+
function findFiles(inputs) {
|
|
82
|
+
const list = Array.isArray(inputs) ? inputs : [inputs];
|
|
83
|
+
const found = [];
|
|
84
|
+
for (const input of list) {
|
|
85
|
+
for (const file of expand(input)) {
|
|
86
|
+
if (!found.some((other) => (0, safety_1.isSameFile)(other, file))) {
|
|
87
|
+
found.push(file);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (found.length === 0) {
|
|
92
|
+
throw new Error(`Nenhum arquivo encontrado em ${list.join(", ")}. Confira o caminho e a extensão.`);
|
|
93
|
+
}
|
|
94
|
+
return found.sort((a, b) => a.localeCompare(b, "pt-BR", { numeric: true }));
|
|
95
|
+
}
|
|
96
|
+
function extensionFor(target) {
|
|
97
|
+
const key = target.trim().toLowerCase().replace(/^\./, "");
|
|
98
|
+
if (!Object.hasOwn(TARGETS, key)) {
|
|
99
|
+
throw new Error(`Não conheço o formato "${target}". Os que dá pra usar: ${Object.keys(TARGETS).join(", ")}.`);
|
|
100
|
+
}
|
|
101
|
+
return TARGETS[key];
|
|
102
|
+
}
|
|
103
|
+
function fileSafe(name) {
|
|
104
|
+
const clean = name
|
|
105
|
+
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, "_")
|
|
106
|
+
.trim()
|
|
107
|
+
.replace(/[. ]+$/, "");
|
|
108
|
+
if (clean === "")
|
|
109
|
+
return "aba";
|
|
110
|
+
return /^(con|prn|aux|nul|com\d|lpt\d)$/i.test(clean) ? `_${clean}` : clean;
|
|
111
|
+
}
|
|
112
|
+
function checkPlan(plan) {
|
|
113
|
+
const label = (item) => item.sheet === undefined
|
|
114
|
+
? item.input
|
|
115
|
+
: `a aba "${item.sheet}" de ${item.input}`;
|
|
116
|
+
plan.forEach((item, index) => {
|
|
117
|
+
const twin = plan.find((other, otherIndex) => otherIndex < index && (0, safety_1.isSameFile)(other.output, item.output));
|
|
118
|
+
if (twin) {
|
|
119
|
+
throw new Error(`${label(twin)} e ${label(item)} iam virar o mesmo arquivo (${item.output}). Renomeie um deles ou converta em pastas separadas.`);
|
|
120
|
+
}
|
|
121
|
+
const overwritten = plan.find((other) => (0, safety_1.isSameFile)(other.input, item.output));
|
|
122
|
+
if (overwritten) {
|
|
123
|
+
throw new Error(`Converter ${label(item)} ia gravar por cima de ${overwritten.input}, que é uma das entradas. Escolha outro formato ou use --out-dir com outra pasta.`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return plan;
|
|
127
|
+
}
|
|
128
|
+
function outputPath(input, extension, outDir, suffix = "") {
|
|
129
|
+
return path_1.default.join(outDir ?? path_1.default.dirname(input), `${path_1.default.basename(input, path_1.default.extname(input))}${suffix}${extension}`);
|
|
130
|
+
}
|
|
131
|
+
function planOutputs(files, target, outDir) {
|
|
132
|
+
const extension = extensionFor(target);
|
|
133
|
+
return checkPlan(files.map((input) => ({
|
|
134
|
+
input,
|
|
135
|
+
output: outputPath(input, extension, outDir),
|
|
136
|
+
})));
|
|
137
|
+
}
|
|
138
|
+
function planSheetOutputs(input, sheets, target, outDir) {
|
|
139
|
+
const extension = extensionFor(target);
|
|
140
|
+
return sheets.map((sheet) => ({
|
|
141
|
+
input,
|
|
142
|
+
sheet,
|
|
143
|
+
output: outputPath(input, extension, outDir, `-${fileSafe(sheet)}`),
|
|
144
|
+
}));
|
|
145
|
+
}
|
package/dist/api/formats.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { DestinationConfig, SourceConfig } from "../config/ioSchema";
|
|
2
|
+
import { AlignOptions } from "../filters/align";
|
|
2
3
|
export type FileFormat = "csv" | "json" | "xml" | "excel" | "parquet" | "sqlite";
|
|
3
4
|
export interface ReadOptions {
|
|
4
5
|
table?: string | undefined;
|
|
5
6
|
sheet?: string | undefined;
|
|
7
|
+
allSheets?: boolean | undefined;
|
|
8
|
+
sheetColumn?: string | undefined;
|
|
9
|
+
alignColumns?: boolean | AlignOptions | undefined;
|
|
6
10
|
recordsPath?: string | undefined;
|
|
7
11
|
delimiter?: string | undefined;
|
|
8
12
|
encoding?: "utf-8" | "latin1" | undefined;
|
package/dist/api/formats.js
CHANGED
|
@@ -61,7 +61,11 @@ function sourceFromPath(filePath, options = {}) {
|
|
|
61
61
|
return defined({
|
|
62
62
|
type: "excel",
|
|
63
63
|
path: filePath,
|
|
64
|
-
sheet: options.sheet,
|
|
64
|
+
sheet: options.allSheets ? undefined : options.sheet,
|
|
65
|
+
allSheets: options.allSheets || undefined,
|
|
66
|
+
sheetColumn: options.allSheets
|
|
67
|
+
? options.sheetColumn
|
|
68
|
+
: undefined,
|
|
65
69
|
});
|
|
66
70
|
case "parquet":
|
|
67
71
|
return { type: "parquet", path: filePath };
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export { detectFormat, sourceFromPath, destinationFromPath } from "./formats";
|
|
2
2
|
export type { FileFormat, ReadOptions, WriteOptions } from "./formats";
|
|
3
|
-
export { readRows, writeRows, convert, DEFAULT_PENDING_NAME } from "./rows";
|
|
4
|
-
export
|
|
5
|
-
export {
|
|
3
|
+
export { readRows, writeRows, convert, convertMany, readSheets, DEFAULT_PENDING_NAME, } from "./rows";
|
|
4
|
+
export { findFiles, planOutputs, planSheetOutputs, extensionFor, } from "./files";
|
|
5
|
+
export { DEFAULT_SHEET_COLUMN } from "../io/tabs";
|
|
6
|
+
export type { SheetRows } from "../io/tabs";
|
|
7
|
+
export type { PlannedFile } from "./files";
|
|
8
|
+
export type { Input, Output, ReadRowsOptions, WriteRowsOptions, ConvertOptions, ConvertManyOptions, ConvertedFile, } from "./rows";
|
|
9
|
+
export { clean, alignColumns, findSimilarColumns, fillEmpty, combineColumns, validate, dedupe, merge, mergeColumnsFor, defineConfig, defineRules, rulesSchema, } from "./clean";
|
|
6
10
|
export type { Rules, ConfigInput, CleanOptions, CleanResult, DedupeOptions, MergeOptions, } from "./clean";
|
|
7
11
|
export { describeColumns, listNormalizers, normalize } from "./inspect";
|
|
8
12
|
export type { ColumnInfo, ColumnKind } from "./inspect";
|
|
13
|
+
export type { AlignOptions } from "../filters/align";
|
package/dist/api/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalize = exports.listNormalizers = exports.describeColumns = exports.rulesSchema = exports.defineRules = exports.defineConfig = exports.mergeColumnsFor = exports.merge = exports.dedupe = exports.validate = exports.combineColumns = exports.fillEmpty = exports.clean = exports.DEFAULT_PENDING_NAME = exports.convert = exports.writeRows = exports.readRows = exports.destinationFromPath = exports.sourceFromPath = exports.detectFormat = void 0;
|
|
3
|
+
exports.normalize = exports.listNormalizers = exports.describeColumns = exports.rulesSchema = exports.defineRules = exports.defineConfig = exports.mergeColumnsFor = exports.merge = exports.dedupe = exports.validate = exports.combineColumns = exports.fillEmpty = exports.findSimilarColumns = exports.alignColumns = exports.clean = exports.DEFAULT_SHEET_COLUMN = exports.extensionFor = exports.planSheetOutputs = exports.planOutputs = exports.findFiles = exports.DEFAULT_PENDING_NAME = exports.readSheets = exports.convertMany = exports.convert = exports.writeRows = exports.readRows = exports.destinationFromPath = exports.sourceFromPath = exports.detectFormat = void 0;
|
|
4
4
|
var formats_1 = require("./formats");
|
|
5
5
|
Object.defineProperty(exports, "detectFormat", { enumerable: true, get: function () { return formats_1.detectFormat; } });
|
|
6
6
|
Object.defineProperty(exports, "sourceFromPath", { enumerable: true, get: function () { return formats_1.sourceFromPath; } });
|
|
@@ -9,9 +9,20 @@ var rows_1 = require("./rows");
|
|
|
9
9
|
Object.defineProperty(exports, "readRows", { enumerable: true, get: function () { return rows_1.readRows; } });
|
|
10
10
|
Object.defineProperty(exports, "writeRows", { enumerable: true, get: function () { return rows_1.writeRows; } });
|
|
11
11
|
Object.defineProperty(exports, "convert", { enumerable: true, get: function () { return rows_1.convert; } });
|
|
12
|
+
Object.defineProperty(exports, "convertMany", { enumerable: true, get: function () { return rows_1.convertMany; } });
|
|
13
|
+
Object.defineProperty(exports, "readSheets", { enumerable: true, get: function () { return rows_1.readSheets; } });
|
|
12
14
|
Object.defineProperty(exports, "DEFAULT_PENDING_NAME", { enumerable: true, get: function () { return rows_1.DEFAULT_PENDING_NAME; } });
|
|
15
|
+
var files_1 = require("./files");
|
|
16
|
+
Object.defineProperty(exports, "findFiles", { enumerable: true, get: function () { return files_1.findFiles; } });
|
|
17
|
+
Object.defineProperty(exports, "planOutputs", { enumerable: true, get: function () { return files_1.planOutputs; } });
|
|
18
|
+
Object.defineProperty(exports, "planSheetOutputs", { enumerable: true, get: function () { return files_1.planSheetOutputs; } });
|
|
19
|
+
Object.defineProperty(exports, "extensionFor", { enumerable: true, get: function () { return files_1.extensionFor; } });
|
|
20
|
+
var tabs_1 = require("../io/tabs");
|
|
21
|
+
Object.defineProperty(exports, "DEFAULT_SHEET_COLUMN", { enumerable: true, get: function () { return tabs_1.DEFAULT_SHEET_COLUMN; } });
|
|
13
22
|
var clean_1 = require("./clean");
|
|
14
23
|
Object.defineProperty(exports, "clean", { enumerable: true, get: function () { return clean_1.clean; } });
|
|
24
|
+
Object.defineProperty(exports, "alignColumns", { enumerable: true, get: function () { return clean_1.alignColumns; } });
|
|
25
|
+
Object.defineProperty(exports, "findSimilarColumns", { enumerable: true, get: function () { return clean_1.findSimilarColumns; } });
|
|
15
26
|
Object.defineProperty(exports, "fillEmpty", { enumerable: true, get: function () { return clean_1.fillEmpty; } });
|
|
16
27
|
Object.defineProperty(exports, "combineColumns", { enumerable: true, get: function () { return clean_1.combineColumns; } });
|
|
17
28
|
Object.defineProperty(exports, "validate", { enumerable: true, get: function () { return clean_1.validate; } });
|
package/dist/api/rows.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { DestinationConfig, SourceConfig } from "../config/ioSchema";
|
|
|
2
2
|
import { Logger } from "../logger";
|
|
3
3
|
import { TableRow } from "../types";
|
|
4
4
|
import { ReadOptions, WriteOptions } from "./formats";
|
|
5
|
+
import { SheetRows } from "../io/tabs";
|
|
5
6
|
export type Input = string | SourceConfig;
|
|
6
7
|
export type Output = string | DestinationConfig;
|
|
7
8
|
export interface ReadRowsOptions extends ReadOptions {
|
|
@@ -21,3 +22,15 @@ export interface ConvertOptions {
|
|
|
21
22
|
logger?: Logger | undefined;
|
|
22
23
|
}
|
|
23
24
|
export declare function convert(input: Input, output: Output, options?: ConvertOptions): Promise<number>;
|
|
25
|
+
export declare function readSheets(input: Input, options?: ReadRowsOptions): Promise<SheetRows[]>;
|
|
26
|
+
export interface ConvertManyOptions extends ConvertOptions {
|
|
27
|
+
to: string;
|
|
28
|
+
outDir?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
export interface ConvertedFile {
|
|
31
|
+
input: string;
|
|
32
|
+
output: string;
|
|
33
|
+
rows: number;
|
|
34
|
+
sheet?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
export declare function convertMany(inputs: string | string[], options: ConvertManyOptions): Promise<ConvertedFile[]>;
|
package/dist/api/rows.js
CHANGED
|
@@ -4,12 +4,21 @@ exports.DEFAULT_PENDING_NAME = void 0;
|
|
|
4
4
|
exports.readRows = readRows;
|
|
5
5
|
exports.writeRows = writeRows;
|
|
6
6
|
exports.convert = convert;
|
|
7
|
+
exports.readSheets = readSheets;
|
|
8
|
+
exports.convertMany = convertMany;
|
|
7
9
|
const ioSchema_1 = require("../config/ioSchema");
|
|
8
10
|
const factory_1 = require("../io/factory");
|
|
9
11
|
const normalizers_1 = require("../normalizers");
|
|
10
12
|
const logger_1 = require("../logger");
|
|
11
13
|
const formats_1 = require("./formats");
|
|
12
14
|
const parse_1 = require("./parse");
|
|
15
|
+
const files_1 = require("./files");
|
|
16
|
+
const formats_2 = require("./formats");
|
|
17
|
+
const align_1 = require("../filters/align");
|
|
18
|
+
function aligned(rows, options) {
|
|
19
|
+
const align = (0, align_1.alignOptionsOf)(options.alignColumns);
|
|
20
|
+
return align ? (0, align_1.alignRows)(rows, align) : rows;
|
|
21
|
+
}
|
|
13
22
|
exports.DEFAULT_PENDING_NAME = "Pendências";
|
|
14
23
|
function sourceOf(input, options) {
|
|
15
24
|
return typeof input === "string"
|
|
@@ -26,7 +35,7 @@ async function readRows(input, options = {}) {
|
|
|
26
35
|
const config = { mode: "raw", source: sourceOf(input, options) };
|
|
27
36
|
const source = (0, factory_1.createSource)(config, options.logger ?? logger_1.silentLogger);
|
|
28
37
|
try {
|
|
29
|
-
return await source.read();
|
|
38
|
+
return aligned(await source.read(), options);
|
|
30
39
|
}
|
|
31
40
|
finally {
|
|
32
41
|
await source.close?.();
|
|
@@ -53,7 +62,7 @@ async function convert(input, output, options = {}) {
|
|
|
53
62
|
const reader = (0, factory_1.createSource)(config, logger);
|
|
54
63
|
const sink = (0, factory_1.createSink)(config, logger);
|
|
55
64
|
try {
|
|
56
|
-
const rows = await reader.read();
|
|
65
|
+
const rows = aligned(await reader.read(), options.read ?? {});
|
|
57
66
|
await sink.write(rows);
|
|
58
67
|
return rows.length;
|
|
59
68
|
}
|
|
@@ -61,3 +70,74 @@ async function convert(input, output, options = {}) {
|
|
|
61
70
|
await reader.close?.();
|
|
62
71
|
}
|
|
63
72
|
}
|
|
73
|
+
async function readSheets(input, options = {}) {
|
|
74
|
+
(0, normalizers_1.registerBuiltinKeyNormalizers)();
|
|
75
|
+
const config = { mode: "raw", source: sourceOf(input, options) };
|
|
76
|
+
const source = (0, factory_1.createSource)(config, options.logger ?? logger_1.silentLogger);
|
|
77
|
+
try {
|
|
78
|
+
if (!source.readTabs) {
|
|
79
|
+
throw new Error("Só dá pra ler aba por aba de um arquivo Excel ou de uma planilha do Google.");
|
|
80
|
+
}
|
|
81
|
+
return (await source.readTabs()).map((tab) => ({
|
|
82
|
+
sheet: tab.sheet,
|
|
83
|
+
rows: aligned(tab.rows, options),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await source.close?.();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function convertMany(inputs, options) {
|
|
91
|
+
const files = (0, files_1.findFiles)(inputs);
|
|
92
|
+
const byTab = options.read?.allSheets === true;
|
|
93
|
+
if (!byTab) {
|
|
94
|
+
return convertPlan((0, files_1.planOutputs)(files, options.to, options.outDir), options, new Map());
|
|
95
|
+
}
|
|
96
|
+
(0, files_1.extensionFor)(options.to);
|
|
97
|
+
const tabs = new Map();
|
|
98
|
+
const plan = [];
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
if ((0, formats_2.detectFormat)(file) !== "excel") {
|
|
101
|
+
plan.push(...(0, files_1.planOutputs)([file], options.to, options.outDir));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const sheets = await readSheets(file, {
|
|
105
|
+
...options.read,
|
|
106
|
+
logger: options.logger,
|
|
107
|
+
});
|
|
108
|
+
const planned = (0, files_1.planSheetOutputs)(file, sheets.map((tab) => tab.sheet), options.to, options.outDir);
|
|
109
|
+
planned.forEach((item, index) => tabs.set(item.output, sheets[index]));
|
|
110
|
+
plan.push(...planned);
|
|
111
|
+
}
|
|
112
|
+
return convertPlan((0, files_1.checkPlan)(plan), options, tabs);
|
|
113
|
+
}
|
|
114
|
+
async function convertPlan(plan, options, tabs) {
|
|
115
|
+
const read = { ...options.read, allSheets: undefined };
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const { input, output, sheet } of plan) {
|
|
118
|
+
try {
|
|
119
|
+
const tab = tabs.get(output);
|
|
120
|
+
let rows;
|
|
121
|
+
if (tab) {
|
|
122
|
+
await writeRows(tab.rows, output, {
|
|
123
|
+
...options.write,
|
|
124
|
+
logger: options.logger,
|
|
125
|
+
});
|
|
126
|
+
rows = tab.rows.length;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
rows = await convert(input, output, { ...options, read });
|
|
130
|
+
}
|
|
131
|
+
results.push({ input, output, rows, sheet });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
135
|
+
const done = results.length === 0
|
|
136
|
+
? ""
|
|
137
|
+
: ` Os ${results.length} anteriores já foram convertidos.`;
|
|
138
|
+
const where = sheet === undefined ? input : `${input} (aba ${sheet})`;
|
|
139
|
+
throw new Error(`Parei em ${where}: ${reason}${done}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type CliOptions = {
|
|
|
6
6
|
mode?: string | undefined;
|
|
7
7
|
dryRun: boolean;
|
|
8
8
|
input?: string | undefined;
|
|
9
|
+
outDir?: string | undefined;
|
|
10
|
+
to?: string | undefined;
|
|
9
11
|
output?: string | undefined;
|
|
10
12
|
} | {
|
|
11
13
|
command: "validate";
|
|
@@ -21,6 +23,13 @@ export type CliOptions = {
|
|
|
21
23
|
output: string;
|
|
22
24
|
read: ReadOptions;
|
|
23
25
|
write: WriteOptions;
|
|
26
|
+
} | {
|
|
27
|
+
command: "convert-many";
|
|
28
|
+
inputs: string[];
|
|
29
|
+
to: string;
|
|
30
|
+
outDir?: string | undefined;
|
|
31
|
+
read: ReadOptions;
|
|
32
|
+
write: WriteOptions;
|
|
24
33
|
} | {
|
|
25
34
|
command: "dedupe";
|
|
26
35
|
input: string;
|
|
@@ -58,5 +67,28 @@ export type CliOptions = {
|
|
|
58
67
|
value: string;
|
|
59
68
|
};
|
|
60
69
|
export declare function dateraVersion(): string;
|
|
70
|
+
interface ReadFlags {
|
|
71
|
+
table?: string;
|
|
72
|
+
sheet?: string;
|
|
73
|
+
allSheets?: boolean;
|
|
74
|
+
sheetColumn?: string;
|
|
75
|
+
alignColumns?: boolean;
|
|
76
|
+
rename?: [string, string][];
|
|
77
|
+
recordsPath?: string;
|
|
78
|
+
delimiter?: string;
|
|
79
|
+
encoding?: "utf-8" | "latin1";
|
|
80
|
+
}
|
|
81
|
+
interface WriteFlags {
|
|
82
|
+
outputTable?: string;
|
|
83
|
+
outputSheet?: string;
|
|
84
|
+
outputDelimiter?: string;
|
|
85
|
+
root?: string;
|
|
86
|
+
record?: string;
|
|
87
|
+
}
|
|
88
|
+
export declare function convertRequest(files: string[], flags: ReadFlags & WriteFlags & {
|
|
89
|
+
to?: string;
|
|
90
|
+
outDir?: string;
|
|
91
|
+
}): CliOptions;
|
|
61
92
|
export declare function buildProgram(onCommand: (options: CliOptions) => void): Command;
|
|
62
93
|
export declare function parseCli(argv?: string[]): CliOptions;
|
|
94
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.dateraVersion = dateraVersion;
|
|
7
|
+
exports.convertRequest = convertRequest;
|
|
7
8
|
exports.buildProgram = buildProgram;
|
|
8
9
|
exports.parseCli = parseCli;
|
|
9
10
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -19,6 +20,10 @@ function withReadFlags(command) {
|
|
|
19
20
|
return command
|
|
20
21
|
.option("--table <tabela>", "Tabela a ler, quando a entrada é SQLite.")
|
|
21
22
|
.option("--sheet <aba>", "Aba a ler, quando a entrada é Excel.")
|
|
23
|
+
.option("--all-sheets", 'Lê todas as abas do Excel, com uma coluna "aba" dizendo de onde veio cada linha. Com --to, cada aba vira um arquivo.')
|
|
24
|
+
.option("--sheet-column <nome>", 'Nome da coluna que guarda a aba, com --all-sheets (padrão: "aba").')
|
|
25
|
+
.option("--align-columns", "Junta as colunas que só mudam em maiúscula, acento ou espaço (E-mail, email, Email).")
|
|
26
|
+
.option("--rename <de=para>", 'Diz que uma coluna é outra (tipo --rename "Email do aluno=email"). Pode repetir.', renamePair)
|
|
22
27
|
.option("--records-path <caminho>", "Onde estão os registros no JSON ou XML (ex.: escola.alunos.aluno).")
|
|
23
28
|
.option("--delimiter <separador>", "Separador do CSV de entrada.")
|
|
24
29
|
.addOption(new commander_1.Option("--encoding <codificação>", "Codificação do CSV de entrada.").choices(["utf-8", "latin1"]));
|
|
@@ -35,6 +40,16 @@ function readOptions(flags) {
|
|
|
35
40
|
return {
|
|
36
41
|
table: flags.table,
|
|
37
42
|
sheet: flags.sheet,
|
|
43
|
+
allSheets: flags.allSheets,
|
|
44
|
+
sheetColumn: flags.sheetColumn,
|
|
45
|
+
alignColumns: flags.alignColumns || flags.rename
|
|
46
|
+
? {
|
|
47
|
+
auto: flags.alignColumns ?? false,
|
|
48
|
+
...(flags.rename
|
|
49
|
+
? { rename: Object.fromEntries(flags.rename) }
|
|
50
|
+
: {}),
|
|
51
|
+
}
|
|
52
|
+
: undefined,
|
|
38
53
|
recordsPath: flags.recordsPath,
|
|
39
54
|
delimiter: flags.delimiter,
|
|
40
55
|
encoding: flags.encoding,
|
|
@@ -49,6 +64,15 @@ function writeOptions(flags) {
|
|
|
49
64
|
record: flags.record,
|
|
50
65
|
};
|
|
51
66
|
}
|
|
67
|
+
function renamePair(value, previous = []) {
|
|
68
|
+
const index = value.indexOf("=");
|
|
69
|
+
const from = index < 0 ? "" : value.slice(0, index).trim();
|
|
70
|
+
const to = index < 0 ? "" : value.slice(index + 1).trim();
|
|
71
|
+
if (from === "" || to === "") {
|
|
72
|
+
throw new commander_1.InvalidArgumentError('use o formato "coluna antiga=coluna nova", tipo "Email do aluno=email".');
|
|
73
|
+
}
|
|
74
|
+
return [...previous, [from, to]];
|
|
75
|
+
}
|
|
52
76
|
function list(value, previous = []) {
|
|
53
77
|
return [
|
|
54
78
|
...previous,
|
|
@@ -89,6 +113,36 @@ const ERRORS = [
|
|
|
89
113
|
function translateError(text) {
|
|
90
114
|
return ERRORS.reduce((current, [pattern, replacement]) => current.replace(pattern, replacement), text);
|
|
91
115
|
}
|
|
116
|
+
function convertRequest(files, flags) {
|
|
117
|
+
const read = readOptions(flags);
|
|
118
|
+
const write = writeOptions(flags);
|
|
119
|
+
if (flags.to !== undefined) {
|
|
120
|
+
return {
|
|
121
|
+
command: "convert-many",
|
|
122
|
+
inputs: files,
|
|
123
|
+
to: flags.to,
|
|
124
|
+
outDir: flags.outDir,
|
|
125
|
+
read,
|
|
126
|
+
write,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (flags.outDir !== undefined) {
|
|
130
|
+
throw new Error("Com --out-dir, diga também o formato da saída com --to (tipo --to csv).");
|
|
131
|
+
}
|
|
132
|
+
if (files.length === 1) {
|
|
133
|
+
throw new Error('Faltou dizer a saída. Use "datera convert entrada.csv saida.xlsx" ou "datera convert entrada.csv --to xlsx".');
|
|
134
|
+
}
|
|
135
|
+
if (files.length > 2) {
|
|
136
|
+
throw new Error("Pra converter vários arquivos, use --to com o formato (e --out-dir se quiser outra pasta).");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
command: "convert",
|
|
140
|
+
input: files[0],
|
|
141
|
+
output: files[1],
|
|
142
|
+
read,
|
|
143
|
+
write,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
92
146
|
function buildProgram(onCommand) {
|
|
93
147
|
const program = new commander_1.Command();
|
|
94
148
|
program
|
|
@@ -111,6 +165,8 @@ function buildProgram(onCommand) {
|
|
|
111
165
|
.option("--dry-run", "Roda tudo e mostra o resultado, sem gravar nada.")
|
|
112
166
|
.option("--input <arquivo>", "Lê desse arquivo em vez da fonte da config.")
|
|
113
167
|
.option("--output <arquivo>", "Grava nesse arquivo em vez do destino da config.")
|
|
168
|
+
.option("--out-dir <pasta>", 'Com vários arquivos no --input (tipo "matriculas/*.xlsx"), grava um resultado pra cada um nessa pasta.')
|
|
169
|
+
.option("--to <formato>", "Formato de cada resultado quando são vários arquivos (csv, xlsx, json, xml, parquet ou db).")
|
|
114
170
|
.action((config, flags) => onCommand({
|
|
115
171
|
command: "run",
|
|
116
172
|
config: config ?? flags.config ?? DEFAULT_CONFIG,
|
|
@@ -118,6 +174,8 @@ function buildProgram(onCommand) {
|
|
|
118
174
|
dryRun: flags.dryRun ?? false,
|
|
119
175
|
input: flags.input,
|
|
120
176
|
output: flags.output,
|
|
177
|
+
outDir: flags.outDir,
|
|
178
|
+
to: flags.to,
|
|
121
179
|
}));
|
|
122
180
|
program
|
|
123
181
|
.command("validate")
|
|
@@ -141,15 +199,17 @@ function buildProgram(onCommand) {
|
|
|
141
199
|
}));
|
|
142
200
|
withWriteFlags(withReadFlags(program
|
|
143
201
|
.command("convert")
|
|
144
|
-
.description(
|
|
145
|
-
.argument("<
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
202
|
+
.description('Converte de um formato pra outro, sem mexer nos dados. Com --to, converte vários de uma vez (tipo "matriculas/*.xlsx").')
|
|
203
|
+
.argument("<arquivos...>", "A entrada e a saída, ou só as entradas quando usar --to. Aceita pasta e * no nome do arquivo.")
|
|
204
|
+
.option("--to <formato>", "Formato da saída de cada arquivo (csv, xlsx, json, xml, parquet ou db).")
|
|
205
|
+
.option("--out-dir <pasta>", "Pasta onde gravar os convertidos. Sem ela, ficam do lado de cada entrada."))).action((files, flags, command) => {
|
|
206
|
+
try {
|
|
207
|
+
onCommand(convertRequest(files, flags));
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
command.error(`erro: ${error instanceof Error ? error.message : String(error)}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
153
213
|
withWriteFlags(withReadFlags(program
|
|
154
214
|
.command("dedupe")
|
|
155
215
|
.description("Tira as linhas repetidas olhando uma coluna.")
|