budibase/packages/server/src/api/controllers/row.js

413 lines
9.8 KiB
JavaScript
Raw Normal View History

2020-05-07 11:53:34 +02:00
const CouchDB = require("../../db")
2020-05-28 16:39:29 +02:00
const validateJs = require("validate.js")
const linkRows = require("../../db/linkedRows")
const {
getRowParams,
generateRowID,
DocumentTypes,
SEPARATOR,
2020-11-24 18:00:15 +01:00
ViewNames,
} = require("../../db/utils")
2020-11-24 18:00:15 +01:00
const usersController = require("./user")
2020-10-06 22:37:10 +02:00
const { cloneDeep } = require("lodash")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
2020-05-04 18:13:57 +02:00
2020-10-15 13:09:41 +02:00
const CALCULATION_TYPES = {
SUM: "sum",
COUNT: "count",
2020-10-15 18:05:09 +02:00
STATS: "stats",
2020-10-15 13:09:41 +02:00
}
validateJs.extend(validateJs.validators.datetime, {
2020-10-14 16:13:22 +02:00
parse: function(value) {
return new Date(value).getTime()
},
// Input is a unix timestamp
2020-10-14 16:13:22 +02:00
format: function(value) {
return new Date(value).toISOString()
2020-09-09 17:27:46 +02:00
},
})
// lots of row functionality too specific to pass to user controller, simply handle the
// password deletion here
function removePassword(tableId, row) {
if (tableId === ViewNames.USERS) {
delete row.password
}
return row
}
2020-10-14 16:13:22 +02:00
exports.patch = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
let row = await db.get(ctx.params.id)
const table = await db.get(row.tableId)
2020-09-10 10:36:14 +02:00
const patchfields = ctx.request.body
row = coerceRowValues(row, table)
2020-09-10 10:36:14 +02:00
for (let key of Object.keys(patchfields)) {
if (!table.schema[key]) continue
row[key] = patchfields[key]
2020-09-10 10:36:14 +02:00
}
2020-09-10 22:11:05 +02:00
2020-09-10 10:36:14 +02:00
const validateResult = await validate({
row,
table,
2020-09-10 10:36:14 +02:00
})
if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
errors: validateResult.errors,
}
return
}
// returned row is cleaned and prepared for writing to DB
row = await linkRows.updateLinks({
appId,
eventType: linkRows.EventType.ROW_UPDATE,
row,
tableId: row.tableId,
table,
})
// Creation of a new user goes to the user controller
if (row.tableId === ViewNames.USERS) {
await usersController.update(ctx)
return
}
const response = await db.put(row)
row._rev = response.rev
row.type = "row"
2020-10-29 11:55:52 +01:00
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:update`, appId, row, table)
ctx.body = row
2020-09-10 10:36:14 +02:00
ctx.status = 200
ctx.message = `${table.name} updated successfully.`
2020-09-10 10:36:14 +02:00
}
2020-10-14 16:13:22 +02:00
exports.save = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
let row = ctx.request.body
row.tableId = ctx.params.tableId
2020-04-08 17:57:27 +02:00
// TODO: find usage of this and break out into own endpoint
2020-10-13 17:17:07 +02:00
if (ctx.request.body.type === "delete") {
await bulkDelete(ctx)
ctx.body = ctx.request.body.rows
return
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
if (existingRow) {
ctx.params.id = row._id
await exports.patch(ctx)
return
}
if (!row._rev && !row._id) {
row._id = generateRowID(row.tableId)
2020-05-14 16:12:30 +02:00
}
const table = await db.get(row.tableId)
2020-05-28 16:39:29 +02:00
row = coerceRowValues(row, table)
2020-10-02 15:14:58 +02:00
2020-05-28 16:39:29 +02:00
const validateResult = await validate({
row,
table,
2020-05-07 11:53:34 +02:00
})
2020-04-09 11:13:19 +02:00
2020-05-28 16:39:29 +02:00
if (!validateResult.valid) {
2020-04-22 17:35:20 +02:00
ctx.status = 400
ctx.body = {
status: 400,
2020-05-28 16:39:29 +02:00
errors: validateResult.errors,
2020-05-07 11:53:34 +02:00
}
return
2020-04-09 11:13:19 +02:00
}
// make sure link rows are up to date
row = await linkRows.updateLinks({
appId,
2020-10-14 16:06:48 +02:00
eventType: linkRows.EventType.ROW_SAVE,
row,
tableId: row.tableId,
table,
2020-09-30 16:41:52 +02:00
})
2020-11-24 18:00:15 +01:00
// Creation of a new user goes to the user controller
if (row.tableId === ViewNames.USERS) {
await usersController.create(ctx)
2020-04-22 17:35:20 +02:00
return
2020-04-09 11:13:19 +02:00
}
row.type = "row"
const response = await db.put(row)
row._rev = response.rev
2020-10-29 11:55:52 +01:00
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
ctx.body = row
2020-04-22 17:35:20 +02:00
ctx.status = 200
ctx.message = `${table.name} saved successfully`
2020-04-09 11:13:19 +02:00
}
2020-10-14 16:13:22 +02:00
exports.fetchView = async function(ctx) {
const appId = ctx.user.appId
const viewName = ctx.params.viewName
// if this is a table view being looked for just transfer to that
if (viewName.indexOf(TABLE_VIEW_BEGINS_WITH) === 0) {
ctx.params.tableId = viewName.substring(4)
await exports.fetchTableRows(ctx)
return
}
const db = new CouchDB(appId)
const { calculation, group, field } = ctx.query
const response = await db.query(`database/${viewName}`, {
include_docs: !calculation,
2020-08-18 18:14:26 +02:00
group,
2020-05-07 11:53:34 +02:00
})
if (!calculation) {
response.rows = response.rows.map(row => row.doc)
ctx.body = await linkRows.attachLinkInfo(appId, response.rows)
}
2020-10-15 13:09:41 +02:00
if (calculation === CALCULATION_TYPES.STATS) {
2020-08-24 12:46:28 +02:00
response.rows = response.rows.map(row => ({
group: row.key,
field,
2020-08-24 12:46:28 +02:00
...row.value,
avg: row.value.sum / row.value.count,
}))
ctx.body = response.rows
}
2020-10-15 13:09:41 +02:00
if (
calculation === CALCULATION_TYPES.COUNT ||
calculation === CALCULATION_TYPES.SUM
) {
ctx.body = response.rows.map(row => ({
group: row.key,
field,
value: row.value,
}))
}
2020-04-09 11:13:19 +02:00
}
2020-10-14 16:13:22 +02:00
exports.fetchTableRows = async function(ctx) {
const appId = ctx.user.appId
// special case for users, fetch through the user controller
let rows
if (ctx.params.tableId === ViewNames.USERS) {
await usersController.fetch(ctx)
rows = ctx.body
} else {
const db = new CouchDB(appId)
const response = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
rows = response.rows.map(row => row.doc)
}
ctx.body = await linkRows.attachLinkInfo(appId, rows)
2020-06-11 15:35:45 +02:00
}
2020-10-14 16:13:22 +02:00
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")
2020-05-27 18:23:01 +02:00
return
}
row = removePassword(ctx.params.tableId, row)
ctx.body = await linkRows.attachLinkInfo(appId, row)
2020-04-09 11:13:19 +02:00
}
2020-10-14 16:13:22 +02:00
exports.destroy = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const row = await db.get(ctx.params.rowId)
if (row.tableId !== ctx.params.tableId) {
ctx.throw(400, "Supplied tableId doesn't match the row's tableId")
2020-05-27 18:23:01 +02:00
return
}
await linkRows.updateLinks({
appId,
eventType: linkRows.EventType.ROW_DELETE,
row,
tableId: row.tableId,
})
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId)
ctx.status = 200
// for automations include the row that was deleted
ctx.row = row
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
2020-05-07 11:53:34 +02:00
}
2020-05-28 16:39:29 +02:00
2020-10-14 16:13:22 +02:00
exports.validate = async function(ctx) {
2020-05-28 16:39:29 +02:00
const errors = await validate({
appId: ctx.user.appId,
tableId: ctx.params.tableId,
row: ctx.request.body,
2020-05-28 16:39:29 +02:00
})
ctx.status = 200
ctx.body = errors
}
async function validate({ appId, tableId, row, table }) {
if (!table) {
const db = new CouchDB(appId)
table = await db.get(tableId)
2020-05-28 16:39:29 +02:00
}
const errors = {}
for (let fieldName of Object.keys(table.schema)) {
2020-09-14 15:35:03 +02:00
const res = validateJs.single(
row[fieldName],
table.schema[fieldName].constraints
2020-09-14 15:35:03 +02:00
)
2020-05-28 16:39:29 +02:00
if (res) errors[fieldName] = res
}
return { valid: Object.keys(errors).length === 0, errors }
}
2020-10-14 16:13:22 +02:00
exports.fetchEnrichedRow = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const tableId = ctx.params.tableId
const rowId = ctx.params.rowId
if (appId == null || tableId == null || rowId == null) {
ctx.status = 400
ctx.body = {
status: 400,
error:
"Cannot handle request, URI params have not been successfully prepared.",
}
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)
// get the link docs
const linkVals = await linkRows.getLinkDocuments({
appId,
tableId,
rowId,
})
// look up the actual rows based on the ids
const response = await db.allDocs({
include_docs: true,
keys: linkVals.map(linkVal => linkVal.id),
})
// need to include the IDs in these rows for any links they may have
let linkedRows = await linkRows.attachLinkInfo(
appId,
response.rows.map(row => row.doc)
)
// insert the link rows in the correct place throughout the main row
for (let fieldName of Object.keys(table.schema)) {
let field = table.schema[fieldName]
if (field.type === "link") {
row[fieldName] = linkedRows.filter(
linkRow => linkRow.tableId === field.tableId
)
}
}
ctx.body = row
ctx.status = 200
}
2020-11-24 18:00:15 +01:00
function coerceRowValues(record, table) {
const row = cloneDeep(record)
for (let [key, value] of Object.entries(row)) {
const field = table.schema[key]
2020-10-02 15:14:58 +02:00
if (!field) continue
2020-10-06 22:37:10 +02:00
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
row[key] = TYPE_TRANSFORM_MAP[field.type][value]
2020-10-06 22:37:10 +02:00
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
row[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
2020-10-06 22:37:10 +02:00
}
2020-10-02 15:14:58 +02:00
}
return row
2020-10-02 15:14:58 +02:00
}
const TYPE_TRANSFORM_MAP = {
link: {
"": [],
[null]: [],
[undefined]: undefined,
},
options: {
"": "",
[null]: "",
[undefined]: undefined,
},
2020-10-02 15:14:58 +02:00
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
longform: {
"": "",
[null]: "",
[undefined]: undefined,
},
2020-10-02 15:14:58 +02:00
number: {
"": null,
[null]: null,
2020-10-02 15:14:58 +02:00
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
2020-10-02 15:14:58 +02:00
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
2020-10-02 15:14:58 +02:00
[null]: null,
[undefined]: undefined,
2020-10-06 22:37:10 +02:00
true: true,
false: false,
2020-10-02 15:14:58 +02:00
},
}
2020-10-13 17:17:07 +02:00
async function bulkDelete(ctx) {
const appId = ctx.user.appId
2020-10-13 17:17:07 +02:00
const { rows } = ctx.request.body
const db = new CouchDB(appId)
2020-10-13 17:17:07 +02:00
const linkUpdates = rows.map(row =>
linkRows.updateLinks({
appId,
2020-10-13 17:17:07 +02:00
eventType: linkRows.EventType.ROW_DELETE,
row,
tableId: row.tableId,
})
)
await db.bulkDocs(rows.map(row => ({ ...row, _deleted: true })))
await Promise.all(linkUpdates)
rows.forEach(row => {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
2020-10-13 17:17:07 +02:00
})
}