2020-09-28 18:36:59 +02:00
|
|
|
const CouchDB = require("../index")
|
2020-09-30 18:52:57 +02:00
|
|
|
const { IncludeDocs, getLinkDocuments } = require("./linkUtils")
|
2020-10-02 17:49:36 +02:00
|
|
|
const { generateLinkID } = require("../utils")
|
2020-10-16 14:48:59 +02:00
|
|
|
const Sentry = require("@sentry/node")
|
2021-02-10 18:55:19 +01:00
|
|
|
const { FieldTypes } = require("../../constants")
|
2020-09-28 18:36:59 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
2020-10-09 19:49:23 +02:00
|
|
|
* @param {string} tableId1 The ID of the first table (the linker).
|
|
|
|
* @param {string} tableId2 The ID of the second table (the linked).
|
2020-09-28 18:36:59 +02:00
|
|
|
* @param {string} fieldName1 The name of the field in the linker table.
|
|
|
|
* @param {string} fieldName2 The name of the field in the linked table.
|
2020-10-09 20:10:28 +02:00
|
|
|
* @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.
|
2020-09-28 18:36:59 +02:00
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
function LinkDocument(
|
2020-10-09 19:49:23 +02:00
|
|
|
tableId1,
|
2020-09-28 18:36:59 +02:00
|
|
|
fieldName1,
|
2020-10-09 20:10:28 +02:00
|
|
|
rowId1,
|
2020-10-09 19:49:23 +02:00
|
|
|
tableId2,
|
2020-09-28 18:36:59 +02:00
|
|
|
fieldName2,
|
2020-10-09 20:10:28 +02:00
|
|
|
rowId2
|
2020-09-28 18:36:59 +02:00
|
|
|
) {
|
2020-09-29 12:55:18 +02:00
|
|
|
// build the ID out of unique references to this link document
|
2020-10-09 20:10:28 +02:00
|
|
|
this._id = generateLinkID(tableId1, tableId2, rowId1, rowId2)
|
2020-09-29 12:55:18 +02:00
|
|
|
// required for referencing in view
|
2021-02-10 18:55:19 +01:00
|
|
|
this.type = FieldTypes.LINK
|
2020-09-28 18:36:59 +02:00
|
|
|
this.doc1 = {
|
2020-10-09 19:49:23 +02:00
|
|
|
tableId: tableId1,
|
2020-09-28 18:36:59 +02:00
|
|
|
fieldName: fieldName1,
|
2020-10-09 20:10:28 +02:00
|
|
|
rowId: rowId1,
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
this.doc2 = {
|
2020-10-09 19:49:23 +02:00
|
|
|
tableId: tableId2,
|
2020-09-28 18:36:59 +02:00
|
|
|
fieldName: fieldName2,
|
2020-10-09 20:10:28 +02:00
|
|
|
rowId: rowId2,
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class LinkController {
|
2020-10-29 11:28:27 +01:00
|
|
|
constructor({ appId, tableId, row, table, oldTable }) {
|
|
|
|
this._appId = appId
|
|
|
|
this._db = new CouchDB(appId)
|
2020-10-09 19:49:23 +02:00
|
|
|
this._tableId = tableId
|
2020-10-09 20:10:28 +02:00
|
|
|
this._row = row
|
2020-10-09 19:49:23 +02:00
|
|
|
this._table = table
|
|
|
|
this._oldTable = oldTable
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-10-09 19:49:23 +02:00
|
|
|
* Retrieves the table, if it was not already found in the eventData.
|
|
|
|
* @returns {Promise<object>} This will return a table based on the event data, either
|
|
|
|
* if it was in the event already, or it uses the specified tableId to get it.
|
2020-09-28 18:36:59 +02:00
|
|
|
*/
|
2020-10-09 19:49:23 +02:00
|
|
|
async table() {
|
|
|
|
if (this._table == null) {
|
|
|
|
this._table =
|
|
|
|
this._table == null ? await this._db.get(this._tableId) : this._table
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
2020-10-09 19:49:23 +02:00
|
|
|
return this._table
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-10-09 19:49:23 +02:00
|
|
|
* Checks if the table this was constructed with has any linking columns currently.
|
|
|
|
* If the table has not been retrieved this will retrieve it based on the eventData.
|
|
|
|
* @params {object|null} table If a table that is not known to the link controller is to be tested.
|
2020-09-28 18:36:59 +02:00
|
|
|
* @returns {Promise<boolean>} True if there are any linked fields, otherwise it will return
|
|
|
|
* false.
|
|
|
|
*/
|
2020-10-09 19:49:23 +02:00
|
|
|
async doesTableHaveLinkedFields(table = null) {
|
|
|
|
if (table == null) {
|
|
|
|
table = await this.table()
|
2020-09-30 18:52:57 +02:00
|
|
|
}
|
2020-10-09 19:49:23 +02:00
|
|
|
for (let fieldName of Object.keys(table.schema)) {
|
|
|
|
const { type } = table.schema[fieldName]
|
2021-02-10 18:55:19 +01:00
|
|
|
if (type === FieldTypes.LINK) {
|
2020-09-28 18:36:59 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility function for main getLinkDocuments function - refer to it for functionality.
|
|
|
|
*/
|
2020-10-09 20:10:28 +02:00
|
|
|
getRowLinkDocs(rowId) {
|
2020-09-30 18:52:57 +02:00
|
|
|
return getLinkDocuments({
|
2020-10-29 11:28:27 +01:00
|
|
|
appId: this._appId,
|
2020-10-09 19:49:23 +02:00
|
|
|
tableId: this._tableId,
|
2020-10-09 20:10:28 +02:00
|
|
|
rowId,
|
2020-10-01 13:30:14 +02:00
|
|
|
includeDocs: IncludeDocs.INCLUDE,
|
2020-09-30 18:52:57 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility function for main getLinkDocuments function - refer to it for functionality.
|
|
|
|
*/
|
2020-10-09 19:49:23 +02:00
|
|
|
getTableLinkDocs() {
|
2020-09-30 18:52:57 +02:00
|
|
|
return getLinkDocuments({
|
2020-10-29 11:28:27 +01:00
|
|
|
appId: this._appId,
|
2020-10-09 19:49:23 +02:00
|
|
|
tableId: this._tableId,
|
2020-10-01 13:30:14 +02:00
|
|
|
includeDocs: IncludeDocs.INCLUDE,
|
2020-09-28 18:36:59 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-10-09 19:49:23 +02:00
|
|
|
// all operations here will assume that the table
|
2020-10-09 20:10:28 +02:00
|
|
|
// this operation is related to has linked rows
|
2020-09-28 18:36:59 +02:00
|
|
|
/**
|
2020-10-09 20:10:28 +02:00
|
|
|
* When a row is saved this will carry out the necessary operations to make sure
|
2020-09-28 18:36:59 +02:00
|
|
|
* the link has been created/updated.
|
2020-10-09 20:10:28 +02:00
|
|
|
* @returns {Promise<object>} returns the row that has been cleaned and prepared to be written to the DB - links
|
2020-09-29 17:40:59 +02:00
|
|
|
* have also been created.
|
2020-09-28 18:36:59 +02:00
|
|
|
*/
|
2020-10-09 20:10:28 +02:00
|
|
|
async rowSaved() {
|
2020-10-09 19:49:23 +02:00
|
|
|
const table = await this.table()
|
2020-10-09 20:10:28 +02:00
|
|
|
const row = this._row
|
2020-09-29 17:40:59 +02:00
|
|
|
const operations = []
|
2020-09-30 16:37:38 +02:00
|
|
|
// get link docs to compare against
|
2020-10-09 20:10:28 +02:00
|
|
|
const linkDocs = await this.getRowLinkDocs(row._id)
|
2020-10-09 19:49:23 +02:00
|
|
|
for (let fieldName of Object.keys(table.schema)) {
|
2020-10-09 20:10:28 +02:00
|
|
|
// get the links this row wants to make
|
|
|
|
const rowField = row[fieldName]
|
2020-10-09 19:49:23 +02:00
|
|
|
const field = table.schema[fieldName]
|
2021-02-10 18:55:19 +01:00
|
|
|
if (field.type === FieldTypes.LINK && rowField != null) {
|
2020-10-09 20:10:28 +02:00
|
|
|
// check which links actual pertain to the update in this row
|
2020-10-01 13:30:14 +02:00
|
|
|
const thisFieldLinkDocs = linkDocs.filter(
|
|
|
|
linkDoc =>
|
|
|
|
linkDoc.doc1.fieldName === fieldName ||
|
|
|
|
linkDoc.doc2.fieldName === fieldName
|
2020-09-30 16:37:38 +02:00
|
|
|
)
|
2020-10-01 13:30:14 +02:00
|
|
|
const linkDocIds = thisFieldLinkDocs.map(linkDoc => {
|
2020-10-09 20:10:28 +02:00
|
|
|
return linkDoc.doc1.rowId === row._id
|
|
|
|
? linkDoc.doc2.rowId
|
|
|
|
: linkDoc.doc1.rowId
|
2020-10-01 13:30:14 +02:00
|
|
|
})
|
2020-10-09 20:10:28 +02:00
|
|
|
// iterate through the link IDs in the row field, see if any don't exist already
|
|
|
|
for (let linkId of rowField) {
|
2020-09-30 13:00:56 +02:00
|
|
|
if (linkId && linkId !== "" && linkDocIds.indexOf(linkId) === -1) {
|
2021-02-09 14:18:59 +01:00
|
|
|
// first check the doc we're linking to exists
|
|
|
|
try {
|
2021-02-09 15:50:02 +01:00
|
|
|
await this._db.get(linkId)
|
2021-02-09 14:18:59 +01:00
|
|
|
} catch (err) {
|
|
|
|
// skip links that don't exist
|
|
|
|
continue
|
|
|
|
}
|
2020-09-28 18:36:59 +02:00
|
|
|
operations.push(
|
|
|
|
new LinkDocument(
|
2020-10-09 19:49:23 +02:00
|
|
|
table._id,
|
2020-09-28 18:36:59 +02:00
|
|
|
fieldName,
|
2020-10-09 20:10:28 +02:00
|
|
|
row._id,
|
2020-10-09 19:49:23 +02:00
|
|
|
field.tableId,
|
2020-09-28 18:36:59 +02:00
|
|
|
field.fieldName,
|
|
|
|
linkId
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2020-10-01 13:30:14 +02:00
|
|
|
// find the docs that need to be deleted
|
|
|
|
let toDeleteDocs = thisFieldLinkDocs
|
|
|
|
.filter(doc => {
|
|
|
|
let correctDoc =
|
|
|
|
doc.doc1.fieldName === fieldName ? doc.doc2 : doc.doc1
|
2020-10-09 20:10:28 +02:00
|
|
|
return rowField.indexOf(correctDoc.rowId) === -1
|
2020-10-01 13:30:14 +02:00
|
|
|
})
|
|
|
|
.map(doc => {
|
|
|
|
return { ...doc, _deleted: true }
|
|
|
|
})
|
|
|
|
// now add the docs to be deleted to the bulk operation
|
|
|
|
operations.push(...toDeleteDocs)
|
2020-10-12 18:45:11 +02:00
|
|
|
// remove the field from this row, link doc will be added to row on way out
|
2020-10-09 20:10:28 +02:00
|
|
|
delete row[fieldName]
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
await this._db.bulkDocs(operations)
|
2020-10-09 20:10:28 +02:00
|
|
|
return row
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-10-09 20:10:28 +02:00
|
|
|
* When a row is deleted this will carry out the necessary operations to make sure
|
2020-09-28 18:36:59 +02:00
|
|
|
* any links that existed have been removed.
|
2020-09-29 17:40:59 +02:00
|
|
|
* @returns {Promise<object>} The operation has been completed and the link documents should now
|
2020-10-09 20:10:28 +02:00
|
|
|
* be accurate. This also returns the row that was deleted.
|
2020-09-28 18:36:59 +02:00
|
|
|
*/
|
2020-10-09 20:10:28 +02:00
|
|
|
async rowDeleted() {
|
|
|
|
const row = this._row
|
2020-09-29 17:40:59 +02:00
|
|
|
// need to get the full link docs to be be able to delete it
|
2020-10-09 20:10:28 +02:00
|
|
|
const linkDocs = await this.getRowLinkDocs(row._id)
|
2020-09-30 18:52:57 +02:00
|
|
|
if (linkDocs.length === 0) {
|
2020-09-29 12:55:18 +02:00
|
|
|
return null
|
|
|
|
}
|
2020-09-30 18:52:57 +02:00
|
|
|
const toDelete = linkDocs.map(doc => {
|
2020-09-28 18:36:59 +02:00
|
|
|
return {
|
2020-09-30 18:52:57 +02:00
|
|
|
...doc,
|
2020-09-28 18:36:59 +02:00
|
|
|
_deleted: true,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
await this._db.bulkDocs(toDelete)
|
2020-10-09 20:10:28 +02:00
|
|
|
return row
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
|
2020-10-01 00:14:39 +02:00
|
|
|
/**
|
2020-10-09 20:10:28 +02:00
|
|
|
* Remove a field from a table as well as any linked rows that pertained to it.
|
2020-10-09 19:49:23 +02:00
|
|
|
* @param {string} fieldName The field to be removed from the table.
|
|
|
|
* @returns {Promise<void>} The table has now been updated.
|
2020-10-01 00:14:39 +02:00
|
|
|
*/
|
2020-10-09 19:49:23 +02:00
|
|
|
async removeFieldFromTable(fieldName) {
|
|
|
|
let oldTable = this._oldTable
|
|
|
|
let field = oldTable.schema[fieldName]
|
|
|
|
const linkDocs = await this.getTableLinkDocs()
|
2020-10-01 00:14:39 +02:00
|
|
|
let toDelete = linkDocs.filter(linkDoc => {
|
|
|
|
let correctFieldName =
|
2020-10-09 19:49:23 +02:00
|
|
|
linkDoc.doc1.tableId === oldTable._id
|
2020-10-01 00:14:39 +02:00
|
|
|
? linkDoc.doc1.fieldName
|
|
|
|
: linkDoc.doc2.fieldName
|
|
|
|
return correctFieldName === fieldName
|
|
|
|
})
|
|
|
|
await this._db.bulkDocs(
|
|
|
|
toDelete.map(doc => {
|
|
|
|
return {
|
|
|
|
...doc,
|
|
|
|
_deleted: true,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
)
|
2020-10-09 19:49:23 +02:00
|
|
|
// remove schema from other table
|
|
|
|
let linkedTable = await this._db.get(field.tableId)
|
|
|
|
delete linkedTable.schema[field.fieldName]
|
|
|
|
this._db.put(linkedTable)
|
2020-10-01 00:14:39 +02:00
|
|
|
}
|
|
|
|
|
2020-09-28 18:36:59 +02:00
|
|
|
/**
|
2020-10-09 19:49:23 +02:00
|
|
|
* When a table is saved this will carry out the necessary operations to make sure
|
|
|
|
* any linked tables are notified and updated correctly.
|
2020-09-29 17:40:59 +02:00
|
|
|
* @returns {Promise<object>} The operation has been completed and the link documents should now
|
2020-10-09 19:49:23 +02:00
|
|
|
* be accurate. Also returns the table that was operated on.
|
2020-09-28 18:36:59 +02:00
|
|
|
*/
|
2020-10-09 19:49:23 +02:00
|
|
|
async tableSaved() {
|
|
|
|
const table = await this.table()
|
|
|
|
const schema = table.schema
|
2020-09-29 17:40:59 +02:00
|
|
|
for (let fieldName of Object.keys(schema)) {
|
2020-09-28 18:36:59 +02:00
|
|
|
const field = schema[fieldName]
|
2021-02-10 18:55:19 +01:00
|
|
|
if (field.type === FieldTypes.LINK) {
|
2020-10-12 18:02:52 +02:00
|
|
|
// handle this in a separate try catch, want
|
|
|
|
// the put to bubble up as an error, if can't update
|
|
|
|
// table for some reason
|
2020-10-12 18:45:11 +02:00
|
|
|
let linkedTable
|
2020-10-12 18:02:52 +02:00
|
|
|
try {
|
2020-10-12 18:45:11 +02:00
|
|
|
linkedTable = await this._db.get(field.tableId)
|
2020-10-12 18:02:52 +02:00
|
|
|
} catch (err) {
|
|
|
|
continue
|
|
|
|
}
|
2020-10-09 19:49:23 +02:00
|
|
|
// create the link field in the other table
|
|
|
|
linkedTable.schema[field.fieldName] = {
|
2020-09-29 17:40:59 +02:00
|
|
|
name: field.fieldName,
|
2021-02-10 18:55:19 +01:00
|
|
|
type: FieldTypes.LINK,
|
2020-09-29 17:40:59 +02:00
|
|
|
// these are the props of the table that initiated the link
|
2020-10-09 19:49:23 +02:00
|
|
|
tableId: table._id,
|
2020-09-28 18:36:59 +02:00
|
|
|
fieldName: fieldName,
|
|
|
|
}
|
2021-02-02 12:46:10 +01:00
|
|
|
const response = await this._db.put(linkedTable)
|
|
|
|
// special case for when linking back to self, make sure rev updated
|
|
|
|
if (linkedTable._id === table._id) {
|
|
|
|
table._rev = response.rev
|
|
|
|
}
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
}
|
2020-10-09 19:49:23 +02:00
|
|
|
return table
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
|
2020-10-01 00:14:39 +02:00
|
|
|
/**
|
2020-10-09 19:49:23 +02:00
|
|
|
* Update a table, this means if a field is removed need to handle removing from other table and removing
|
2020-10-01 00:14:39 +02:00
|
|
|
* any link docs that pertained to it.
|
2020-10-09 19:49:23 +02:00
|
|
|
* @returns {Promise<Object>} The table which has been saved, same response as with the tableSaved function.
|
2020-10-01 00:14:39 +02:00
|
|
|
*/
|
2020-10-09 19:49:23 +02:00
|
|
|
async tableUpdated() {
|
|
|
|
const oldTable = this._oldTable
|
2020-09-30 18:52:57 +02:00
|
|
|
// first start by checking if any link columns have been deleted
|
2020-10-09 19:49:23 +02:00
|
|
|
const newTable = await this.table()
|
|
|
|
for (let fieldName of Object.keys(oldTable.schema)) {
|
|
|
|
const field = oldTable.schema[fieldName]
|
|
|
|
// this field has been removed from the table schema
|
2021-02-10 18:55:19 +01:00
|
|
|
if (
|
|
|
|
field.type === FieldTypes.LINK &&
|
|
|
|
newTable.schema[fieldName] == null
|
|
|
|
) {
|
2020-10-09 19:49:23 +02:00
|
|
|
await this.removeFieldFromTable(fieldName)
|
2020-09-30 18:52:57 +02:00
|
|
|
}
|
|
|
|
}
|
2020-10-01 00:14:39 +02:00
|
|
|
// now handle as if its a new save
|
2020-10-09 19:49:23 +02:00
|
|
|
return this.tableSaved()
|
2020-09-30 18:52:57 +02:00
|
|
|
}
|
|
|
|
|
2020-09-28 18:36:59 +02:00
|
|
|
/**
|
2020-10-09 19:49:23 +02:00
|
|
|
* When a table is deleted this will carry out the necessary operations to make sure
|
|
|
|
* any linked tables have the joining column correctly removed as well as removing any
|
2020-09-28 18:36:59 +02:00
|
|
|
* now stale linking documents.
|
2020-09-29 17:40:59 +02:00
|
|
|
* @returns {Promise<object>} The operation has been completed and the link documents should now
|
2020-10-09 19:49:23 +02:00
|
|
|
* be accurate. Also returns the table that was operated on.
|
2020-09-28 18:36:59 +02:00
|
|
|
*/
|
2020-10-09 19:49:23 +02:00
|
|
|
async tableDeleted() {
|
|
|
|
const table = await this.table()
|
|
|
|
const schema = table.schema
|
2020-09-29 17:40:59 +02:00
|
|
|
for (let fieldName of Object.keys(schema)) {
|
|
|
|
const field = schema[fieldName]
|
2020-10-16 14:48:59 +02:00
|
|
|
try {
|
2021-02-10 18:55:19 +01:00
|
|
|
if (field.type === FieldTypes.LINK) {
|
2020-10-16 14:48:59 +02:00
|
|
|
const linkedTable = await this._db.get(field.tableId)
|
|
|
|
delete linkedTable.schema[field.fieldName]
|
|
|
|
await this._db.put(linkedTable)
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
Sentry.captureException(err)
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
}
|
2020-09-29 17:40:59 +02:00
|
|
|
// need to get the full link docs to delete them
|
2020-10-09 19:49:23 +02:00
|
|
|
const linkDocs = await this.getTableLinkDocs()
|
2020-09-30 18:52:57 +02:00
|
|
|
if (linkDocs.length === 0) {
|
2020-09-29 12:55:18 +02:00
|
|
|
return null
|
|
|
|
}
|
2020-10-09 19:49:23 +02:00
|
|
|
// get link docs for this table and configure for deletion
|
2020-09-30 18:52:57 +02:00
|
|
|
const toDelete = linkDocs.map(doc => {
|
2020-09-28 18:36:59 +02:00
|
|
|
return {
|
2020-09-30 18:52:57 +02:00
|
|
|
...doc,
|
2020-09-28 18:36:59 +02:00
|
|
|
_deleted: true,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
await this._db.bulkDocs(toDelete)
|
2020-10-09 19:49:23 +02:00
|
|
|
return table
|
2020-09-28 18:36:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = LinkController
|