Implement many-to-many user column migrations.

This commit is contained in:
Sam Rose 2023-10-19 17:28:55 +01:00
parent 2176fe75ff
commit a3ad8780de
No known key found for this signature in database
5 changed files with 154 additions and 13 deletions

View File

@ -8,6 +8,7 @@ import {
AutoFieldSubTypes,
InternalTable,
FieldSubtype,
Row,
} from "@budibase/types"
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
@ -421,8 +422,13 @@ describe("/tables", () => {
})
describe("migrate", () => {
it("should successfully migrate a user relationship to a user column", async () => {
const users = await config.api.row.fetch(InternalTable.USER_METADATA)
it("should successfully migrate a one-to-many user relationship to a user column", async () => {
const users = await Promise.all([
config.createUser({ email: "1@example.com" }),
config.createUser({ email: "2@example.com" }),
config.createUser({ email: "3@example.com" }),
])
const table = await config.api.table.create({
name: "table",
type: "table",
@ -441,9 +447,11 @@ describe("/tables", () => {
},
})
await config.api.row.save(table._id!, {
"user relationship": users,
})
const rows = await Promise.all(
users.map(u =>
config.api.row.save(table._id!, { "user relationship": [u] })
)
)
await config.api.table.migrate(table._id!, {
oldColumn: table.schema["user relationship"],
@ -458,9 +466,80 @@ describe("/tables", () => {
expect(migratedTable.schema["user column"]).toBeDefined()
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
const rows = await config.api.row.fetch(table._id!)
expect(rows[0]["user column"]).toBeDefined()
expect(rows[0]["user relationship"]).not.toBeDefined()
const migratedRows = await config.api.row.fetch(table._id!)
rows.sort((a, b) => a._id!.localeCompare(b._id!))
migratedRows.sort((a, b) => a._id!.localeCompare(b._id!))
for (const [i, row] of rows.entries()) {
const migratedRow = migratedRows[i]
expect(migratedRow["user column"]).toBeDefined()
expect(migratedRow["user relationship"]).not.toBeDefined()
expect(row["user relationship"][0]._id).toEqual(
migratedRow["user column"][0]._id
)
}
})
it("should successfully migrate a many-to-many user relationship to a users column", async () => {
const users = await Promise.all([
config.createUser({ email: "1@example.com" }),
config.createUser({ email: "2@example.com" }),
config.createUser({ email: "3@example.com" }),
])
const table = await config.api.table.create({
name: "table",
type: "table",
schema: {
"user relationship": {
type: FieldType.LINK,
fieldName: "test",
name: "user relationship",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.MANY_TO_MANY,
tableId: InternalTable.USER_METADATA,
},
},
})
const row1 = await config.api.row.save(table._id!, {
"user relationship": [users[0], users[1]],
})
const row2 = await config.api.row.save(table._id!, {
"user relationship": [users[2]],
})
await config.api.table.migrate(table._id!, {
oldColumn: table.schema["user relationship"],
newColumn: {
name: "user column",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
})
const migratedTable = await config.api.table.get(table._id!)
expect(migratedTable.schema["user column"]).toBeDefined()
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
const row1Migrated = (await config.api.row.get(table._id!, row1._id!))
.body as Row
expect(row1Migrated["user relationship"]).not.toBeDefined()
expect(row1Migrated["user column"].map((r: Row) => r._id)).toEqual(
expect.arrayContaining([users[0]._id, users[1]._id])
)
const row2Migrated = (await config.api.row.get(table._id!, row2._id!))
.body as Row
expect(row2Migrated["user relationship"]).not.toBeDefined()
expect(row2Migrated["user column"].map((r: Row) => r._id)).toEqual([
users[2]._id,
])
})
})
})

View File

@ -57,6 +57,6 @@ export async function fetchView(
tableId: string,
viewName: string,
params: ViewParams
) {
): Promise<Row[]> {
return pickApi(tableId).fetchView(viewName, params)
}

View File

@ -2,8 +2,12 @@ import { BadRequestError, context } from "@budibase/backend-core"
import {
BBReferenceFieldMetadata,
FieldSchema,
FieldSubtype,
InternalTable,
ManyToManyRelationshipFieldMetadata,
OneToManyRelationshipFieldMetadata,
RelationshipFieldMetadata,
RelationshipType,
Row,
Table,
isBBReferenceField,
@ -77,13 +81,30 @@ function getColumnMigrator(
)
}
return new UserColumnMigrator(table, oldColumn, newColumn)
if (oldColumn.relationshipType === RelationshipType.ONE_TO_MANY) {
if (newColumn.subtype !== FieldSubtype.USER) {
throw new BadRequestError(
`Column "${oldColumn.name}" is a one-to-many column but "${newColumn.name}" is not a single user column`
)
}
return new OneToManyUserColumnMigrator(table, oldColumn, newColumn)
}
if (oldColumn.relationshipType === RelationshipType.MANY_TO_MANY) {
if (newColumn.subtype !== FieldSubtype.USERS) {
throw new BadRequestError(
`Column "${oldColumn.name}" is a many-to-many column but "${newColumn.name}" is not a multi user column`
)
}
return new ManyToManyUserColumnMigrator(table, oldColumn, newColumn)
}
throw new BadRequestError(`Unknown migration type`)
}
class UserColumnMigrator implements ColumnMigrator {
class OneToManyUserColumnMigrator implements ColumnMigrator {
constructor(
private table: Table,
private oldColumn: RelationshipFieldMetadata,
private oldColumn: OneToManyRelationshipFieldMetadata,
private newColumn: BBReferenceFieldMetadata
) {}
@ -115,3 +136,42 @@ class UserColumnMigrator implements ColumnMigrator {
await db.bulkDocs(rows)
}
}
class ManyToManyUserColumnMigrator implements ColumnMigrator {
constructor(
private table: Table,
private oldColumn: ManyToManyRelationshipFieldMetadata,
private newColumn: BBReferenceFieldMetadata
) {}
async doMigration() {
let rows = await sdk.rows.fetchRaw(this.table._id!)
let rowsById = rows.reduce((acc, row) => {
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.newColumn.name]) {
row[this.newColumn.name] = []
}
row[this.newColumn.name].push(userId)
}
let db = context.getAppDB()
await db.bulkDocs(rows)
}
}

View File

@ -55,6 +55,7 @@ import {
RelationshipType,
CreateViewRequest,
RelationshipFieldMetadata,
User,
} from "@budibase/types"
import API from "./api"
@ -254,7 +255,7 @@ class TestConfiguration {
} catch (err) {
existing = { email }
}
const user = {
const user: User = {
_id: id,
...existing,
roles: roles || {},

View File

@ -27,5 +27,6 @@ export default class API {
this.datasource = new DatasourceAPI(config)
this.screen = new ScreenAPI(config)
this.application = new ApplicationAPI(config)
this.global = new GlobalAPI(config)
}
}