diff --git a/packages/server/src/db/linkedRows/LinkController.js b/packages/server/src/db/linkedRows/LinkController.ts similarity index 84% rename from packages/server/src/db/linkedRows/LinkController.js rename to packages/server/src/db/linkedRows/LinkController.ts index df24b97e85..fc28137f58 100644 --- a/packages/server/src/db/linkedRows/LinkController.js +++ b/packages/server/src/db/linkedRows/LinkController.ts @@ -1,12 +1,32 @@ -const { IncludeDocs, getLinkDocuments } = require("./linkUtils") -const { InternalTables, getUserMetadataParams } = require("../utils") -const Sentry = require("@sentry/node") -const { FieldTypes, RelationshipTypes } = require("../../constants") -const { context } = require("@budibase/backend-core") -const LinkDocument = require("./LinkDocument") +import { IncludeDocs, getLinkDocuments } from "./linkUtils" +import { InternalTables, getUserMetadataParams } from "../utils" +import Sentry from "@sentry/node" +import { FieldTypes, RelationshipTypes } from "../../constants" +import { context } from "@budibase/backend-core" +import LinkDocument from "./LinkDocument" +import { + Database, + FieldSchema, + LinkDocumentValue, + Row, + Table, +} from "@budibase/types" + +type LinkControllerOpts = { + tableId: string + row?: Row + table?: Table + oldTable?: Table +} class LinkController { - constructor({ tableId, row, table, oldTable }) { + _db: Database + _tableId: string + _row?: Row + _table?: Table + _oldTable?: Table + + constructor({ tableId, row, table, oldTable }: LinkControllerOpts) { this._db = context.getAppDB() this._tableId = tableId this._row = row @@ -24,7 +44,7 @@ class LinkController { this._table = this._table == null ? await this._db.get(this._tableId) : this._table } - return this._table + return this._table! } /** @@ -34,7 +54,7 @@ class LinkController { * @returns {Promise} True if there are any linked fields, otherwise it will return * false. */ - async doesTableHaveLinkedFields(table = null) { + async doesTableHaveLinkedFields(table?: Table) { if (table == null) { table = await this.table() } @@ -50,7 +70,7 @@ class LinkController { /** * Utility function for main getLinkDocuments function - refer to it for functionality. */ - getRowLinkDocs(rowId) { + getRowLinkDocs(rowId: string) { return getLinkDocuments({ tableId: this._tableId, rowId, @@ -61,23 +81,23 @@ class LinkController { /** * Utility function for main getLinkDocuments function - refer to it for functionality. */ - getTableLinkDocs() { - return getLinkDocuments({ + async getTableLinkDocs() { + return (await getLinkDocuments({ tableId: this._tableId, includeDocs: IncludeDocs.INCLUDE, - }) + })) as LinkDocument[] } /** * Makes sure the passed in table schema contains valid relationship structures. */ - validateTable(table) { + validateTable(table: Table) { const usedAlready = [] for (let schema of Object.values(table.schema)) { if (schema.type !== FieldTypes.LINK) { continue } - const unique = schema.tableId + schema.fieldName + const unique = schema.tableId! + schema?.fieldName if (usedAlready.indexOf(unique) !== -1) { throw new Error( "Cannot re-use the linked column name for a linked table." @@ -90,7 +110,7 @@ class LinkController { /** * Returns whether the two link schemas are equal (in the important parts, not a pure equality check) */ - areLinkSchemasEqual(linkSchema1, linkSchema2) { + areLinkSchemasEqual(linkSchema1: FieldSchema, linkSchema2: FieldSchema) { const compareFields = [ "name", "type", @@ -100,6 +120,7 @@ class LinkController { "relationshipType", ] for (let field of compareFields) { + // @ts-ignore if (linkSchema1[field] !== linkSchema2[field]) { return false } @@ -111,7 +132,7 @@ class LinkController { * Given the link field of this table, and the link field of the linked table, this makes sure * the state of relationship type is accurate on both. */ - handleRelationshipType(linkerField, linkedField) { + handleRelationshipType(linkerField: FieldSchema, linkedField: FieldSchema) { if ( !linkerField.relationshipType || linkerField.relationshipType === RelationshipTypes.MANY_TO_MANY @@ -138,10 +159,10 @@ class LinkController { */ async rowSaved() { const table = await this.table() - const row = this._row + const row = this._row! const operations = [] // get link docs to compare against - const linkDocs = await this.getRowLinkDocs(row._id) + const linkDocs = (await this.getRowLinkDocs(row._id!)) as LinkDocument[] for (let fieldName of Object.keys(table.schema)) { // get the links this row wants to make const rowField = row[fieldName] @@ -161,30 +182,32 @@ class LinkController { // if 1:N, ensure that this ID is not already attached to another record const linkedTable = await this._db.get(field.tableId) - const linkedSchema = linkedTable.schema[field.fieldName] + const linkedSchema = linkedTable.schema[field.fieldName!] // We need to map the global users to metadata in each app for relationships if (field.tableId === InternalTables.USER_METADATA) { const users = await this._db.allDocs(getUserMetadataParams(null, {})) const metadataRequired = rowField.filter( - userId => !users.rows.some(user => user.id === userId) + (userId: string) => !users.rows.some(user => user.id === userId) ) // ensure non-existing user metadata is created in the app DB await this._db.bulkDocs( - metadataRequired.map(userId => ({ _id: userId })) + metadataRequired.map((userId: string) => ({ _id: userId })) ) } // iterate through the link IDs in the row field, see if any don't exist already for (let linkId of rowField) { - if (linkedSchema.relationshipType === RelationshipTypes.ONE_TO_MANY) { + if ( + linkedSchema?.relationshipType === RelationshipTypes.ONE_TO_MANY + ) { let links = ( - await getLinkDocuments({ + (await getLinkDocuments({ tableId: field.tableId, rowId: linkId, includeDocs: IncludeDocs.EXCLUDE, - }) + })) as LinkDocumentValue[] ).filter( link => link.id !== row._id && link.fieldName === linkedSchema.name @@ -209,11 +232,11 @@ class LinkController { } operations.push( new LinkDocument( - table._id, + table._id!, fieldName, - row._id, - field.tableId, - field.fieldName, + row._id!, + field.tableId!, + field.fieldName!, linkId ) ) @@ -246,9 +269,9 @@ class LinkController { * be accurate. This also returns the row that was deleted. */ async rowDeleted() { - const row = this._row + const row = this._row! // need to get the full link docs to be be able to delete it - const linkDocs = await this.getRowLinkDocs(row._id) + const linkDocs = await this.getRowLinkDocs(row._id!) if (linkDocs.length === 0) { return null } @@ -267,13 +290,13 @@ class LinkController { * @param {string} fieldName The field to be removed from the table. * @returns {Promise} The table has now been updated. */ - async removeFieldFromTable(fieldName) { + async removeFieldFromTable(fieldName: string) { let oldTable = this._oldTable - let field = oldTable.schema[fieldName] + let field = oldTable?.schema[fieldName] as FieldSchema const linkDocs = await this.getTableLinkDocs() let toDelete = linkDocs.filter(linkDoc => { let correctFieldName = - linkDoc.doc1.tableId === oldTable._id + linkDoc.doc1.tableId === oldTable?._id ? linkDoc.doc1.fieldName : linkDoc.doc2.fieldName return correctFieldName === fieldName @@ -288,7 +311,9 @@ class LinkController { ) // remove schema from other table let linkedTable = await this._db.get(field.tableId) - delete linkedTable.schema[field.fieldName] + if (field.fieldName) { + delete linkedTable.schema[field.fieldName] + } await this._db.put(linkedTable) } @@ -305,7 +330,7 @@ class LinkController { const schema = table.schema for (let fieldName of Object.keys(schema)) { const field = schema[fieldName] - if (field.type === FieldTypes.LINK) { + if (field.type === FieldTypes.LINK && field.fieldName) { // handle this in a separate try catch, want // the put to bubble up as an error, if can't update // table for some reason @@ -362,8 +387,8 @@ class LinkController { const oldTable = this._oldTable // first start by checking if any link columns have been deleted const newTable = await this.table() - for (let fieldName of Object.keys(oldTable.schema)) { - const field = oldTable.schema[fieldName] + for (let fieldName of Object.keys(oldTable?.schema || {})) { + const field = oldTable?.schema[fieldName] as FieldSchema // this field has been removed from the table schema if ( field.type === FieldTypes.LINK && @@ -389,7 +414,7 @@ class LinkController { for (let fieldName of Object.keys(schema)) { const field = schema[fieldName] try { - if (field.type === FieldTypes.LINK) { + if (field.type === FieldTypes.LINK && field.fieldName) { const linkedTable = await this._db.get(field.tableId) delete linkedTable.schema[field.fieldName] await this._db.put(linkedTable) @@ -416,4 +441,4 @@ class LinkController { } } -module.exports = LinkController +export = LinkController diff --git a/packages/server/src/db/linkedRows/LinkDocument.js b/packages/server/src/db/linkedRows/LinkDocument.js deleted file mode 100644 index 58aa630adc..0000000000 --- a/packages/server/src/db/linkedRows/LinkDocument.js +++ /dev/null @@ -1,47 +0,0 @@ -const { generateLinkID } = require("../utils") -const { FieldTypes } = require("../../constants") - -/** - * Creates a new link document structure which can be put to the database. It is important to - * note that while this talks about linker/linked the link is bi-directional and for all intent - * and purposes it does not matter from which direction the link was initiated. - * @param {string} tableId1 The ID of the first table (the linker). - * @param {string} tableId2 The ID of the second table (the linked). - * @param {string} fieldName1 The name of the field in the linker table. - * @param {string} fieldName2 The name of the field in the linked table. - * @param {string} rowId1 The ID of the row which is acting as the linker. - * @param {string} rowId2 The ID of the row which is acting as the linked. - * @constructor - */ -function LinkDocument( - tableId1, - fieldName1, - rowId1, - tableId2, - fieldName2, - rowId2 -) { - // build the ID out of unique references to this link document - this._id = generateLinkID( - tableId1, - tableId2, - rowId1, - rowId2, - fieldName1, - fieldName2 - ) - // required for referencing in view - this.type = FieldTypes.LINK - this.doc1 = { - tableId: tableId1, - fieldName: fieldName1, - rowId: rowId1, - } - this.doc2 = { - tableId: tableId2, - fieldName: fieldName2, - rowId: rowId2, - } -} - -module.exports = LinkDocument diff --git a/packages/server/src/db/linkedRows/LinkDocument.ts b/packages/server/src/db/linkedRows/LinkDocument.ts new file mode 100644 index 0000000000..d90f08e78c --- /dev/null +++ b/packages/server/src/db/linkedRows/LinkDocument.ts @@ -0,0 +1,60 @@ +import { generateLinkID } from "../utils" +import { FieldTypes } from "../../constants" +import { LinkDocument } from "@budibase/types" + +/** + * Creates a new link document structure which can be put to the database. It is important to + * note that while this talks about linker/linked the link is bi-directional and for all intent + * and purposes it does not matter from which direction the link was initiated. + * @param {string} tableId1 The ID of the first table (the linker). + * @param {string} tableId2 The ID of the second table (the linked). + * @param {string} fieldName1 The name of the field in the linker table. + * @param {string} fieldName2 The name of the field in the linked table. + * @param {string} rowId1 The ID of the row which is acting as the linker. + * @param {string} rowId2 The ID of the row which is acting as the linked. + * @constructor + */ +class LinkDocumentImpl implements LinkDocument { + _id: string + type: string + doc1: { + rowId: string + fieldName: string + tableId: string + } + doc2: { + rowId: string + fieldName: string + tableId: string + } + constructor( + tableId1: string, + fieldName1: string, + rowId1: string, + tableId2: string, + fieldName2: string, + rowId2: string + ) { + this._id = generateLinkID( + tableId1, + tableId2, + rowId1, + rowId2, + fieldName1, + fieldName2 + ) + this.type = FieldTypes.LINK + this.doc1 = { + tableId: tableId1, + fieldName: fieldName1, + rowId: rowId1, + } + this.doc2 = { + tableId: tableId2, + fieldName: fieldName2, + rowId: rowId2, + } + } +} + +export = LinkDocumentImpl diff --git a/packages/server/src/db/linkedRows/index.js b/packages/server/src/db/linkedRows/index.ts similarity index 80% rename from packages/server/src/db/linkedRows/index.js rename to packages/server/src/db/linkedRows/index.ts index 1ee98f6148..1559e644af 100644 --- a/packages/server/src/db/linkedRows/index.js +++ b/packages/server/src/db/linkedRows/index.ts @@ -1,5 +1,5 @@ -const LinkController = require("./LinkController") -const { +import LinkController from "./LinkController" +import { IncludeDocs, getLinkDocuments, createLinkView, @@ -7,21 +7,24 @@ const { getRelatedTableForField, getLinkedTableIDs, getLinkedTable, -} = require("./linkUtils") -const { flatten } = require("lodash") -const { FieldTypes } = require("../../constants") -const { getMultiIDParams, USER_METDATA_PREFIX } = require("../../db/utils") -const { partition } = require("lodash") -const { getGlobalUsersFromMetadata } = require("../../utilities/global") -const { processFormulas } = require("../../utilities/rowProcessor/utils") -const { context } = require("@budibase/backend-core") +} from "./linkUtils" +import { flatten } from "lodash" +import { FieldTypes } from "../../constants" +import { getMultiIDParams, USER_METDATA_PREFIX } from "../utils" +import { partition } from "lodash" +import { getGlobalUsersFromMetadata } from "../../utilities/global" +import { processFormulas } from "../../utilities/rowProcessor" +import { context } from "@budibase/backend-core" +import { Table, Row, LinkDocumentValue } from "@budibase/types" + +export { IncludeDocs, getLinkDocuments, createLinkView } from "./linkUtils" /** * This functionality makes sure that when rows with links are created, updated or deleted they are processed * correctly - making sure that no stale links are left around and that all links have been made successfully. */ -const EventType = { +export const EventType = { ROW_SAVE: "row:save", ROW_UPDATE: "row:update", ROW_DELETE: "row:delete", @@ -30,13 +33,7 @@ const EventType = { TABLE_DELETE: "table:delete", } -exports.EventType = EventType -// re-export search here for ease of use -exports.IncludeDocs = IncludeDocs -exports.getLinkDocuments = getLinkDocuments -exports.createLinkView = createLinkView - -function clearRelationshipFields(table, rows) { +function clearRelationshipFields(table: Table, rows: Row[]) { for (let [key, field] of Object.entries(table.schema)) { if (field.type === FieldTypes.LINK) { rows = rows.map(row => { @@ -48,18 +45,17 @@ function clearRelationshipFields(table, rows) { return rows } -async function getLinksForRows(rows) { +async function getLinksForRows(rows: Row[]) { const tableIds = [...new Set(rows.map(el => el.tableId))] // start by getting all the link values for performance reasons + const promises = tableIds.map(tableId => + getLinkDocuments({ + tableId: tableId, + includeDocs: IncludeDocs.EXCLUDE, + }) + ) const responses = flatten( - await Promise.all( - tableIds.map(tableId => - getLinkDocuments({ - tableId: tableId, - includeDocs: IncludeDocs.EXCLUDE, - }) - ) - ) + (await Promise.all(promises)) as LinkDocumentValue[][] ) // have to get unique as the previous table query can // return duplicates, could be querying for both tables in a relation @@ -72,7 +68,7 @@ async function getLinksForRows(rows) { ) } -async function getFullLinkedDocs(links) { +async function getFullLinkedDocs(links: LinkDocumentValue[]) { // create DBs const db = context.getAppDB() const linkedRowIds = links.map(link => link.id) @@ -103,12 +99,18 @@ async function getFullLinkedDocs(links) { * @returns {Promise} When the update is complete this will respond successfully. Returns the row for * row operations and the table for table operations. */ -exports.updateLinks = async function (args) { +export async function updateLinks(args: { + tableId: string + eventType: string + row?: Row + table?: Table + oldTable?: Table +}) { const { eventType, row, tableId, table, oldTable } = args const baseReturnObj = row == null ? table : row // make sure table ID is set if (tableId == null && table != null) { - args.tableId = table._id + args.tableId = table._id! } let linkController = new LinkController(args) try { @@ -146,7 +148,7 @@ exports.updateLinks = async function (args) { * @param {array} rows The rows which are to be enriched. * @return {Promise<*>} returns the rows with all of the enriched relationships on it. */ -exports.attachFullLinkedDocs = async (table, rows) => { +export async function attachFullLinkedDocs(table: Table, rows: Row[]) { const linkedTableIds = getLinkedTableIDs(table) if (linkedTableIds.length === 0) { return rows @@ -159,7 +161,7 @@ exports.attachFullLinkedDocs = async (table, rows) => { rows = clearRelationshipFields(table, rows) // now get the docs and combine into the rows let linked = await getFullLinkedDocs(links) - const linkedTables = [] + const linkedTables: Table[] = [] for (let row of rows) { for (let link of links.filter(link => link.thisId === row._id)) { if (row[link.fieldName] == null) { @@ -185,13 +187,16 @@ exports.attachFullLinkedDocs = async (table, rows) => { * @param {array} enriched The pre-enriched rows (full docs) which are to be squashed. * @returns {Promise} The rows after having their links squashed to only contain the ID and primary display. */ -exports.squashLinksToPrimaryDisplay = async (table, enriched) => { +export async function squashLinksToPrimaryDisplay( + table: Table, + enriched: Row[] +) { // will populate this as we find them const linkedTables = [table] for (let row of enriched) { // this only fetches the table if its not already in array - const rowTable = await getLinkedTable(row.tableId, linkedTables) - for (let [column, schema] of Object.entries(rowTable.schema)) { + const rowTable = await getLinkedTable(row.tableId!, linkedTables) + for (let [column, schema] of Object.entries(rowTable?.schema || {})) { if (schema.type !== FieldTypes.LINK || !Array.isArray(row[column])) { continue } @@ -199,8 +204,8 @@ exports.squashLinksToPrimaryDisplay = async (table, enriched) => { for (let link of row[column]) { const linkTblId = link.tableId || getRelatedTableForField(table, column) const linkedTable = await getLinkedTable(linkTblId, linkedTables) - const obj = { _id: link._id } - if (link[linkedTable.primaryDisplay]) { + const obj: any = { _id: link._id } + if (linkedTable?.primaryDisplay && link[linkedTable.primaryDisplay]) { obj.primaryDisplay = link[linkedTable.primaryDisplay] } newLinks.push(obj) diff --git a/packages/server/src/db/linkedRows/linkUtils.js b/packages/server/src/db/linkedRows/linkUtils.ts similarity index 70% rename from packages/server/src/db/linkedRows/linkUtils.js rename to packages/server/src/db/linkedRows/linkUtils.ts index 25a1b5fcf8..c7db7d522a 100644 --- a/packages/server/src/db/linkedRows/linkUtils.js +++ b/packages/server/src/db/linkedRows/linkUtils.ts @@ -1,20 +1,24 @@ -const Sentry = require("@sentry/node") -const { ViewName, getQueryIndex } = require("../utils") -const { FieldTypes } = require("../../constants") -const { createLinkView } = require("../views/staticViews") -const { context } = require("@budibase/backend-core") +import { ViewName, getQueryIndex } from "../utils" +import { FieldTypes } from "../../constants" +import { createLinkView } from "../views/staticViews" +import { context, logging } from "@budibase/backend-core" +import { + FieldSchema, + LinkDocument, + LinkDocumentValue, + Table, +} from "@budibase/types" +export { createLinkView } from "../views/staticViews" /** * Only needed so that boolean parameters are being used for includeDocs * @type {{EXCLUDE: boolean, INCLUDE: boolean}} */ -exports.IncludeDocs = { +export const IncludeDocs = { INCLUDE: true, EXCLUDE: false, } -exports.createLinkView = createLinkView - /** * Gets the linking documents, not the linked documents themselves. * @param {string} args.tableId The table which we are searching for linked rows against. @@ -28,10 +32,14 @@ exports.createLinkView = createLinkView * @returns {Promise} This will return an array of the linking documents that were found * (if any). */ -exports.getLinkDocuments = async function (args) { +export async function getLinkDocuments(args: { + tableId?: string + rowId?: string + includeDocs?: any +}): Promise { const { tableId, rowId, includeDocs } = args const db = context.getAppDB() - let params + let params: any if (rowId != null) { params = { key: [tableId, rowId] } } @@ -43,7 +51,7 @@ exports.getLinkDocuments = async function (args) { try { let linkRows = (await db.query(getQueryIndex(ViewName.LINK), params)).rows // filter to get unique entries - const foundIds = [] + const foundIds: string[] = [] linkRows = linkRows.filter(link => { // make sure anything unique is the correct key if ( @@ -60,35 +68,36 @@ exports.getLinkDocuments = async function (args) { }) if (includeDocs) { - return linkRows.map(row => row.doc) + return linkRows.map(row => row.doc) as LinkDocument[] } else { - return linkRows.map(row => row.value) + return linkRows.map(row => row.value) as LinkDocumentValue[] } - } catch (err) { + } catch (err: any) { // check if the view doesn't exist, it should for all new instances if (err != null && err.name === "not_found") { - await exports.createLinkView() - return exports.getLinkDocuments(arguments[0]) + await createLinkView() + return getLinkDocuments(arguments[0]) } else { /* istanbul ignore next */ - Sentry.captureException(err) + logging.logAlert("Failed to get link documents", err) + throw err } } } -exports.getUniqueByProp = (array, prop) => { +export function getUniqueByProp(array: any[], prop: string) { return array.filter((obj, pos, arr) => { return arr.map(mapObj => mapObj[prop]).indexOf(obj[prop]) === pos }) } -exports.getLinkedTableIDs = table => { +export function getLinkedTableIDs(table: Table) { return Object.values(table.schema) - .filter(column => column.type === FieldTypes.LINK) + .filter((column: FieldSchema) => column.type === FieldTypes.LINK) .map(column => column.tableId) } -exports.getLinkedTable = async (id, tables) => { +export async function getLinkedTable(id: string, tables: Table[]) { const db = context.getAppDB() let linkedTable = tables.find(table => table._id === id) if (linkedTable) { @@ -101,7 +110,7 @@ exports.getLinkedTable = async (id, tables) => { return linkedTable } -exports.getRelatedTableForField = (table, fieldName) => { +export function getRelatedTableForField(table: Table, fieldName: string) { // look to see if its on the table, straight in the schema const field = table.schema[fieldName] if (field != null) { diff --git a/packages/types/src/documents/app/links.ts b/packages/types/src/documents/app/links.ts index b27d36e0c6..d6b2adddf8 100644 --- a/packages/types/src/documents/app/links.ts +++ b/packages/types/src/documents/app/links.ts @@ -1,4 +1,6 @@ -export interface LinkDocument { +import { Document } from "../document" + +export interface LinkDocument extends Document { type: string doc1: { rowId: string @@ -11,3 +13,9 @@ export interface LinkDocument { tableId: string } } + +export interface LinkDocumentValue { + id: string + thisId: string + fieldName: string +}