1
0
Fork 0
mirror of synced 2024-06-27 18:40:42 +12:00
budibase/packages/server/src/api/controllers/row.js

448 lines
11 KiB
JavaScript

const CouchDB = require("../../db")
const validateJs = require("validate.js")
const linkRows = require("../../db/linkedRows")
const {
getRowParams,
generateRowID,
DocumentTypes,
SEPARATOR,
ViewNames,
generateUserID,
} = require("../../db/utils")
const usersController = require("./user")
const {
inputProcessing,
outputProcessing,
} = require("../../utilities/rowProcessor")
const { FieldTypes } = require("../../constants")
const { isEqual } = require("lodash")
const { cloneDeep } = require("lodash/fp")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
const CALCULATION_TYPES = {
SUM: "sum",
COUNT: "count",
STATS: "stats",
}
validateJs.extend(validateJs.validators.datetime, {
parse: function(value) {
return new Date(value).getTime()
},
// Input is a unix timestamp
format: function(value) {
return new Date(value).toISOString()
},
})
async function findRow(db, appId, tableId, rowId) {
let row
if (tableId === ViewNames.USERS) {
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
}
exports.patch = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
let dbRow = await db.get(ctx.params.rowId)
let dbTable = await db.get(dbRow.tableId)
const patchfields = ctx.request.body
// need to build up full patch fields before coerce
for (let key of Object.keys(patchfields)) {
if (!dbTable.schema[key]) continue
dbRow[key] = patchfields[key]
}
// this returns the table and row incase they have been updated
let { table, row } = inputProcessing(ctx.user, dbTable, dbRow)
const validateResult = await validate({
row,
table,
})
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) {
// the row has been updated, need to put it into the ctx
ctx.request.body = {
...row,
password: ctx.request.body.password,
}
await usersController.update(ctx)
return
}
const response = await db.put(row)
// don't worry about rev, tables handle rev/lastID updates
if (!isEqual(dbTable, table)) {
await db.put(table)
}
row._rev = response.rev
row.type = "row"
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:update`, appId, row, table)
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} updated successfully.`
}
exports.save = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
let inputs = ctx.request.body
inputs.tableId = ctx.params.tableId
// TODO: find usage of this and break out into own endpoint
if (inputs.type === "delete") {
await bulkDelete(ctx)
ctx.body = inputs.rows
return
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
if (existingRow) {
ctx.params.rowId = inputs._id
await exports.patch(ctx)
return
}
if (!inputs._rev && !inputs._id) {
if (inputs.tableId === ViewNames.USERS) {
inputs._id = generateUserID(inputs.email)
} else {
inputs._id = generateRowID(inputs.tableId)
}
}
// this returns the table and row incase they have been updated
const dbTable = await db.get(inputs.tableId)
let { table, row } = inputProcessing(ctx.user, dbTable, inputs)
const validateResult = await validate({
row,
table,
})
if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
errors: validateResult.errors,
}
return
}
// make sure link rows are up to date
row = await linkRows.updateLinks({
appId,
eventType: linkRows.EventType.ROW_SAVE,
row,
tableId: row.tableId,
table,
})
// Creation of a new user goes to the user controller
if (row.tableId === ViewNames.USERS) {
// the row has been updated, need to put it into the ctx
ctx.request.body = row
await usersController.create(ctx)
return
}
row.type = "row"
const response = await db.put(row)
// don't worry about rev, tables handle rev/lastID updates
if (!isEqual(dbTable, table)) {
await db.put(table)
}
row._rev = response.rev
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} saved successfully`
}
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.startsWith(TABLE_VIEW_BEGINS_WITH)) {
ctx.params.tableId = viewName.substring(4)
await exports.fetchTableRows(ctx)
return
}
const db = new CouchDB(appId)
const { calculation, group, field } = ctx.query
const designDoc = await db.get("_design/database")
const viewInfo = designDoc.views[viewName]
if (!viewInfo) {
ctx.throw(400, "View does not exist.")
}
const response = await db.query(`database/${viewName}`, {
include_docs: !calculation,
group,
})
if (!calculation) {
response.rows = response.rows.map(row => row.doc)
let table
try {
table = await db.get(viewInfo.meta.tableId)
} catch (err) {
/* istanbul ignore next */
table = {
schema: {},
}
}
ctx.body = await outputProcessing(appId, table, response.rows)
}
if (calculation === CALCULATION_TYPES.STATS) {
response.rows = response.rows.map(row => ({
group: row.key,
field,
...row.value,
avg: row.value.sum / row.value.count,
}))
ctx.body = response.rows
}
if (
calculation === CALCULATION_TYPES.COUNT ||
calculation === CALCULATION_TYPES.SUM
) {
ctx.body = response.rows.map(row => ({
group: row.key,
field,
value: row.value,
}))
}
}
exports.search = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const {
query,
pagination: { pageSize = 10, page },
} = ctx.request.body
// make all strings a starts with operation rather than pure equality
for (const [key, queryVal] of Object.entries(query)) {
if (typeof queryVal === "string") {
query[key] = {
$gt: queryVal,
$lt: `${queryVal}\uffff`,
}
}
}
// pure equality for table
query.tableId = ctx.params.tableId
const response = await db.find({
selector: query,
limit: pageSize,
skip: pageSize * page,
})
const rows = response.docs
// delete passwords from users
if (query.tableId === ViewNames.USERS) {
for (let row of rows) {
delete row.password
}
}
const table = await db.get(ctx.params.tableId)
ctx.body = await outputProcessing(appId, table, rows)
}
exports.fetchTableRows = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
// special case for users, fetch through the user controller
let rows,
table = await db.get(ctx.params.tableId)
if (ctx.params.tableId === ViewNames.USERS) {
await usersController.fetch(ctx)
rows = ctx.body
} else {
const response = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
rows = response.rows.map(row => row.doc)
}
ctx.body = await outputProcessing(appId, table, rows)
}
exports.find = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
try {
const table = await db.get(ctx.params.tableId)
const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId)
ctx.body = await outputProcessing(appId, table, row)
} catch (err) {
ctx.throw(400, err)
}
}
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")
}
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)
}
exports.validate = async function(ctx) {
const errors = await validate({
appId: ctx.user.appId,
tableId: ctx.params.tableId,
row: ctx.request.body,
})
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)
}
const errors = {}
for (let fieldName of Object.keys(table.schema)) {
const constraints = cloneDeep(table.schema[fieldName].constraints)
// special case for options, need to always allow unselected (null)
if (
table.schema[fieldName].type === FieldTypes.OPTIONS &&
constraints.inclusion
) {
constraints.inclusion.push(null)
}
const res = validateJs.single(row[fieldName], constraints)
if (res) errors[fieldName] = res
}
return { valid: Object.keys(errors).length === 0, errors }
}
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
// need table to work out where links go in 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,
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 outputProcessing(
appId,
table,
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 === FieldTypes.LINK) {
// find the links that pertain to this field, get their indexes
const linkIndexes = linkVals
.filter(link => link.fieldName === fieldName)
.map(link => linkVals.indexOf(link))
// find the rows that the links state are linked to this field
row[fieldName] = linkedRows.filter((linkRow, index) =>
linkIndexes.includes(index)
)
}
}
ctx.body = row
ctx.status = 200
}
async function bulkDelete(ctx) {
const appId = ctx.user.appId
const { rows } = ctx.request.body
const db = new CouchDB(appId)
const linkUpdates = rows.map(row =>
linkRows.updateLinks({
appId,
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)
})
}