Implement many-to-many user column migrations.
This commit is contained in:
parent
2176fe75ff
commit
a3ad8780de
|
@ -8,6 +8,7 @@ import {
|
||||||
AutoFieldSubTypes,
|
AutoFieldSubTypes,
|
||||||
InternalTable,
|
InternalTable,
|
||||||
FieldSubtype,
|
FieldSubtype,
|
||||||
|
Row,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
|
@ -421,8 +422,13 @@ describe("/tables", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("migrate", () => {
|
describe("migrate", () => {
|
||||||
it("should successfully migrate a user relationship to a user column", async () => {
|
it("should successfully migrate a one-to-many user relationship to a user column", async () => {
|
||||||
const users = await config.api.row.fetch(InternalTable.USER_METADATA)
|
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({
|
const table = await config.api.table.create({
|
||||||
name: "table",
|
name: "table",
|
||||||
type: "table",
|
type: "table",
|
||||||
|
@ -441,9 +447,11 @@ describe("/tables", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await config.api.row.save(table._id!, {
|
const rows = await Promise.all(
|
||||||
"user relationship": users,
|
users.map(u =>
|
||||||
})
|
config.api.row.save(table._id!, { "user relationship": [u] })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
await config.api.table.migrate(table._id!, {
|
await config.api.table.migrate(table._id!, {
|
||||||
oldColumn: table.schema["user relationship"],
|
oldColumn: table.schema["user relationship"],
|
||||||
|
@ -458,9 +466,80 @@ describe("/tables", () => {
|
||||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
expect(migratedTable.schema["user column"]).toBeDefined()
|
||||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||||
|
|
||||||
const rows = await config.api.row.fetch(table._id!)
|
const migratedRows = await config.api.row.fetch(table._id!)
|
||||||
expect(rows[0]["user column"]).toBeDefined()
|
|
||||||
expect(rows[0]["user relationship"]).not.toBeDefined()
|
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,
|
||||||
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -57,6 +57,6 @@ export async function fetchView(
|
||||||
tableId: string,
|
tableId: string,
|
||||||
viewName: string,
|
viewName: string,
|
||||||
params: ViewParams
|
params: ViewParams
|
||||||
) {
|
): Promise<Row[]> {
|
||||||
return pickApi(tableId).fetchView(viewName, params)
|
return pickApi(tableId).fetchView(viewName, params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,12 @@ import { BadRequestError, context } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
BBReferenceFieldMetadata,
|
BBReferenceFieldMetadata,
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
|
FieldSubtype,
|
||||||
InternalTable,
|
InternalTable,
|
||||||
|
ManyToManyRelationshipFieldMetadata,
|
||||||
|
OneToManyRelationshipFieldMetadata,
|
||||||
RelationshipFieldMetadata,
|
RelationshipFieldMetadata,
|
||||||
|
RelationshipType,
|
||||||
Row,
|
Row,
|
||||||
Table,
|
Table,
|
||||||
isBBReferenceField,
|
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(
|
constructor(
|
||||||
private table: Table,
|
private table: Table,
|
||||||
private oldColumn: RelationshipFieldMetadata,
|
private oldColumn: OneToManyRelationshipFieldMetadata,
|
||||||
private newColumn: BBReferenceFieldMetadata
|
private newColumn: BBReferenceFieldMetadata
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -115,3 +136,42 @@ class UserColumnMigrator implements ColumnMigrator {
|
||||||
await db.bulkDocs(rows)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ import {
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
CreateViewRequest,
|
CreateViewRequest,
|
||||||
RelationshipFieldMetadata,
|
RelationshipFieldMetadata,
|
||||||
|
User,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
import API from "./api"
|
import API from "./api"
|
||||||
|
@ -254,7 +255,7 @@ class TestConfiguration {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
existing = { email }
|
existing = { email }
|
||||||
}
|
}
|
||||||
const user = {
|
const user: User = {
|
||||||
_id: id,
|
_id: id,
|
||||||
...existing,
|
...existing,
|
||||||
roles: roles || {},
|
roles: roles || {},
|
||||||
|
|
|
@ -27,5 +27,6 @@ export default class API {
|
||||||
this.datasource = new DatasourceAPI(config)
|
this.datasource = new DatasourceAPI(config)
|
||||||
this.screen = new ScreenAPI(config)
|
this.screen = new ScreenAPI(config)
|
||||||
this.application = new ApplicationAPI(config)
|
this.application = new ApplicationAPI(config)
|
||||||
|
this.global = new GlobalAPI(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue