Emit table updates to the builder socket, and some minor refactoring.

This commit is contained in:
Sam Rose 2023-10-25 11:03:35 +01:00
parent 0f5c2cff00
commit 78afba63de
No known key found for this signature in database
3 changed files with 78 additions and 85 deletions

View File

@ -165,7 +165,11 @@ export async function migrate(ctx: UserCtx<MigrateRequest, MigrateResponse>) {
const { oldColumn, newColumn } = ctx.request.body const { oldColumn, newColumn } = ctx.request.body
let tableId = ctx.params.tableId as string let tableId = ctx.params.tableId as string
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
await sdk.tables.migrate(table, oldColumn, newColumn) let result = await sdk.tables.migrate(table, oldColumn, newColumn)
for (let table of result.tablesUpdated) {
builderSocket?.emitTableUpdate(ctx, table)
}
ctx.status = 200 ctx.status = 200
ctx.body = { message: `Column ${oldColumn.name} migrated.` } ctx.body = { message: `Column ${oldColumn.name} migrated.` }

View File

@ -1,37 +1,43 @@
import { BadRequestError, context } from "@budibase/backend-core" import { BadRequestError, context, db as dbCore } from "@budibase/backend-core"
import { import {
BBReferenceFieldMetadata, BBReferenceFieldMetadata,
FieldSchema, FieldSchema,
FieldSubtype, FieldSubtype,
InternalTable, InternalTable,
ManyToManyRelationshipFieldMetadata, isBBReferenceField,
ManyToOneRelationshipFieldMetadata, isRelationshipField,
OneToManyRelationshipFieldMetadata, LinkDocument,
RelationshipFieldMetadata,
RelationshipType, RelationshipType,
Row, Row,
Table, Table,
isBBReferenceField,
isRelationshipField,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { isExternalTable } from "../../../../src/integrations/utils" import { isExternalTable } from "../../../integrations/utils"
import { db as dbCore } from "@budibase/backend-core" import { EventType, updateLinks } from "../../../db/linkedRows"
import { EventType, updateLinks } from "../../../../src/db/linkedRows"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
export interface MigrationResult {
tablesUpdated: Table[]
}
export async function migrate( export async function migrate(
table: Table, table: Table,
oldColumn: FieldSchema, oldColumn: FieldSchema,
newColumn: FieldSchema newColumn: FieldSchema
) { ): Promise<MigrationResult> {
let migrator = getColumnMigrator(table, oldColumn, newColumn) if (newColumn.name in table.schema) {
let oldTable = cloneDeep(table) throw new BadRequestError(`Column "${newColumn.name}" already exists`)
}
table.schema[newColumn.name] = newColumn table.schema[newColumn.name] = newColumn
table = await sdk.tables.saveTable(table) table = await sdk.tables.saveTable(table)
let migrator = getColumnMigrator(table, oldColumn, newColumn)
let result: MigrationResult
try { try {
await migrator.doMigration() result = await migrator.doMigration()
} catch (e) { } catch (e) {
// If the migration fails then we need to roll back the table schema // If the migration fails then we need to roll back the table schema
// change. // change.
@ -40,13 +46,11 @@ export async function migrate(
throw e throw e
} }
delete table.schema[oldColumn.name] return result
table = await sdk.tables.saveTable(table)
await updateLinks({ eventType: EventType.TABLE_UPDATED, table, oldTable })
} }
interface ColumnMigrator { interface ColumnMigrator {
doMigration(): Promise<void> doMigration(): Promise<MigrationResult>
} }
function getColumnMigrator( function getColumnMigrator(
@ -54,8 +58,8 @@ function getColumnMigrator(
oldColumn: FieldSchema, oldColumn: FieldSchema,
newColumn: FieldSchema newColumn: FieldSchema
): ColumnMigrator { ): ColumnMigrator {
// For now we're only supporting migrations of user relationships to user // For now, we're only supporting migrations of user relationships to user
// columns in internal tables. In future we may want to support other // columns in internal tables. In the future, we may want to support other
// migrations but for now return an error if we aren't migrating a user // migrations but for now return an error if we aren't migrating a user
// relationship. // relationship.
if (isExternalTable(table._id!)) { if (isExternalTable(table._id!)) {
@ -66,10 +70,6 @@ function getColumnMigrator(
throw new BadRequestError(`Column "${oldColumn.name}" does not exist`) throw new BadRequestError(`Column "${oldColumn.name}" does not exist`)
} }
if (newColumn.name in table.schema) {
throw new BadRequestError(`Column "${newColumn.name}" already exists`)
}
if (!isBBReferenceField(newColumn)) { if (!isBBReferenceField(newColumn)) {
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`) throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
} }
@ -113,14 +113,17 @@ function getColumnMigrator(
throw new BadRequestError(`Unknown migration type`) throw new BadRequestError(`Unknown migration type`)
} }
class SingleUserColumnMigrator implements ColumnMigrator { abstract class UserColumnMigrator implements ColumnMigrator {
constructor( constructor(
private table: Table, protected table: Table,
private oldColumn: OneToManyRelationshipFieldMetadata, protected oldColumn: RelationshipFieldMetadata,
private newColumn: BBReferenceFieldMetadata protected newColumn: BBReferenceFieldMetadata
) {} ) {}
async doMigration() { abstract updateRow(row: Row, link: LinkDocument): void
async doMigration(): Promise<MigrationResult> {
let oldTable = cloneDeep(this.table)
let rows = await sdk.rows.fetchRaw(this.table._id!) let rows = await sdk.rows.fetchRaw(this.table._id!)
let rowsById = rows.reduce((acc, row) => { let rowsById = rows.reduce((acc, row) => {
acc[row._id!] = row acc[row._id!] = row
@ -129,17 +132,14 @@ class SingleUserColumnMigrator implements ColumnMigrator {
let links = await sdk.links.fetchWithDocument(this.table._id!) let links = await sdk.links.fetchWithDocument(this.table._id!)
for (let link of links) { for (let link of links) {
if (link.doc1.tableId !== this.table._id) { if (
continue link.doc1.tableId !== this.table._id ||
} link.doc1.fieldName !== this.oldColumn.name ||
if (link.doc1.fieldName !== this.oldColumn.name) { link.doc2.tableId !== InternalTable.USER_METADATA
continue ) {
}
if (link.doc2.tableId !== InternalTable.USER_METADATA) {
continue continue
} }
let userId = dbCore.getGlobalIDFromUserMetadataID(link.doc2.rowId)
let row = rowsById[link.doc1.rowId] let row = rowsById[link.doc1.rowId]
if (!row) { if (!row) {
// This can happen if the row has been deleted but the link hasn't, // This can happen if the row has been deleted but the link hasn't,
@ -148,58 +148,42 @@ class SingleUserColumnMigrator implements ColumnMigrator {
continue continue
} }
row[this.newColumn.name] = userId this.updateRow(row, link)
} }
let db = context.getAppDB() let db = context.getAppDB()
await db.bulkDocs(rows) await db.bulkDocs(rows)
}
}
class MultiUserColumnMigrator implements ColumnMigrator { delete this.table.schema[this.oldColumn.name]
constructor( this.table = await sdk.tables.saveTable(this.table)
private table: Table, await updateLinks({
private oldColumn: eventType: EventType.TABLE_UPDATED,
| ManyToManyRelationshipFieldMetadata table: this.table,
| ManyToOneRelationshipFieldMetadata, oldTable,
private newColumn: BBReferenceFieldMetadata })
) {}
async doMigration() { let otherTable = await sdk.tables.getTable(this.oldColumn.tableId)
let rows = await sdk.rows.fetchRaw(this.table._id!) return {
let rowsById = rows.reduce((acc, row) => { tablesUpdated: [this.table, otherTable],
acc[row._id!] = row
return acc
}, {} as Record<string, Row>)
let links = await sdk.links.fetchWithDocument(this.table._id!)
for (let link of links) {
if (link.doc1.tableId !== this.table._id) {
continue
}
if (link.doc1.fieldName !== this.oldColumn.name) {
continue
}
if (link.doc2.tableId !== InternalTable.USER_METADATA) {
continue
}
let userId = dbCore.getGlobalIDFromUserMetadataID(link.doc2.rowId)
let row = rowsById[link.doc1.rowId]
if (!row) {
// This can happen if the row has been deleted but the link hasn't,
// which was a state that was found during the initial testing of this
// feature. Not sure exactly what can cause it, but best to be safe.
continue
}
if (!row[this.newColumn.name]) {
row[this.newColumn.name] = []
}
row[this.newColumn.name].push(userId)
} }
}
let db = context.getAppDB() }
await db.bulkDocs(rows)
class SingleUserColumnMigrator extends UserColumnMigrator {
updateRow(row: Row, link: LinkDocument): void {
row[this.newColumn.name] = dbCore.getGlobalIDFromUserMetadataID(
link.doc2.rowId
)
}
}
class MultiUserColumnMigrator extends UserColumnMigrator {
updateRow(row: Row, link: LinkDocument): void {
if (!row[this.newColumn.name]) {
row[this.newColumn.name] = []
}
row[this.newColumn.name].push(
dbCore.getGlobalIDFromUserMetadataID(link.doc2.rowId)
)
} }
} }

View File

@ -58,8 +58,13 @@ export class TableAPI extends TestAPI {
.post(`/api/tables/${tableId}/migrate`) .post(`/api/tables/${tableId}/migrate`)
.send(data) .send(data)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) if (res.status !== expectStatus) {
.expect(expectStatus) throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body return res.body
} }
} }