import * as fs from "fs"; import * as _ from "lodash"; import * as Papa from "papaparse"; import * as sequelize from "sequelize"; import * as moment from "moment"; import * as convert from "./convert"; import * as types from "./types"; import * as sql from "./sql"; import * as utils from "./utils"; import * as data from "./data"; export class Table { private _headers: string[] = []; private _types: types.ValueType[] = []; private _rows: any[][] = []; static sql(path: string) {} setStructure(headers, types) { this._headers = _.clone(headers); this._types = _.clone(types); } get structure() { return _.zipObject(this.headers, this.types); } get headers() { return _.clone(this._headers); } get types() { return _.clone(this._types); } row(index: number) { return this._rows[index]; } rows() { return _.clone(this._rows); } data(): types.DataRowType[] { return this._rows.map(row => _.zipObject(this._headers, row)); } column(column: number | string) { return this._rows.map(row => row[this.columnIndex(column)]); } private async _sql(url: string, tableName: string, indexes = []) { return sql.createTable( url, _.zipObject(this.headers, this.types), this.rows(), indexes, true, tableName ); } async query(q: string, replace = true) { const tableName = "data"; const { sequelize } = await this._sql("sqlite://:memory:", tableName); const regex = /from\s+([a-zA-Z0-9]+)/g; if (replace) { q = q.replace(regex, `from ${tableName}`); } const result = await sequelize.query(q); return new Promise(resolve => resolve(Table.import.data(result[0]))); } // save(path: string) {} // csv(path: string) {} add(rows: any[][]) { rows.forEach((row, rowIndex) => { try { this._rows.push(data.getConvertedValues(this.types, row)); } catch (error) { console.error( `Failed to add row at index ${rowIndex} or line ${rowIndex + 1}` ); throw error; } }); } merge(table: Table): Table { const merged = new Table(); let headers = table.headers; let types = table.types; if (headers.length == 0) { headers = this.headers; types = this.types; } if (table.headers.length > 0 && this.headers.length > 0) { if (this.headers.join("-") !== table.headers.join("-")) { console.error(this.headers); console.error(table.headers); throw Error( `Table.merge: headers are not equal: ${_.difference( this.headers, table.headers )}` ); } } merged.setStructure(headers, types); merged.add(this.rows()); merged.add(table.rows()); return merged; } map(f: (row: types.DataRowType) => types.DataRowType): Table { return Table.import.data(this.data().map(f)); } /** * Returns the index for a column name. Throws and error if it does not exist. * @param name name of the column */ columnIndex(name: string | number) { if (_.isNumber(name) && this.headers[name]) { return name; } const index = this._headers.indexOf(name as string); if (index === -1) { throw Error(`Table.columnIndex: no column named ${name}`); } return index; } columnType(name: string | number) { return this._types[this.columnIndex(name)]; } columnConverter(name: string | number): (any) => any { const columnType = this.columnType(name); const columnConverter = convert.Types[columnType]; if (!columnConverter) { throw Error( `Table.columnConverter: no converter for column ${name} with type ${columnType}` ); } return columnConverter; } convertRow(row: any[]) { return row.map((value, column) => this.columnConverter(column)(value)); } static import = { /** * Import from data objects like {name: "Koen", age: 35} */ data: (rows: types.DataRowType[]) => { return Table.import.rows( data.getDataRows(rows), data.getDataHeaders(rows) ); }, /** * Imports from arrays, optionally guesses the columns and types. */ rows: (rows: types.RawRowType[], headers?: string[]): Table => { if (!rows.length) { return new Table(); } if (!headers) { const guessedHeaders = data.getRowHeaders(rows); if (guessedHeaders) { rows = _.slice(rows, 1); } headers = guessedHeaders; } headers = headers.map(utils.sanitizeName); const types = data.getRowTypes(rows); const table = new Table(); table.setStructure(headers, types); table.add(rows); return table; }, /** * Imports from csv file, optionally guesses the columns and types. */ csv: async (path: string, structure?: types.StructureType) => { const data = await utils.getRowsFromFile(path); const table = Table.import.rows(data); return new Promise
(resolve => resolve(table)); }, /** * Imports from sqlite file. */ sql: (path: string, tableName?: string) => {} }; export = { csv: (path: string) => { const convert = value => { if (_.isDate(value)) { return moment.utc(value).format("YYYY/MM/DD"); } return value; }; const data = this.data().map(row => { return _.mapValues(row, convert); }); fs.writeFileSync(path, Papa.unparse(data)); }, sqlite: async (path: string, tableName?: string) => { await this._sql(`sqlite://${path}`, tableName); }, sql: async (url: string, tableName?: string) => { await this._sql(url, tableName); } }; }