2022-11-26 07:54:57 +13:00
|
|
|
import LinkController from "./LinkController"
|
|
|
|
import {
|
2020-10-17 00:13:27 +13:00
|
|
|
IncludeDocs,
|
|
|
|
getLinkDocuments,
|
|
|
|
createLinkView,
|
|
|
|
getUniqueByProp,
|
2021-02-20 02:21:38 +13:00
|
|
|
getRelatedTableForField,
|
|
|
|
getLinkedTableIDs,
|
|
|
|
getLinkedTable,
|
2022-11-26 07:54:57 +13:00
|
|
|
} from "./linkUtils"
|
2023-07-28 07:36:32 +12:00
|
|
|
import flatten from "lodash/flatten"
|
2022-11-26 07:54:57 +13:00
|
|
|
import { getMultiIDParams, USER_METDATA_PREFIX } from "../utils"
|
2023-07-28 07:36:32 +12:00
|
|
|
import partition from "lodash/partition"
|
2022-11-26 07:54:57 +13:00
|
|
|
import { getGlobalUsersFromMetadata } from "../../utilities/global"
|
|
|
|
import { processFormulas } from "../../utilities/rowProcessor"
|
|
|
|
import { context } from "@budibase/backend-core"
|
2023-10-05 07:18:21 +13:00
|
|
|
import { Table, Row, LinkDocumentValue, FieldType } from "@budibase/types"
|
|
|
|
import sdk from "../../sdk"
|
2022-11-26 07:54:57 +13:00
|
|
|
|
|
|
|
export { IncludeDocs, getLinkDocuments, createLinkView } from "./linkUtils"
|
2020-09-29 05:36:59 +13:00
|
|
|
|
|
|
|
/**
|
2020-10-10 07:10:28 +13:00
|
|
|
* This functionality makes sure that when rows with links are created, updated or deleted they are processed
|
2020-09-29 05:36:59 +13:00
|
|
|
* correctly - making sure that no stale links are left around and that all links have been made successfully.
|
|
|
|
*/
|
|
|
|
|
2022-11-26 07:54:57 +13:00
|
|
|
export const EventType = {
|
2020-10-10 07:10:28 +13:00
|
|
|
ROW_SAVE: "row:save",
|
|
|
|
ROW_UPDATE: "row:update",
|
|
|
|
ROW_DELETE: "row:delete",
|
2020-10-10 06:49:23 +13:00
|
|
|
TABLE_SAVE: "table:save",
|
|
|
|
TABLE_UPDATED: "table:updated",
|
|
|
|
TABLE_DELETE: "table:delete",
|
2020-09-29 05:36:59 +13:00
|
|
|
}
|
|
|
|
|
2022-11-26 07:54:57 +13:00
|
|
|
function clearRelationshipFields(table: Table, rows: Row[]) {
|
2021-09-14 03:28:52 +12:00
|
|
|
for (let [key, field] of Object.entries(table.schema)) {
|
2023-10-05 07:18:21 +13:00
|
|
|
if (field.type === FieldType.LINK) {
|
2021-09-14 03:28:52 +12:00
|
|
|
rows = rows.map(row => {
|
|
|
|
delete row[key]
|
|
|
|
return row
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return rows
|
|
|
|
}
|
|
|
|
|
2023-10-05 07:18:21 +13:00
|
|
|
async function getLinksForRows(rows: Row[]): Promise<LinkDocumentValue[]> {
|
2021-05-04 22:32:22 +12:00
|
|
|
const tableIds = [...new Set(rows.map(el => el.tableId))]
|
2021-02-18 06:09:59 +13:00
|
|
|
// start by getting all the link values for performance reasons
|
2022-11-26 07:54:57 +13:00
|
|
|
const promises = tableIds.map(tableId =>
|
|
|
|
getLinkDocuments({
|
|
|
|
tableId: tableId,
|
|
|
|
includeDocs: IncludeDocs.EXCLUDE,
|
|
|
|
})
|
|
|
|
)
|
2021-02-18 07:04:21 +13:00
|
|
|
const responses = flatten(
|
2022-11-26 07:54:57 +13:00
|
|
|
(await Promise.all(promises)) as LinkDocumentValue[][]
|
2021-02-18 06:09:59 +13:00
|
|
|
)
|
|
|
|
// have to get unique as the previous table query can
|
|
|
|
// return duplicates, could be querying for both tables in a relation
|
|
|
|
return getUniqueByProp(
|
|
|
|
responses
|
2022-05-13 05:14:38 +12:00
|
|
|
.filter(el => el != null)
|
2021-02-18 06:09:59 +13:00
|
|
|
// create a unique ID which we can use for getting only unique ones
|
2021-05-04 22:32:22 +12:00
|
|
|
.map(el => ({ ...el, unique: el.id + el.thisId + el.fieldName })),
|
2021-02-18 06:09:59 +13:00
|
|
|
"unique"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-11-26 07:54:57 +13:00
|
|
|
async function getFullLinkedDocs(links: LinkDocumentValue[]) {
|
2021-06-09 03:06:30 +12:00
|
|
|
// create DBs
|
2022-11-23 08:49:59 +13:00
|
|
|
const db = context.getAppDB()
|
2021-06-09 03:06:30 +12:00
|
|
|
const linkedRowIds = links.map(link => link.id)
|
2021-09-22 02:59:50 +12:00
|
|
|
const uniqueRowIds = [...new Set(linkedRowIds)]
|
|
|
|
let dbRows = (await db.allDocs(getMultiIDParams(uniqueRowIds))).rows.map(
|
2021-06-09 03:06:30 +12:00
|
|
|
row => row.doc
|
|
|
|
)
|
2021-09-22 02:59:50 +12:00
|
|
|
// convert the unique db rows back to a full list of linked rows
|
2021-10-22 05:23:10 +13:00
|
|
|
const linked = linkedRowIds
|
|
|
|
.map(id => dbRows.find(row => row && row._id === id))
|
|
|
|
.filter(row => row != null)
|
2021-06-09 03:06:30 +12:00
|
|
|
// need to handle users as specific cases
|
2021-06-09 03:11:46 +12:00
|
|
|
let [users, other] = partition(linked, linkRow =>
|
|
|
|
linkRow._id.startsWith(USER_METDATA_PREFIX)
|
|
|
|
)
|
2022-01-28 07:18:31 +13:00
|
|
|
users = await getGlobalUsersFromMetadata(users)
|
2021-06-09 03:11:46 +12:00
|
|
|
return [...other, ...users]
|
2021-06-09 03:06:30 +12:00
|
|
|
}
|
|
|
|
|
2020-09-29 23:55:18 +13:00
|
|
|
/**
|
2020-10-10 07:10:28 +13:00
|
|
|
* Update link documents for a row or table - this is to be called by the API controller when a change is occurring.
|
2021-06-26 04:14:23 +12:00
|
|
|
* @param {string} args.eventType states what type of change which is occurring, means this can be expanded upon in the
|
2020-09-29 23:55:18 +13:00
|
|
|
* future quite easily (all updates go through one function).
|
2021-06-26 04:14:23 +12:00
|
|
|
* @param {string} args.tableId The ID of the of the table which is being changed.
|
2022-01-28 13:05:39 +13:00
|
|
|
* @param {object|undefined} args.row The row which is changing, e.g. created, updated or deleted.
|
|
|
|
* @param {object|undefined} args.table If the table has already been retrieved this can be used to reduce database gets.
|
|
|
|
* @param {object|undefined} args.oldTable If the table is being updated then the old table can be provided for differencing.
|
2020-10-10 07:10:28 +13:00
|
|
|
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the row for
|
|
|
|
* row operations and the table for table operations.
|
2020-09-29 23:55:18 +13:00
|
|
|
*/
|
2022-11-26 07:54:57 +13:00
|
|
|
export async function updateLinks(args: {
|
2022-11-27 04:10:41 +13:00
|
|
|
tableId?: string
|
2022-11-26 07:54:57 +13:00
|
|
|
eventType: string
|
|
|
|
row?: Row
|
|
|
|
table?: Table
|
|
|
|
oldTable?: Table
|
|
|
|
}) {
|
2022-01-28 07:18:31 +13:00
|
|
|
const { eventType, row, tableId, table, oldTable } = args
|
2020-10-13 05:45:11 +13:00
|
|
|
const baseReturnObj = row == null ? table : row
|
2020-10-10 06:49:23 +13:00
|
|
|
// make sure table ID is set
|
|
|
|
if (tableId == null && table != null) {
|
2022-11-26 07:54:57 +13:00
|
|
|
args.tableId = table._id!
|
2020-09-29 05:36:59 +13:00
|
|
|
}
|
2021-06-26 04:14:23 +12:00
|
|
|
let linkController = new LinkController(args)
|
2020-10-13 05:45:11 +13:00
|
|
|
try {
|
|
|
|
if (
|
2021-09-22 02:59:50 +12:00
|
|
|
!(await linkController.doesTableHaveLinkedFields(table)) &&
|
2020-10-13 05:45:11 +13:00
|
|
|
(oldTable == null ||
|
|
|
|
!(await linkController.doesTableHaveLinkedFields(oldTable)))
|
|
|
|
) {
|
|
|
|
return baseReturnObj
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
return baseReturnObj
|
2020-09-29 23:55:18 +13:00
|
|
|
}
|
|
|
|
switch (eventType) {
|
2020-10-10 07:10:28 +13:00
|
|
|
case EventType.ROW_SAVE:
|
|
|
|
case EventType.ROW_UPDATE:
|
|
|
|
return await linkController.rowSaved()
|
|
|
|
case EventType.ROW_DELETE:
|
|
|
|
return await linkController.rowDeleted()
|
2020-10-10 06:49:23 +13:00
|
|
|
case EventType.TABLE_SAVE:
|
|
|
|
return await linkController.tableSaved()
|
|
|
|
case EventType.TABLE_UPDATED:
|
|
|
|
return await linkController.tableUpdated()
|
|
|
|
case EventType.TABLE_DELETE:
|
|
|
|
return await linkController.tableDeleted()
|
2020-09-29 23:55:18 +13:00
|
|
|
default:
|
2020-10-10 07:10:28 +13:00
|
|
|
throw "Type of event is not known, linked row handler requires update."
|
2020-09-29 23:55:18 +13:00
|
|
|
}
|
|
|
|
}
|
2020-09-29 05:36:59 +13:00
|
|
|
|
2021-02-18 07:04:21 +13:00
|
|
|
/**
|
2021-04-30 06:06:58 +12:00
|
|
|
* Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row.
|
|
|
|
* This is required for formula fields, this may only be utilised internally (for now).
|
2021-02-18 07:04:21 +13:00
|
|
|
* @param {object} table The table from which the rows originated.
|
2021-04-30 06:06:58 +12:00
|
|
|
* @param {array<object>} rows The rows which are to be enriched.
|
2023-10-06 06:23:18 +13:00
|
|
|
* @param {object} opts optional - options like passing in a base row to use for enrichment.
|
2021-04-30 06:06:58 +12:00
|
|
|
* @return {Promise<*>} returns the rows with all of the enriched relationships on it.
|
2021-02-18 07:04:21 +13:00
|
|
|
*/
|
2023-10-06 06:23:18 +13:00
|
|
|
export async function attachFullLinkedDocs(
|
|
|
|
table: Table,
|
|
|
|
rows: Row[],
|
|
|
|
opts?: { fromRow?: Row }
|
|
|
|
) {
|
2021-02-18 07:04:21 +13:00
|
|
|
const linkedTableIds = getLinkedTableIDs(table)
|
|
|
|
if (linkedTableIds.length === 0) {
|
|
|
|
return rows
|
|
|
|
}
|
2023-10-05 07:18:21 +13:00
|
|
|
// get tables and links
|
|
|
|
let response = await Promise.all([
|
|
|
|
getLinksForRows(rows),
|
|
|
|
sdk.tables.getTables(linkedTableIds),
|
|
|
|
])
|
2023-10-06 06:23:18 +13:00
|
|
|
// find the links that pertain to one of the rows that is being enriched
|
2023-10-05 07:18:21 +13:00
|
|
|
const links = (response[0] as LinkDocumentValue[]).filter(link =>
|
2021-05-04 22:32:22 +12:00
|
|
|
rows.some(row => row._id === link.thisId)
|
2021-02-18 06:09:59 +13:00
|
|
|
)
|
2023-10-06 06:23:18 +13:00
|
|
|
// if fromRow has been passed in, then we don't need to fetch it (optimisation)
|
|
|
|
let linksWithoutFromRow = links
|
|
|
|
if (opts?.fromRow) {
|
|
|
|
linksWithoutFromRow = links.filter(link => link.id !== opts?.fromRow?._id)
|
|
|
|
}
|
2023-10-05 07:18:21 +13:00
|
|
|
const linkedTables = response[1] as Table[]
|
2021-09-14 03:28:52 +12:00
|
|
|
// clear any existing links that could be dupe'd
|
|
|
|
rows = clearRelationshipFields(table, rows)
|
|
|
|
// now get the docs and combine into the rows
|
2023-10-06 06:23:18 +13:00
|
|
|
let linked = []
|
|
|
|
if (linksWithoutFromRow.length > 0) {
|
|
|
|
linked = await getFullLinkedDocs(linksWithoutFromRow)
|
|
|
|
}
|
2021-02-18 06:09:59 +13:00
|
|
|
for (let row of rows) {
|
2021-05-04 22:32:22 +12:00
|
|
|
for (let link of links.filter(link => link.thisId === row._id)) {
|
2021-02-20 02:21:38 +13:00
|
|
|
if (row[link.fieldName] == null) {
|
|
|
|
row[link.fieldName] = []
|
|
|
|
}
|
2023-10-06 06:23:18 +13:00
|
|
|
let linkedRow: Row
|
|
|
|
if (opts?.fromRow && opts?.fromRow?._id === link.id) {
|
|
|
|
linkedRow = opts.fromRow!
|
|
|
|
} else {
|
|
|
|
linkedRow = linked.find(row => row._id === link.id)
|
|
|
|
}
|
2021-10-22 05:23:10 +13:00
|
|
|
if (linkedRow) {
|
|
|
|
const linkedTableId =
|
|
|
|
linkedRow.tableId || getRelatedTableForField(table, link.fieldName)
|
2023-10-05 07:18:21 +13:00
|
|
|
const linkedTable = linkedTables.find(
|
|
|
|
table => table._id === linkedTableId
|
|
|
|
)
|
2021-10-22 05:23:10 +13:00
|
|
|
if (linkedTable) {
|
|
|
|
row[link.fieldName].push(processFormulas(linkedTable, linkedRow))
|
|
|
|
}
|
2021-02-20 02:21:38 +13:00
|
|
|
}
|
|
|
|
}
|
2021-02-18 06:09:59 +13:00
|
|
|
}
|
2021-02-19 23:32:24 +13:00
|
|
|
return rows
|
2021-02-18 06:09:59 +13:00
|
|
|
}
|
2021-04-30 06:06:58 +12:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This function will take the given enriched rows and squash the links to only contain the primary display field.
|
|
|
|
* @param {object} table The table from which the rows originated.
|
|
|
|
* @param {array<object>} enriched The pre-enriched rows (full docs) which are to be squashed.
|
|
|
|
* @returns {Promise<Array>} The rows after having their links squashed to only contain the ID and primary display.
|
|
|
|
*/
|
2022-11-26 07:54:57 +13:00
|
|
|
export async function squashLinksToPrimaryDisplay(
|
|
|
|
table: Table,
|
2023-06-02 05:18:12 +12:00
|
|
|
enriched: Row[] | Row
|
2022-11-26 07:54:57 +13:00
|
|
|
) {
|
2021-04-30 06:06:58 +12:00
|
|
|
// will populate this as we find them
|
2021-09-04 01:49:56 +12:00
|
|
|
const linkedTables = [table]
|
2023-06-02 05:18:12 +12:00
|
|
|
const isArray = Array.isArray(enriched)
|
|
|
|
let enrichedArray = !isArray ? [enriched] : enriched
|
|
|
|
for (let row of enrichedArray) {
|
2021-09-04 01:49:56 +12:00
|
|
|
// this only fetches the table if its not already in array
|
2022-11-26 07:54:57 +13:00
|
|
|
const rowTable = await getLinkedTable(row.tableId!, linkedTables)
|
|
|
|
for (let [column, schema] of Object.entries(rowTable?.schema || {})) {
|
2023-10-05 07:18:21 +13:00
|
|
|
if (schema.type !== FieldType.LINK || !Array.isArray(row[column])) {
|
2021-04-30 06:06:58 +12:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
const newLinks = []
|
|
|
|
for (let link of row[column]) {
|
|
|
|
const linkTblId = link.tableId || getRelatedTableForField(table, column)
|
2023-10-05 07:18:21 +13:00
|
|
|
const linkedTable = await getLinkedTable(linkTblId!, linkedTables)
|
2022-11-26 07:54:57 +13:00
|
|
|
const obj: any = { _id: link._id }
|
|
|
|
if (linkedTable?.primaryDisplay && link[linkedTable.primaryDisplay]) {
|
2021-04-30 06:06:58 +12:00
|
|
|
obj.primaryDisplay = link[linkedTable.primaryDisplay]
|
|
|
|
}
|
|
|
|
newLinks.push(obj)
|
|
|
|
}
|
|
|
|
row[column] = newLinks
|
|
|
|
}
|
|
|
|
}
|
2023-06-02 05:18:12 +12:00
|
|
|
return isArray ? enrichedArray : enrichedArray[0]
|
2021-04-30 06:06:58 +12:00
|
|
|
}
|