2021-02-22 12:39:58 +01:00
|
|
|
const CouchDB = require("../../../db")
|
|
|
|
const csvParser = require("../../../utilities/csvParser")
|
2021-04-09 18:33:21 +02:00
|
|
|
const {
|
|
|
|
getRowParams,
|
|
|
|
generateRowID,
|
|
|
|
InternalTables,
|
|
|
|
} = require("../../../db/utils")
|
2021-02-22 12:39:58 +01:00
|
|
|
const { isEqual } = require("lodash/fp")
|
2021-05-18 23:14:27 +02:00
|
|
|
const { AutoFieldSubTypes, FieldTypes } = require("../../../constants")
|
2021-02-22 12:39:58 +01:00
|
|
|
const { inputProcessing } = require("../../../utilities/rowProcessor")
|
|
|
|
const { USERS_TABLE_SCHEMA } = require("../../../constants")
|
2021-10-19 18:00:54 +02:00
|
|
|
const {
|
|
|
|
isExternalTable,
|
|
|
|
breakExternalTableId,
|
|
|
|
} = require("../../../integrations/utils")
|
2021-10-20 21:01:49 +02:00
|
|
|
const { getViews, saveView } = require("../view/utils")
|
|
|
|
const viewTemplate = require("../view/viewBuilder")
|
2021-02-22 12:39:58 +01:00
|
|
|
|
|
|
|
exports.checkForColumnUpdates = async (db, oldTable, updatedTable) => {
|
2021-02-22 13:05:59 +01:00
|
|
|
let updatedRows = []
|
2021-02-22 12:39:58 +01:00
|
|
|
const rename = updatedTable._rename
|
|
|
|
let deletedColumns = []
|
|
|
|
if (oldTable && oldTable.schema && updatedTable.schema) {
|
|
|
|
deletedColumns = Object.keys(oldTable.schema).filter(
|
2021-05-04 12:32:22 +02:00
|
|
|
colName => updatedTable.schema[colName] == null
|
2021-02-22 12:39:58 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
// check for renaming of columns or deleted columns
|
|
|
|
if (rename || deletedColumns.length !== 0) {
|
2021-10-20 21:01:49 +02:00
|
|
|
// Update all rows
|
2021-02-22 12:39:58 +01:00
|
|
|
const rows = await db.allDocs(
|
|
|
|
getRowParams(updatedTable._id, null, {
|
|
|
|
include_docs: true,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
updatedRows = rows.rows.map(({ doc }) => {
|
|
|
|
if (rename) {
|
|
|
|
doc[rename.updated] = doc[rename.old]
|
|
|
|
delete doc[rename.old]
|
|
|
|
} else if (deletedColumns.length !== 0) {
|
2021-05-04 12:32:22 +02:00
|
|
|
deletedColumns.forEach(colName => delete doc[colName])
|
2021-02-22 12:39:58 +01:00
|
|
|
}
|
|
|
|
return doc
|
|
|
|
})
|
2021-10-20 21:01:49 +02:00
|
|
|
|
|
|
|
// Update views
|
|
|
|
await exports.checkForViewUpdates(db, updatedTable, rename, deletedColumns)
|
2021-02-22 12:39:58 +01:00
|
|
|
delete updatedTable._rename
|
|
|
|
}
|
|
|
|
return { rows: updatedRows, table: updatedTable }
|
|
|
|
}
|
|
|
|
|
|
|
|
// makes sure the passed in table isn't going to reset the auto ID
|
|
|
|
exports.makeSureTableUpToDate = (table, tableToSave) => {
|
|
|
|
if (!table) {
|
|
|
|
return tableToSave
|
|
|
|
}
|
|
|
|
// sure sure rev is up to date
|
|
|
|
tableToSave._rev = table._rev
|
|
|
|
// make sure auto IDs are always updated - these are internal
|
|
|
|
// so the client may not know they have changed
|
|
|
|
for (let [field, column] of Object.entries(table.schema)) {
|
|
|
|
if (
|
|
|
|
column.autocolumn &&
|
|
|
|
column.subtype === AutoFieldSubTypes.AUTO_ID &&
|
|
|
|
tableToSave.schema[field]
|
|
|
|
) {
|
|
|
|
tableToSave.schema[field].lastID = column.lastID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return tableToSave
|
|
|
|
}
|
|
|
|
|
2021-03-29 18:32:05 +02:00
|
|
|
exports.handleDataImport = async (appId, user, table, dataImport) => {
|
|
|
|
const db = new CouchDB(appId)
|
2021-02-22 12:39:58 +01:00
|
|
|
if (dataImport && dataImport.csvString) {
|
|
|
|
// Populate the table with rows imported from CSV in a bulk update
|
|
|
|
const data = await csvParser.transform(dataImport)
|
|
|
|
|
2021-09-21 16:59:50 +02:00
|
|
|
let finalData = []
|
2021-02-22 12:39:58 +01:00
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
let row = data[i]
|
|
|
|
row._id = generateRowID(table._id)
|
|
|
|
row.tableId = table._id
|
2021-09-21 16:59:50 +02:00
|
|
|
const processed = inputProcessing(user, table, row, {
|
|
|
|
noAutoRelationships: true,
|
|
|
|
})
|
2021-05-18 23:14:27 +02:00
|
|
|
table = processed.table
|
2021-02-22 12:39:58 +01:00
|
|
|
row = processed.row
|
2021-08-26 15:13:30 +02:00
|
|
|
|
2021-02-22 12:39:58 +01:00
|
|
|
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
2021-05-18 23:14:27 +02:00
|
|
|
// check whether the options need to be updated for inclusion as part of the data import
|
2021-02-22 12:39:58 +01:00
|
|
|
if (
|
2021-05-18 23:14:27 +02:00
|
|
|
schema.type === FieldTypes.OPTIONS &&
|
|
|
|
(!schema.constraints.inclusion ||
|
|
|
|
schema.constraints.inclusion.indexOf(row[fieldName]) === -1)
|
2021-02-22 12:39:58 +01:00
|
|
|
) {
|
2021-05-18 23:14:27 +02:00
|
|
|
schema.constraints.inclusion = [
|
|
|
|
...schema.constraints.inclusion,
|
|
|
|
row[fieldName],
|
|
|
|
]
|
2021-02-22 12:39:58 +01:00
|
|
|
}
|
|
|
|
}
|
2021-09-21 16:59:50 +02:00
|
|
|
|
2021-10-15 15:31:45 +02:00
|
|
|
finalData.push(row)
|
2021-02-22 12:39:58 +01:00
|
|
|
}
|
|
|
|
|
2021-10-15 15:31:45 +02:00
|
|
|
await db.bulkDocs(finalData)
|
2021-02-22 12:39:58 +01:00
|
|
|
let response = await db.put(table)
|
|
|
|
table._rev = response._rev
|
|
|
|
}
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
2021-03-15 17:36:38 +01:00
|
|
|
exports.handleSearchIndexes = async (appId, table) => {
|
|
|
|
const db = new CouchDB(appId)
|
2021-02-22 12:39:58 +01:00
|
|
|
// create relevant search indexes
|
|
|
|
if (table.indexes && table.indexes.length > 0) {
|
|
|
|
const currentIndexes = await db.getIndexes()
|
|
|
|
const indexName = `search:${table._id}`
|
|
|
|
|
|
|
|
const existingIndex = currentIndexes.indexes.find(
|
2021-05-04 12:32:22 +02:00
|
|
|
existing => existing.name === indexName
|
2021-02-22 12:39:58 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
if (existingIndex) {
|
|
|
|
const currentFields = existingIndex.def.fields.map(
|
2021-05-04 12:32:22 +02:00
|
|
|
field => Object.keys(field)[0]
|
2021-02-22 12:39:58 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// if index fields have changed, delete the original index
|
|
|
|
if (!isEqual(currentFields, table.indexes)) {
|
|
|
|
await db.deleteIndex(existingIndex)
|
|
|
|
// create/recreate the index with fields
|
|
|
|
await db.createIndex({
|
|
|
|
index: {
|
|
|
|
fields: table.indexes,
|
|
|
|
name: indexName,
|
|
|
|
ddoc: "search_ddoc",
|
|
|
|
type: "json",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// create/recreate the index with fields
|
|
|
|
await db.createIndex({
|
|
|
|
index: {
|
|
|
|
fields: table.indexes,
|
|
|
|
name: indexName,
|
|
|
|
ddoc: "search_ddoc",
|
|
|
|
type: "json",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
2021-05-04 12:32:22 +02:00
|
|
|
exports.checkStaticTables = table => {
|
2021-02-22 12:39:58 +01:00
|
|
|
// check user schema has all required elements
|
2021-04-09 18:33:21 +02:00
|
|
|
if (table._id === InternalTables.USER_METADATA) {
|
2021-02-22 12:39:58 +01:00
|
|
|
for (let [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) {
|
|
|
|
// check if the schema exists on the table to be created/updated
|
|
|
|
if (table.schema[key] == null) {
|
|
|
|
table.schema[key] = schema
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
|
|
|
class TableSaveFunctions {
|
|
|
|
constructor({ db, ctx, oldTable, dataImport }) {
|
|
|
|
this.db = db
|
|
|
|
this.ctx = ctx
|
2021-03-15 17:36:38 +01:00
|
|
|
if (this.ctx && this.ctx.user) {
|
2021-03-29 18:32:05 +02:00
|
|
|
this.appId = this.ctx.appId
|
2021-03-15 17:36:38 +01:00
|
|
|
}
|
2021-02-22 12:39:58 +01:00
|
|
|
this.oldTable = oldTable
|
|
|
|
this.dataImport = dataImport
|
|
|
|
// any rows that need updated
|
|
|
|
this.rows = []
|
|
|
|
}
|
|
|
|
|
|
|
|
// before anything is done
|
|
|
|
async before(table) {
|
|
|
|
if (this.oldTable) {
|
|
|
|
table = exports.makeSureTableUpToDate(this.oldTable, table)
|
|
|
|
}
|
|
|
|
table = exports.checkStaticTables(table)
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
|
|
|
// when confirmed valid
|
|
|
|
async mid(table) {
|
|
|
|
let response = await exports.checkForColumnUpdates(
|
|
|
|
this.db,
|
|
|
|
this.oldTable,
|
|
|
|
table
|
|
|
|
)
|
2021-02-22 13:05:59 +01:00
|
|
|
this.rows = this.rows.concat(response.rows)
|
2021-02-22 12:39:58 +01:00
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
|
|
|
// after saving
|
|
|
|
async after(table) {
|
2021-03-15 17:36:38 +01:00
|
|
|
table = await exports.handleSearchIndexes(this.appId, table)
|
2021-02-22 12:39:58 +01:00
|
|
|
table = await exports.handleDataImport(
|
2021-03-29 18:32:05 +02:00
|
|
|
this.appId,
|
2021-02-22 12:39:58 +01:00
|
|
|
this.ctx.user,
|
|
|
|
table,
|
|
|
|
this.dataImport
|
|
|
|
)
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
|
|
|
getUpdatedRows() {
|
|
|
|
return this.rows
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-23 20:05:32 +02:00
|
|
|
exports.getAllExternalTables = async (appId, datasourceId) => {
|
2021-06-16 17:27:33 +02:00
|
|
|
const db = new CouchDB(appId)
|
|
|
|
const datasource = await db.get(datasourceId)
|
|
|
|
if (!datasource || !datasource.entities) {
|
|
|
|
throw "Datasource is not configured fully."
|
|
|
|
}
|
2021-06-23 20:05:32 +02:00
|
|
|
return datasource.entities
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.getExternalTable = async (appId, datasourceId, tableName) => {
|
|
|
|
const entities = await exports.getAllExternalTables(appId, datasourceId)
|
|
|
|
return entities[tableName]
|
2021-06-16 17:27:33 +02:00
|
|
|
}
|
|
|
|
|
2021-10-19 18:00:54 +02:00
|
|
|
exports.getTable = async (appId, tableId) => {
|
|
|
|
const db = new CouchDB(appId)
|
|
|
|
if (isExternalTable(tableId)) {
|
|
|
|
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
|
|
|
return exports.getExternalTable(appId, datasourceId, tableName)
|
|
|
|
} else {
|
|
|
|
return db.get(tableId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-20 21:01:49 +02:00
|
|
|
exports.checkForViewUpdates = async (db, table, rename, deletedColumns) => {
|
|
|
|
const views = await getViews(db)
|
|
|
|
const tableViews = views.filter(view => view.meta.tableId === table._id)
|
|
|
|
|
|
|
|
// Check each table view to see if impacted by this table action
|
|
|
|
for (let view of tableViews) {
|
|
|
|
let needsUpdated = false
|
|
|
|
|
|
|
|
// First check for renames, otherwise check for deletions
|
|
|
|
if (rename) {
|
|
|
|
// Update calculation field if required
|
|
|
|
if (view.meta.field === rename.old) {
|
|
|
|
view.meta.field = rename.updated
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update group by field if required
|
|
|
|
if (view.meta.groupBy === rename.old) {
|
|
|
|
view.meta.groupBy = rename.updated
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update filters if required
|
|
|
|
view.meta.filters?.forEach(filter => {
|
|
|
|
if (filter.key === rename.old) {
|
|
|
|
filter.key = rename.updated
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else if (deletedColumns?.length) {
|
|
|
|
deletedColumns.forEach(column => {
|
|
|
|
// Remove calculation statement if required
|
|
|
|
if (view.meta.field === column) {
|
|
|
|
delete view.meta.field
|
|
|
|
delete view.meta.calculation
|
|
|
|
delete view.meta.groupBy
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove group by field if required
|
|
|
|
if (view.meta.groupBy === column) {
|
|
|
|
delete view.meta.groupBy
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove filters referencing deleted field if required
|
|
|
|
if (view.meta.filters?.length) {
|
|
|
|
const initialLength = view.meta.filters.length
|
|
|
|
view.meta.filters = view.meta.filters.filter(filter => {
|
|
|
|
return filter.key !== column
|
|
|
|
})
|
|
|
|
if (initialLength !== view.meta.filters.length) {
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update view if required
|
|
|
|
if (needsUpdated) {
|
|
|
|
const newViewTemplate = viewTemplate(view.meta)
|
|
|
|
await saveView(db, null, view.name, newViewTemplate)
|
|
|
|
if (!newViewTemplate.meta.schema) {
|
|
|
|
newViewTemplate.meta.schema = table.schema
|
|
|
|
}
|
|
|
|
table.views[view.name] = newViewTemplate.meta
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-22 12:39:58 +01:00
|
|
|
exports.TableSaveFunctions = TableSaveFunctions
|