diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js index 61dd666f3e..41d2ae15b1 100644 --- a/packages/server/src/api/controllers/row.js +++ b/packages/server/src/api/controllers/row.js @@ -9,7 +9,7 @@ const { ViewNames, } = require("../../db/utils") const usersController = require("./user") -const { cloneDeep } = require("lodash") +const { coerceRowValues } = require("../../utilities") const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}` @@ -29,11 +29,24 @@ validateJs.extend(validateJs.validators.datetime, { }, }) -// lots of row functionality too specific to pass to user controller, simply handle the -// password deletion here -function removePassword(tableId, row) { +async function findRow(db, appId, tableId, rowId) { + let row if (tableId === ViewNames.USERS) { - delete row.password + let ctx = { + params: { + userId: rowId, + }, + user: { + appId, + }, + } + await usersController.find(ctx) + row = ctx.body + } else { + row = await db.get(rowId) + } + if (row.tableId !== tableId) { + throw "Supplied tableId does not match the rows tableId" } return row } @@ -224,13 +237,12 @@ exports.fetchTableRows = async function(ctx) { exports.find = async function(ctx) { const appId = ctx.user.appId const db = new CouchDB(appId) - let row = await db.get(ctx.params.rowId) - if (row.tableId !== ctx.params.tableId) { - ctx.throw(400, "Supplied tableId does not match the rows tableId") - return + try { + const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId) + ctx.body = await linkRows.attachLinkInfo(appId, row) + } catch (err) { + ctx.throw(400, err) } - row = removePassword(ctx.params.tableId, row) - ctx.body = await linkRows.attachLinkInfo(appId, row) } exports.destroy = async function(ctx) { @@ -296,8 +308,10 @@ exports.fetchEnrichedRow = async function(ctx) { return } // need table to work out where links go in row - let [table, row] = await Promise.all([db.get(tableId), db.get(rowId)]) - row = removePassword(tableId, row) + let [table, row] = await Promise.all([ + db.get(tableId), + findRow(db, appId, tableId, rowId), + ]) // get the link docs const linkVals = await linkRows.getLinkDocuments({ appId, @@ -327,68 +341,6 @@ exports.fetchEnrichedRow = async function(ctx) { ctx.status = 200 } -function coerceRowValues(record, table) { - const row = cloneDeep(record) - for (let [key, value] of Object.entries(row)) { - const field = table.schema[key] - if (!field) continue - - // eslint-disable-next-line no-prototype-builtins - if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) { - row[key] = TYPE_TRANSFORM_MAP[field.type][value] - } else if (TYPE_TRANSFORM_MAP[field.type].parse) { - row[key] = TYPE_TRANSFORM_MAP[field.type].parse(value) - } - } - return row -} - -const TYPE_TRANSFORM_MAP = { - link: { - "": [], - [null]: [], - [undefined]: undefined, - }, - options: { - "": "", - [null]: "", - [undefined]: undefined, - }, - string: { - "": "", - [null]: "", - [undefined]: undefined, - }, - longform: { - "": "", - [null]: "", - [undefined]: undefined, - }, - number: { - "": null, - [null]: null, - [undefined]: undefined, - parse: n => parseFloat(n), - }, - datetime: { - "": null, - [undefined]: undefined, - [null]: null, - }, - attachment: { - "": [], - [null]: [], - [undefined]: undefined, - }, - boolean: { - "": null, - [null]: null, - [undefined]: undefined, - true: true, - false: false, - }, -} - async function bulkDelete(ctx) { const appId = ctx.user.appId const { rows } = ctx.request.body diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 7ca2e05015..9ef7c2281f 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -89,9 +89,12 @@ exports.destroy = async function(ctx) { exports.find = async function(ctx) { const database = new CouchDB(ctx.user.appId) - const user = await database.get(generateUserID(ctx.params.email)) - ctx.body = { - email: user.email, - _rev: user._rev, + let lookup = ctx.params.email + ? generateUserID(ctx.params.email) + : ctx.params.userId + const user = await database.get(lookup) + if (user) { + delete user.password } + ctx.body = user } diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index 5972e90ca9..0698250050 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -51,8 +51,6 @@ describe("/rows", () => { describe("save, load, update, delete", () => { - - it("returns a success message when the row is created", async () => { const res = await createRow() expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`) diff --git a/packages/server/src/utilities/index.js b/packages/server/src/utilities/index.js index cde10b6b62..8a43e9d27a 100644 --- a/packages/server/src/utilities/index.js +++ b/packages/server/src/utilities/index.js @@ -1,8 +1,59 @@ const env = require("../environment") const { DocumentTypes, SEPARATOR } = require("../db/utils") +const fs = require("fs") +const { cloneDeep } = require("lodash/fp") const APP_PREFIX = DocumentTypes.APP + SEPARATOR +/** + * A map of how we convert various properties in rows to each other based on the row type. + */ +const TYPE_TRANSFORM_MAP = { + link: { + "": [], + [null]: [], + [undefined]: undefined, + }, + options: { + "": "", + [null]: "", + [undefined]: undefined, + }, + string: { + "": "", + [null]: "", + [undefined]: undefined, + }, + longform: { + "": "", + [null]: "", + [undefined]: undefined, + }, + number: { + "": null, + [null]: null, + [undefined]: undefined, + parse: n => parseFloat(n), + }, + datetime: { + "": null, + [undefined]: undefined, + [null]: null, + }, + attachment: { + "": [], + [null]: [], + [undefined]: undefined, + }, + boolean: { + "": null, + [null]: null, + [undefined]: undefined, + true: true, + false: false, + }, +} + function confirmAppId(possibleAppId) { return possibleAppId && possibleAppId.startsWith(APP_PREFIX) ? possibleAppId @@ -74,3 +125,46 @@ exports.setCookie = (ctx, name, value) => { exports.isClient = ctx => { return ctx.headers["x-budibase-type"] === "client" } + +/** + * Recursively walk a directory tree and execute a callback on all files. + * @param {String} dirPath - Directory to traverse + * @param {Function} callback - callback to execute on files + */ +exports.walkDir = (dirPath, callback) => { + for (let filename of fs.readdirSync(dirPath)) { + const filePath = `${dirPath}/${filename}` + const stat = fs.lstatSync(filePath) + + if (stat.isFile()) { + callback(filePath) + } else { + exports.walkDir(filePath, callback) + } + } +} + +/** + * This will coerce the values in a row to the correct types based on the type transform map and the + * table schema. + * @param {object} row The row which is to be coerced to correct values based on schema, this input + * row will not be updated. + * @param {object} table The table that has been retrieved from DB, this must contain the expected + * schema for the rows. + * @returns {object} The updated row will be returned with all values coerced. + */ +exports.coerceRowValues = (row, table) => { + const clonedRow = cloneDeep(row) + for (let [key, value] of Object.entries(clonedRow)) { + const field = table.schema[key] + if (!field) continue + + // eslint-disable-next-line no-prototype-builtins + if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) { + clonedRow[key] = TYPE_TRANSFORM_MAP[field.type][value] + } else if (TYPE_TRANSFORM_MAP[field.type].parse) { + clonedRow[key] = TYPE_TRANSFORM_MAP[field.type].parse(value) + } + } + return clonedRow +}