diff --git a/README.md b/README.md index cd34dbf..27533d8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,12 @@ Automatically creates typed PocketBase collection and populates it with data. Columns conflicting with PocketBase's autogenerated system fields (`id`, `created`, `updated`; case-insensitive check, target column name's case is not -affected) are prefixed with `_`. +affected) are prefixed with `_`. Collection conflict will cause the import to +fail without any changes to the database. + +No rules, options or constraints are set for the new collection (see the import +log for a full structure). You can modify them after the import from +PocketBase's dashboard. ## Types @@ -22,6 +27,7 @@ PocketBase types are: - `JSON` # Configuration + Install the latest [Deno runtime](https://deno.land/) to run the scripts. In the root directory create `.env` file with the following environment @@ -33,6 +39,9 @@ variables: Place your import files inside of `input` directory. +Make sure the target PocketBase instance is running and pointed to by +`POCKETBASE_URL`. + # Options You can change the default import options to your needs: @@ -42,11 +51,16 @@ You can change the default import options to your needs: | input | CSV/JSON | Yes | The name of the input file (with extension) | --input=example.csv | | id | CSV/JSON | No | Indicates that `_id` column should be typed as plain text, the type is detected by default | --id | | lf | CSV | No | LF (`\n`) EOL character will be used instead of default CLRF (`\r\n`) | --lf | -| delimiter | CSV | No | Column value separator, defaults to `,` | --delimiter=";" | -| quote | CSV | No | Value quote character, defaults to `'` | --quote='~" | +| delimiter | CSV | No | Column value separator, defaults to `,` | --delimiter=";" | +| quote | CSV | No | Value quote character, defaults to `'` | --quote="~" | # CSV +The import is **not** multiline-safe, so if you have a file with strings +spanning across multiple lines the best option for you is to convert the input +file to JSON with tools like +[DB Browser for SQLite](https://sqlitebrowser.org/). + ## Examples Basic import (root directory): @@ -66,3 +80,21 @@ Import with custom parser options (you need to adjust `example.csv`): ``` deno run csv.ts --input=example.csv --delimiter=";" --quote="~" --lf ``` + +# JSON + +The required data format is an array of row objects. + +## Examples + +Basic import (root directory): + +``` +deno run json.ts --input=example.json +``` + +Import without permission prompts and with `_id` column as text: + +``` +deno run --allow-read --allow-env --allow-net json.ts --input=example.json --id +``` diff --git a/csv.ts b/csv.ts index 3cc4c9a..3238bfb 100644 --- a/csv.ts +++ b/csv.ts @@ -5,12 +5,11 @@ import PocketBase, { } from "https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.mjs"; import "https://deno.land/std@0.178.0/dotenv/load.ts"; import { parse } from "https://deno.land/std@0.175.0/flags/mod.ts"; -import { parseCsv } from "./utils/csv.ts"; -import { createSchema, parseData } from "./utils/pocketbase.ts"; +import { parseData, readCsv } from "./utils/csv.ts"; +import { createSchema } from "./utils/pocketbase.ts"; /** * Structures and populates a new collection from a CSV file. - * @returns */ async function importCsv() { // config data @@ -48,27 +47,11 @@ async function importCsv() { if (options.input === null) { console.error("%cOptionError: CSV file name not supplied", "color: red"); - return; + Deno.exit(-1); } - // parser options - const csvOptions = { - columnSeparator: options.delimiter, - lineSeparator: options.lf ? "\n" : "\r\n", - quote: options.quote, - }; - - // parses CSV - const data = await parseCsv(options.input, csvOptions); - - // empty file - if (data === null) { - console.error( - `%c[Import] No data to import from ${options.input}`, - "color: red", - ); - return; - } + // read the file + const data = await readCsv(options.input, options); // sanitize the file name for collection name const collectName = options.input.replace(".csv", ""); @@ -80,7 +63,7 @@ async function importCsv() { const _authResponse = await pb.admins.authWithPassword(adminName, adminPass); // collection schema object - const schema: SchemaField[] = createSchema(data, options.id); + const schema: SchemaField[] = createSchema(data, options.id, "csv"); const creationDate = new Date().toISOString(); @@ -115,6 +98,8 @@ async function importCsv() { // rows to be sent via PocketBase API const rows = parseData(data, schema); + console.log(`[Import] Importing ${rows.length} rows...`); + // number of successfully inserted rows let insertCount = 0; @@ -130,9 +115,9 @@ async function importCsv() { } } - const color = insertCount === rows.length ? "green" : "orange"; + const color = insertCount === data.length ? "green" : "orange"; console.log( - `%c[Import] Imported rows: ${insertCount}/${rows.length}`, + `%c[Import] Imported rows: ${insertCount}/${data.length}`, `color: ${color}`, ); } diff --git a/input/example.json b/input/example.json new file mode 100644 index 0000000..eb0abf8 --- /dev/null +++ b/input/example.json @@ -0,0 +1,22 @@ +[ + { + "id": 1, + "name": "john", + "is_good": true, + "score": 0.8412384213497, + "email": "john.doe@example.com", + "json": [], + "date": "2023-03-05T00:35:21.104Z" + }, + { + "id": 2, + "name": "fire", + "is_good": false, + "score": -80347329472, + "email": "firebase@google.com", + "json": { + "xd": "nice meme" + }, + "date": null + } +] diff --git a/json.ts b/json.ts new file mode 100644 index 0000000..ffd677f --- /dev/null +++ b/json.ts @@ -0,0 +1,113 @@ +// @deno-types="https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.d.mts" +import PocketBase, { + Collection, + SchemaField, +} from "https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.mjs"; +import "https://deno.land/std@0.178.0/dotenv/load.ts"; +import { parse } from "https://deno.land/std@0.175.0/flags/mod.ts"; +import { readJson, resolveConflicts } from "./utils/json.ts"; +import { createSchema } from "./utils/pocketbase.ts"; + +/** + * Structures and populates a new collection from a JSON file. + */ +async function importJson() { + // config data + const pbUrl = Deno.env.get("POCKETBASE_URL") ?? "http://localhost:8090"; + const adminName = Deno.env.get("ADMIN_EMAIL") ?? ""; + const adminPass = Deno.env.get("ADMIN_PASSWORD") ?? ""; + + // parse CLI args + const options = parse(Deno.args, { + string: ["input"], + boolean: ["id"], + default: { + /** + * Name of the JSON file to import (with extension). + */ + input: null, + /** + * Flag to always set `_id` column type to Plain text (detected by default). + */ + id: false, + }, + }); + + if (options.input === null) { + console.error("%cOptionError: JSON file name not supplied", "color: red"); + Deno.exit(-1); + } + + // read the file + const data = await readJson(options.input); + + // sanitize the file name for collection name + const collectName = options.input.replace(".json", ""); + + // connect to pocketbase + const pb = new PocketBase(pbUrl); + + // authenticate as super admin + const _authResponse = await pb.admins.authWithPassword(adminName, adminPass); + + // collection schema object + const schema: SchemaField[] = createSchema(data, options.id, "json"); + + const creationDate = new Date().toISOString(); + + // the new collection + const collection = new Collection({ + name: collectName, + type: "base", + system: false, + schema, + listRule: null, + viewRule: null, + createRule: null, + updateRule: null, + deleteRule: null, + options: {}, + created: creationDate, + updated: creationDate, + }); + + // show the submitted collection + console.log(collection); + + // create the new collection + // import will fail if a collection with the same name exists + await pb.collections.import([collection]); + + console.log( + `%c[Import] Collection '${collectName}' created!`, + "color: green", + ); + + // prefix conflicting column names + const rows = resolveConflicts(data); + + console.log(`[Import] Importing ${rows.length} rows...`); + + // number of successfully inserted rows + let insertCount = 0; + + for (insertCount; insertCount < rows.length; insertCount++) { + try { + await pb.collection(collectName).create(rows[insertCount], { + "$autoCancel": false, + }); + } catch (e) { + // breaks on first error + console.error(e); + break; + } + } + + const color = insertCount === data.length ? "green" : "orange"; + console.log( + `%c[Import] Imported rows: ${insertCount}/${data.length}`, + `color: ${color}`, + ); +} + +importJson(); diff --git a/types/csv.ts b/types/csv.ts index 8bb999c..e0e54f0 100644 --- a/types/csv.ts +++ b/types/csv.ts @@ -6,9 +6,9 @@ import { CommonCSVReaderOptions } from "https://deno.land/x/csv@v0.8.0/reader.ts export type ParserOptions = Partial; /** - * Raw row object with string properties returned by `csv.readCSVObjects`. + * Raw CSV row returned by `csv.readCSVObjects`. */ -export type RawRow = { +export type RawCsvRow = { [key: string]: string; }; @@ -19,3 +19,9 @@ export type ParsedRow = { // deno-lint-ignore no-explicit-any [key: string]: any; }; + +export type CsvOptions = { + delimiter: string; + lf: boolean; + quote: string; +}; diff --git a/types/json.ts b/types/json.ts new file mode 100644 index 0000000..bb75e40 --- /dev/null +++ b/types/json.ts @@ -0,0 +1,7 @@ +/** + * Raw JSON object returned by `JSON.parse`. + */ +export type RawJsonRow = { + // deno-lint-ignore no-explicit-any + [key: string]: any; +}; diff --git a/utils/csv.ts b/utils/csv.ts index 362ccff..d0067aa 100644 --- a/utils/csv.ts +++ b/utils/csv.ts @@ -1,5 +1,51 @@ -import { readCSVObjects } from "https://deno.land/x/csv@v0.8.0/reader.ts"; -import { ParserOptions, RawRow } from "../types/csv.ts"; +import { + CommonCSVReaderOptions, + readCSVObjects, +} from "https://deno.land/x/csv@v0.8.0/reader.ts"; +import { + CsvOptions, + ParsedRow, + ParserOptions, + RawCsvRow, +} from "../types/csv.ts"; +// @deno-types="https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.d.mts" +import { SchemaField } from "https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.mjs"; +import { + POCKETBASE_SYSFIELD, + POCKETBASE_TYPE, + PocketbaseRowSchema, + PocketbaseType, +} from "../types/pocketbase.ts"; +import { createSchemaField, generateRowSchema } from "./pocketbase.ts"; +import { isBool, isDate, isEmail, isJson, isNumber } from "./regex.ts"; + +/** + * Reads raw data from a CSV file. + * @param filename + * @param options + * @returns + */ +export async function readCsv(filename: string, options: CsvOptions) { + // parser options + const csvOptions = { + columnSeparator: options.delimiter, + lineSeparator: options.lf ? "\n" : "\r\n", + quote: options.quote, + } satisfies Partial; + + // parses CSV + const data = await parseCsv(filename, csvOptions); + + if (data === null) { + console.error( + `%c[Import] No data to import from ${filename}`, + "color: red", + ); + Deno.exit(-2); + } + + return data; +} /** * Parse a file to string-based object array. @@ -7,31 +53,31 @@ import { ParserOptions, RawRow } from "../types/csv.ts"; * @param csvOptions - Options for the parser * @returns */ -export async function parseCsv( +async function parseCsv( filename: string | null, csvOptions: ParserOptions, -): Promise { - const results: RawRow[] = []; +): Promise { + const data: RawCsvRow[] = []; try { const f = await Deno.open(`./input/${filename}`); for await (const obj of readCSVObjects(f, csvOptions)) { - results.push(obj); + data.push(obj); } f.close(); } catch (e) { console.error(`%c${e}`, "color: red"); - return null; + Deno.exit(-3); } // No columns - if (results.length === 0) { + if (data.length === 0) { return null; } - return results; + return data; } /** @@ -42,3 +88,129 @@ export async function parseCsv( export function parseBool(value: string): boolean { return ["true", "1"].includes(value); } + +/** + * Matches column data against regular expressions to deduct the PocketBase type and returns a column definition. + * @param data - Raw parser output + * @param prop - Column name + * @returns `SchemaField` + */ +export function addSchemaField(data: RawCsvRow[], prop: string): SchemaField { + // The new column is prefixed with underscore if it conflicts with a system field + const targetProp = POCKETBASE_SYSFIELD.includes(prop.toLowerCase()) + ? `_${prop}` + : prop; + + // Precedence is important, more restricted types are matched on first + if (isBool(data, prop)) { + return createSchemaField(targetProp, "bool"); + } + + if (isNumber(data, prop)) { + return createSchemaField(targetProp, "number"); + } + + if (isEmail(data, prop)) { + return createSchemaField(targetProp, "email"); + } + + if (isJson(data, prop)) { + return createSchemaField(targetProp, "json"); + } + + if (isDate(data, prop)) { + return createSchemaField(targetProp, "date"); + } + + // Plain text is the default type + return createSchemaField(targetProp, "text"); +} + +/** + * Parses typed rows using Pocketbase collection schema. + * @param data - Raw CSV parser output + * @param schema - PocketBase collection schema + * @returns + */ +export function parseData( + data: RawCsvRow[], + schema: SchemaField[], +): ParsedRow[] { + const rows: ParsedRow[] = []; + + // create a row schema for the collection + const rowSchema = generateRowSchema(schema); + console.log("RowSchema", rowSchema); + + data.forEach((rawRow) => { + rows.push(parseRow(rawRow, rowSchema)); + }); + + return rows; +} + +/** + * Creates a typed row object from raw data using row schema. + * @param rawRow - Raw row data + * @param schema - Row type template + * @returns + */ +function parseRow(rawRow: RawCsvRow, schema: PocketbaseRowSchema): ParsedRow { + let parsedRow: ParsedRow = {}; + const keys = Object.keys(rawRow); + + keys.forEach((prop) => { + // Handle conflicts with system names - add underscore + const orgProp = prop; + + if (POCKETBASE_SYSFIELD.includes(prop.toLowerCase())) { + prop = `_${prop}`; + } + + const type = schema[prop]; + const value = parseValue(rawRow[orgProp], type); + parsedRow = { ...parsedRow, [prop]: value }; + }); + + return parsedRow; +} + +/** + * Parses a string to a correspending PocketBase type. + * @param value + * @param type + * @returns + */ +// deno-lint-ignore no-explicit-any +function parseValue(value: string, type: PocketbaseType): any { + switch (type) { + case POCKETBASE_TYPE.BOOL: + if (value == "") { + return null; + } + return parseBool(value); + case POCKETBASE_TYPE.NUMBER: + if (value == "") { + return null; + } + return parseFloat(value); + case POCKETBASE_TYPE.JSON: + if (value == "") { + return null; + } + // this is safe as the values were try-parsed earlier for schema definition + return JSON.parse(value); + case POCKETBASE_TYPE.PLAIN_TEXT: + return value !== "" ? value : null; + case POCKETBASE_TYPE.EMAIL: + return value !== "" ? value : null; + case POCKETBASE_TYPE.DATETIME: + return value !== "" ? value : null; + default: + console.error( + `%cPbTypeError: value parser for type '${type}' is not yet implemented.`, + "color: red", + ); + Deno.exit(-4); + } +} diff --git a/utils/json.ts b/utils/json.ts new file mode 100644 index 0000000..d46c1d5 --- /dev/null +++ b/utils/json.ts @@ -0,0 +1,129 @@ +// @deno-types="https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.d.mts" +import { SchemaField } from "https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.mjs"; +import { RawJsonRow } from "../types/json.ts"; +import { POCKETBASE_SYSFIELD } from "../types/pocketbase.ts"; +import { createSchemaField } from "./pocketbase.ts"; +import { isDate, isEmail } from "./regex.ts"; + +/** + * Reads an array of rows from a JSON file. + * @param filename The extension-inclusive name of input file. + * @returns + */ +export async function readJson(filename: string) { + const json = await parseJson(filename); + + if (json === null) { + console.error(`%cFileError: Could not read ${filename}`, "color: red"); + Deno.exit(-3); + } + + if (!Array.isArray(json)) { + console.error(`%cFileError: ${filename} is not an array`, "color: red"); + Deno.exit(-4); + } + + if (json.length === 0) { + console.error(`%cFileError: No data in ${filename}`, "color: red"); + Deno.exit(-5); + } + + const arrayKeys = json.keys(); + + const rows: RawJsonRow[] = []; + + for (const key of arrayKeys) { + rows.push(json[key] as RawJsonRow); + } + + return rows; +} + +/** + * Parses a JSON file. + * @param filename Name of the .json file (with extension) + * @returns + */ +async function parseJson(filename: string) { + try { + return JSON.parse(await Deno.readTextFile(`./input/${filename}`)); + } catch (e) { + console.error(`%c${e}`, "color: red"); + Deno.exit(-2); + } +} + +/** + * Matches column data against regular expressions to deduct the PocketBase type and returns a column definition. + * @param data Raw input data. + * @param prop Column name. + * @returns `SchemaField` + */ +export function addSchemaField(data: RawJsonRow[], prop: string): SchemaField { + // The new column is prefixed with underscore if it conflicts with a system field + const targetProp = POCKETBASE_SYSFIELD.includes(prop.toLowerCase()) + ? `_${prop}` + : prop; + + let value = data[0][prop]; + + // if necessary find a value + if (value === null) { + for (let i = 0; i < data.length; i++) { + if (data[i][prop] != null) { + value = data[i][prop]; + } + break; + } + } + + // all values are null + if (value == null) { + return createSchemaField(targetProp, "text"); + } + + switch (typeof value) { + case "boolean": + return createSchemaField(targetProp, "bool"); + case "number": + case "bigint": + return createSchemaField(targetProp, "number"); + case "string": + if (isEmail(data, targetProp)) { + return createSchemaField(targetProp, "email"); + } + if (isDate(data, targetProp)) { + return createSchemaField(targetProp, "date"); + } + return createSchemaField(targetProp, "text"); + case "object": + return createSchemaField(targetProp, "json"); + default: + return createSchemaField(targetProp, "text"); + } +} + +/** + * Renames properties conflicting with system column names. + * @param data Data rows. + * @returns + */ +export function resolveConflicts(data: RawJsonRow[]): RawJsonRow[] { + const rows: RawJsonRow[] = []; + + for (const r of data) { + const row = r; + const keys = Object.keys(r); + for (const key of keys) { + if (POCKETBASE_SYSFIELD.includes(key.toLowerCase())) { + const value = r[key]; + delete row[key]; + const newKey = `_${key}`; + row[newKey] = value; + } + } + rows.push(row); + } + + return rows; +} diff --git a/utils/pocketbase.ts b/utils/pocketbase.ts index 2112507..661a48e 100644 --- a/utils/pocketbase.ts +++ b/utils/pocketbase.ts @@ -1,56 +1,19 @@ // @deno-types="https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.d.mts" import { SchemaField } from "https://unpkg.com/pocketbase@0.12.0/dist/pocketbase.es.mjs"; -import { ParsedRow, RawRow } from "../types/csv.ts"; +import { RawCsvRow } from "../types/csv.ts"; +import { RawJsonRow } from "../types/json.ts"; import { - POCKETBASE_SYSFIELD, POCKETBASE_TYPE, PocketbaseRowSchema, PocketbaseType, } from "../types/pocketbase.ts"; -import { parseBool } from "./csv.ts"; -import { isBool, isDate, isEmail, isJson, isNumber } from "./regex.ts"; - -/** - * Matches column data against regular expressions to deduct the PocketBase type and returns a column definition. - * @param data - Raw parser output - * @param prop - Column name - * @returns `SchemaField` - */ -export function addSchemaField(data: RawRow[], prop: string): SchemaField { - // The new column is prefixed with underscore if it conflicts with a system field - const targetProp = POCKETBASE_SYSFIELD.includes(prop.toLowerCase()) - ? `_${prop}` - : prop; - - // Precedence is important, more restricted types are matched on first - if (isBool(data, prop)) { - return createSchemaField(targetProp, "bool"); - } - - if (isNumber(data, prop)) { - return createSchemaField(targetProp, "number"); - } - - if (isEmail(data, prop)) { - return createSchemaField(targetProp, "email"); - } - - if (isJson(data, prop)) { - return createSchemaField(targetProp, "json"); - } - - if (isDate(data, prop)) { - return createSchemaField(targetProp, "date"); - } - - // Plain text is the default type - return createSchemaField(targetProp, "text"); -} +import { addSchemaField as addCsvSchemaField } from "./csv.ts"; +import { addSchemaField as addJsonSchemaField } from "./json.ts"; /** * Finds column's type in the schema. - * @param column - Column name - * @param schema - PocketBase collection schema + * @param column Column name. + * @param schema PocketBase collection schema. * @returns */ export function getSchemaType( @@ -98,11 +61,14 @@ export function getSchemaType( /** * Builds a `SchemaField` object based on data type. - * @param name - Column name - * @param type - PocketBase type + * @param name Column name. + * @param type PocketBase type. * @returns */ -function createSchemaField(name: string, type: PocketbaseType): SchemaField { +export function createSchemaField( + name: string, + type: PocketbaseType, +): SchemaField { switch (type) { case POCKETBASE_TYPE.BOOL: return new SchemaField({ @@ -176,10 +142,10 @@ function createSchemaField(name: string, type: PocketbaseType): SchemaField { /** * Creates a row object schema from PocketBase collection schema. - * @param schema - PocketBase collection schema + * @param schema PocketBase collection schema. * @returns */ -export function generateRowSchema(schema: SchemaField[]) { +export function generateRowSchema(schema: SchemaField[]): PocketbaseRowSchema { let instance: PocketbaseRowSchema = {}; let fieldType: PocketbaseType; @@ -193,12 +159,14 @@ export function generateRowSchema(schema: SchemaField[]) { /** * Parses raw objects into PocketBase collection schema fields. - * @param data - Raw parser output + * @param data Raw input data. * @returns */ +// deno-lint-ignore no-explicit-any export function createSchema( - data: RawRow[], + data: { [key: string]: any }, stringifyId: boolean, + inputFormat: "csv" | "json", ): SchemaField[] { const schema: SchemaField[] = []; @@ -213,95 +181,13 @@ export function createSchema( if (stringifyId && prop.toLowerCase() === "id") { schema.push(createSchemaField(`_${prop}`, "text")); } else { - schema.push(addSchemaField(data, prop)); + schema.push( + inputFormat === "csv" + ? addCsvSchemaField(data as RawCsvRow[], prop) + : addJsonSchemaField(data as RawJsonRow[], prop), + ); } } return schema; } - -/** - * Parses typed rows using Pocketbase collection schema. - * @param data - Raw CSV parser output - * @param schema - PocketBase collection schema - * @returns - */ -export function parseData(data: RawRow[], schema: SchemaField[]): ParsedRow[] { - const rows: ParsedRow[] = []; - - // create a row schema for the collection - const rowSchema = generateRowSchema(schema); - console.log("RowSchema", rowSchema); - - data.forEach((rawRow) => { - rows.push(parseRow(rawRow, rowSchema)); - }); - - return rows; -} - -/** - * Creates a typed row object from raw data using row schema. - * @param rawRow - Raw row data - * @param schema - Row type template - * @returns - */ -function parseRow(rawRow: RawRow, schema: PocketbaseRowSchema): ParsedRow { - let parsedRow: ParsedRow = {}; - const keys = Object.keys(rawRow); - - keys.forEach((prop) => { - // Handle conflicts with system names - add underscore - const orgProp = prop; - - if (POCKETBASE_SYSFIELD.includes(prop.toLowerCase())) { - prop = `_${prop}`; - } - - const type = schema[prop]; - const value = parseValue(rawRow[orgProp], type); - parsedRow = { ...parsedRow, [prop]: value }; - }); - - return parsedRow; -} - -/** - * Parses a string to a value compliant with correspending PocketBase type. - * @param value - * @param type - * @returns - */ -// deno-lint-ignore no-explicit-any -function parseValue(value: string, type: PocketbaseType): any { - switch (type) { - case POCKETBASE_TYPE.BOOL: - if (value == "") { - return null; - } - return parseBool(value); - case POCKETBASE_TYPE.NUMBER: - if (value == "") { - return null; - } - return parseFloat(value); - case POCKETBASE_TYPE.JSON: - if (value == "") { - return null; - } - // this is safe as the values were try-parsed earlier for schema definition - return JSON.parse(value); - case POCKETBASE_TYPE.PLAIN_TEXT: - return value !== "" ? value : null; - case POCKETBASE_TYPE.EMAIL: - return value !== "" ? value : null; - case POCKETBASE_TYPE.DATETIME: - return value !== "" ? value : null; - default: - console.error( - `%cPbTypeError: value parser for type '${type}' is not yet implemented.`, - "color: red", - ); - Deno.exit(-3); - } -} diff --git a/utils/regex.ts b/utils/regex.ts index ce5f417..5b71124 100644 --- a/utils/regex.ts +++ b/utils/regex.ts @@ -1,12 +1,12 @@ -import { RawRow } from "../types/csv.ts"; +import { RawCsvRow } from "../types/csv.ts"; /** * Checks if the column type could be `Bool`. - * @param data - Sample data - * @param prop - Validated property + * @param data Sample data. + * @param prop Validated property. * @returns */ -export function isBool(data: RawRow[], prop: string): boolean { +export function isBool(data: RawCsvRow[], prop: string): boolean { const zeroOrOne = /^(0|1)$/; const trueOrFalse = /^(true|false)$/; @@ -36,11 +36,11 @@ export function isBool(data: RawRow[], prop: string): boolean { /** * Checks if the column type could be `Number` (integer or floating point). - * @param data - Sample data - * @param prop - Validated property + * @param data Sample data. + * @param prop Validated property. * @returns */ -export function isNumber(data: RawRow[], prop: string): boolean { +export function isNumber(data: RawCsvRow[], prop: string): boolean { const integer = /^-?[0-9]+$/; const float = /^-?[0-9]+\.[0-9]*$/; @@ -66,11 +66,14 @@ export function isNumber(data: RawRow[], prop: string): boolean { /** * Checks if the column type could be `Email`. - * @param data - Sample data - * @param prop - Validated property + * @param data Sample data. + * @param prop Validated property. * @returns */ -export function isEmail(data: RawRow[], prop: string): boolean { +export function isEmail( + data: { [key: string]: string }[], + prop: string, +): boolean { const pattern = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; let values = 0; @@ -78,7 +81,9 @@ export function isEmail(data: RawRow[], prop: string): boolean { data.forEach((obj) => { // could be nullable - if (obj[prop] !== "") { + // - empty strings for CSV + // - null values for JSON + if (obj[prop] !== "" && obj[prop] !== null) { values++; if (obj[prop].match(pattern) !== null) { matched++; @@ -92,11 +97,11 @@ export function isEmail(data: RawRow[], prop: string): boolean { /** * Parses the column values as JSON. - * @param data - Sample data - * @param prop - Validated property + * @param data Sample data. + * @param prop Validated property. * @returns */ -export function isJson(data: RawRow[], prop: string): boolean { +export function isJson(data: RawCsvRow[], prop: string): boolean { let values = 0; let parsed = 0; @@ -119,17 +124,22 @@ export function isJson(data: RawRow[], prop: string): boolean { /** * Parses the column values using `Date.parse()`. - * @param data - Sample data - * @param prop - Validated property + * @param data Sample data. + * @param prop Validated property. * @returns */ -export function isDate(data: RawRow[], prop: string): boolean { +export function isDate( + data: { [key: string]: string }[], + prop: string, +): boolean { let values = 0; let parsed = 0; data.forEach((obj) => { // could be nullable - if (obj[prop] !== "") { + // - empty strings for CSV + // - null values for JSON + if (obj[prop] !== "" && obj[prop] !== null) { values++; const timestamp = Date.parse(obj[prop]); if (!isNaN(timestamp)) {