From 5e8607653065f3bb65e718144588e50307f1f6e7 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Fri, 2 Oct 2020 16:16:06 +0100 Subject: [PATCH] CSV Import end to end --- .../nav/ModelNavigator/CreateTable.svelte | 52 ++--- .../nav/ModelNavigator/TableDataImport.svelte | 189 ++++++++++++++++++ packages/server/build/Hosting Portal-2.png | Bin 0 -> 53286 bytes packages/server/src/api/controllers/model.js | 26 ++- packages/server/src/api/controllers/static.js | 35 ---- packages/server/src/api/routes/model.js | 5 + packages/server/src/api/routes/static.js | 1 - packages/server/src/utilities/csvParser.js | 76 +++++++ .../src/utilities/tests/csvParser.spec.js | 0 9 files changed, 301 insertions(+), 83 deletions(-) create mode 100644 packages/builder/src/components/nav/ModelNavigator/TableDataImport.svelte create mode 100644 packages/server/build/Hosting Portal-2.png create mode 100644 packages/server/src/utilities/csvParser.js create mode 100644 packages/server/src/utilities/tests/csvParser.spec.js diff --git a/packages/builder/src/components/nav/ModelNavigator/CreateTable.svelte b/packages/builder/src/components/nav/ModelNavigator/CreateTable.svelte index 705da1365c..3ab5a10fe3 100644 --- a/packages/builder/src/components/nav/ModelNavigator/CreateTable.svelte +++ b/packages/builder/src/components/nav/ModelNavigator/CreateTable.svelte @@ -3,6 +3,7 @@ import { backendUiStore } from "builderStore" import { notifier } from "builderStore/store/notifications" import { + Body, DropdownMenu, Button, Heading, @@ -11,19 +12,20 @@ Select, Dropzone, } from "@budibase/bbui" + import TableDataImport from "./TableDataImport.svelte" import api from "builderStore/api" import analytics from "analytics" let anchor let dropdown let name - let recordImport = {} + let dataImport async function saveTable() { const model = await backendUiStore.actions.models.save({ name, - schema: recordImport.schema || {}, - recordImport, + schema: dataImport.schema || {}, + dataImport, }) notifier.success(`Table ${name} created successfully.`) $goto(`./model/${model._id}`) @@ -36,38 +38,6 @@ name = "" dropdown.hide() } - - function handleFileTooLarge(fileSizeLimit) { - notifier.danger( - `Files cannot exceed ${fileSizeLimit / - BYTES_IN_MB}MB. Please try again with smaller files.` - ) - } - - async function processFiles(fileList) { - const fileArray = Array.from(fileList) - let data = new FormData() - for (var i = 0; i < fileList.length; i++) { - data.append("file", fileList[i]) - } - - const response = await fetch("/api/csv/validate", { - method: "POST", - body: data, - headers: { - Accept: "application/json", - }, - }) - - recordImport = await response.json() - - if (response.status !== 200) { - notifier.danger("CSV Invalid, please try another CSV file") - return [] - } - - return fileArray.map(file => ({ ...file, extension: "csv" })) - }
@@ -76,21 +46,27 @@
Create Table
+ Table Name - Create Table from CSV (Optional) - + Create Table from CSV (Optional) +
- +
diff --git a/packages/builder/src/components/nav/ModelNavigator/TableDataImport.svelte b/packages/builder/src/components/nav/ModelNavigator/TableDataImport.svelte new file mode 100644 index 0000000000..9bdaf7e77d --- /dev/null +++ b/packages/builder/src/components/nav/ModelNavigator/TableDataImport.svelte @@ -0,0 +1,189 @@ + + +
+ + +
+
+ {#if schema} + {#each Object.keys(schema) as columnName} +
+ {columnName} + + + {schema[columnName].success ? 'Success' : 'Failure'} + + omitColumn(columnName)} /> +
+ {/each} + {/if} +
+ + diff --git a/packages/server/build/Hosting Portal-2.png b/packages/server/build/Hosting Portal-2.png new file mode 100644 index 0000000000000000000000000000000000000000..c42db9bd8f8cf0f3447d5826521e07167445f70f GIT binary patch literal 53286 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 literal 0 HcmV?d00001 diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js index 21cf837855..3c3be374c3 100644 --- a/packages/server/src/api/controllers/model.js +++ b/packages/server/src/api/controllers/model.js @@ -1,5 +1,6 @@ const CouchDB = require("../../db") const newid = require("../../db/newid") +const csvParser = require("../../utilities/csvParser") exports.fetch = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) @@ -19,7 +20,7 @@ exports.find = async function(ctx) { exports.save = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) - const { recordImport, ...rest } = ctx.request.body + const { dataImport, ...rest } = ctx.request.body const modelToSave = { type: "model", _id: newid(), @@ -86,15 +87,13 @@ exports.save = async function(ctx) { } await db.put(designDoc) - if (recordImport && recordImport.path) { + if (dataImport && dataImport.path) { // Populate the table with records imported from CSV in a bulk update - const csv = require("csvtojson") - const json = await csv().fromFile(recordImport.path) - const records = json.map(record => ({ - ...record, - modelId: modelToSave._id, - })) - await db.bulkDocs(records) + const data = await csvParser.transform(dataImport) + + for (let row of data) row.modelId = modelToSave._id + + await db.bulkDocs(data) } ctx.status = 200 @@ -135,3 +134,12 @@ exports.destroy = async function(ctx) { ctx.status = 200 ctx.message = `Model ${ctx.params.modelId} deleted.` } + +exports.validateCSVSchema = async function(ctx) { + const { file, schema = {} } = ctx.request.body + const result = await csvParser.parse(file.path, schema) + ctx.body = { + schema: result, + path: file.path, + } +} diff --git a/packages/server/src/api/controllers/static.js b/packages/server/src/api/controllers/static.js index f29b515aaa..663c4c7257 100644 --- a/packages/server/src/api/controllers/static.js +++ b/packages/server/src/api/controllers/static.js @@ -5,7 +5,6 @@ const fetch = require("node-fetch") const fs = require("fs") const uuid = require("uuid") const AWS = require("aws-sdk") -const csv = require("csvtojson") const { prepareUploadForS3 } = require("./deploy/aws") const { @@ -247,37 +246,3 @@ exports.serveComponentLibrary = async function(ctx) { await send(ctx, "/index.js", { root: componentLibraryPath }) } - -function schemaFromCSV(path) { - const result = csv().fromFile(path) - return new Promise((resolve, reject) => { - result.on("header", headers => { - const schema = {} - for (let header of headers) { - schema[header] = { - type: "string", - constraints: { - type: "string", - length: {}, - presence: { - allowEmpty: true, - }, - }, - name: header, - } - } - resolve(schema) - }) - result.on("error", reject) - }) -} - -exports.validateCSV = async function(ctx) { - const file = ctx.request.files.file - const schema = await schemaFromCSV(file.path) - // if (result.length === 0) ctx.throw(400, "CSV Invalid") - ctx.body = { - schema, - path: file.path, - } -} diff --git a/packages/server/src/api/routes/model.js b/packages/server/src/api/routes/model.js index 00eb46d515..fe782d4cf5 100644 --- a/packages/server/src/api/routes/model.js +++ b/packages/server/src/api/routes/model.js @@ -13,6 +13,11 @@ router modelController.find ) .post("/api/models", authorized(BUILDER), modelController.save) + .post( + "/api/models/csv/validate", + authorized(BUILDER), + modelController.validateCSVSchema + ) .delete( "/api/models/:modelId/:revId", authorized(BUILDER), diff --git a/packages/server/src/api/routes/static.js b/packages/server/src/api/routes/static.js index 4026e0205c..aa136a3d15 100644 --- a/packages/server/src/api/routes/static.js +++ b/packages/server/src/api/routes/static.js @@ -28,7 +28,6 @@ router authorized(BUILDER), controller.performLocalFileProcessing ) - .post("/api/csv/validate", authorized(BUILDER), controller.validateCSV) .post("/api/attachments/upload", controller.uploadFile) .get("/componentlibrary", controller.serveComponentLibrary) .get("/assets/:file*", controller.serveAppAsset) diff --git a/packages/server/src/utilities/csvParser.js b/packages/server/src/utilities/csvParser.js new file mode 100644 index 0000000000..1174c2e86f --- /dev/null +++ b/packages/server/src/utilities/csvParser.js @@ -0,0 +1,76 @@ +const csv = require("csvtojson") + +const VALIDATORS = { + string: () => true, + number: attribute => !isNaN(Number(attribute)), + datetime: attribute => !isNaN(new Date(attribute).getTime()), +} + +const PARSERS = { + string: attribute => attribute.toString(), + number: attribute => Number(attribute), + datetime: attribute => new Date(attribute).toISOString(), +} + +function parse(path, parsers) { + const result = csv().fromFile(path) + + const schema = {} + + return new Promise((resolve, reject) => { + result.on("header", headers => { + for (let header of headers) { + schema[header] = { + type: parsers[header] ? parsers[header].type : "string", + success: true, + } + } + }) + result.fromFile(path).subscribe(row => { + // For each CSV row + // parse all the columns that need parsed + for (let key in parsers) { + // if the schema has already borked for a parser, skip this column + if (!schema[key] || !schema[key].success) continue + + // get the validator + const validator = VALIDATORS[parsers[key].type] + + try { + schema[key].success = !!validator(row[key]) + } catch (err) { + schema[key].success = false + } + } + }) + result.on("done", error => { + if (error) { + console.error(error) + reject(error) + } + + resolve(schema) + }) + }) +} + +// TODO: significant refactor +async function transform({ schema, path }) { + const colParser = {} + + for (let key in schema) { + colParser[key] = PARSERS[schema[key].type] + } + + try { + const json = await csv({ colParser }).fromFile(path) + return json + } catch (err) { + console.error(`Error transforming CSV to JSON for data import`, err) + } +} + +module.exports = { + parse, + transform, +} diff --git a/packages/server/src/utilities/tests/csvParser.spec.js b/packages/server/src/utilities/tests/csvParser.spec.js new file mode 100644 index 0000000000..e69de29bb2