Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
172ee71
add fullName to user model/bindings and references
ConorWebb96 Mar 30, 2026
3d3dc0b
treat fullName as system/non-editable user field
ConorWebb96 Mar 30, 2026
3d4720e
add related tests
ConorWebb96 Mar 30, 2026
fb80eb7
added read-time fullName fallback in search metadata path
ConorWebb96 Mar 30, 2026
2522b35
added read-time fullName fallback for single-user fetch
ConorWebb96 Mar 30, 2026
d759658
added regression test covering legacy metadata without persisted full…
ConorWebb96 Mar 30, 2026
b285c36
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 1, 2026
c2e4ea9
amended test to include fullName
ConorWebb96 Apr 1, 2026
824ad03
passed fullName to failing test
ConorWebb96 Apr 1, 2026
f332f64
fixed failing test + reran locally to confirm
ConorWebb96 Apr 1, 2026
04138c5
linting
ConorWebb96 Apr 1, 2026
1fd4d0f
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
melohagan Apr 1, 2026
0ce6d70
feedback remove duplication
ConorWebb96 Apr 1, 2026
49456b1
address feedback add lastName test coverage
ConorWebb96 Apr 1, 2026
380770d
address test feedback
ConorWebb96 Apr 1, 2026
1d80880
Merge branch '18197-add-full-name-binding-to-user-metadata-for-option…
ConorWebb96 Apr 1, 2026
31556a1
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 1, 2026
11ae651
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 1, 2026
d795cf0
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 2, 2026
86178bf
addresses computed test logic feedback
ConorWebb96 Apr 2, 2026
725c249
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 2, 2026
9fa71a3
revert to expect.any test
ConorWebb96 Apr 2, 2026
5a75515
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 7, 2026
6f59c44
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 8, 2026
b34427c
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
deanhannigan Apr 8, 2026
ae90666
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 9, 2026
ed24db9
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 9, 2026
ccbbc15
compute Current User.fullName for row defaults
ConorWebb96 Apr 9, 2026
09763c7
stop persisting user fullName in metadata
ConorWebb96 Apr 9, 2026
02f8f1d
expose computed Current User.fullName binding
ConorWebb96 Apr 9, 2026
812767e
remove email fallback
ConorWebb96 Apr 9, 2026
b87f815
fix broken test
ConorWebb96 Apr 9, 2026
dd80690
Merge branch '18197-add-full-name-binding-to-user-metadata-for-option…
ConorWebb96 Apr 9, 2026
5508a7a
undefined fullName when first and last have no values
ConorWebb96 Apr 9, 2026
42c8407
Merge branch 'master' into 18197-add-full-name-binding-to-user-metada…
ConorWebb96 Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
if (parsedSchema.lastName) {
parsedSchema.lastName.displayName = "Last Name"
}
if (parsedSchema.fullName) {
parsedSchema.fullName.displayName = "Full Name"
}
if (parsedSchema.status) {
parsedSchema.status.displayName = "Status"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
delete customSchema["status"]
delete customSchema["firstName"]
delete customSchema["lastName"]
delete customSchema["fullName"]
return Object.entries(customSchema)
}

Expand Down
1 change: 1 addition & 0 deletions packages/builder/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const UNEDITABLE_USER_FIELDS = [
"status",
"firstName",
"lastName",
"fullName",
]

export const LAYOUT_NAMES = {
Expand Down
38 changes: 38 additions & 0 deletions packages/builder/src/dataBinding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ describe("Builder dataBinding", () => {
runtimeBinding: "count",
type: "context",
},
{
category: "Current User",
icon: "user",
providerId: "user",
readableBinding: "Current User.fullName",
runtimeBinding: "[user].[fullName]",
type: "context",
},
]
it("should convert a runtime binding to a readable one", () => {
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ count }}.`
Expand All @@ -112,6 +120,17 @@ describe("Builder dataBinding", () => {
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
)
})

it("should convert fullName user bindings to readable format", () => {
const textWithBindings = `Hello {{ [user].[fullName] }}`
expect(
runtimeToReadableBinding(
bindableProperties,
textWithBindings,
"readableBinding"
)
).toEqual(`Hello {{ Current User.fullName }}`)
})
})

describe("readableToRuntimeBinding", () => {
Expand Down Expand Up @@ -152,6 +171,14 @@ describe("Builder dataBinding", () => {
runtimeBinding: "[foo].[baz]",
type: "context",
},
{
category: "Current User",
icon: "user",
providerId: "user",
readableBinding: "Current User.fullName",
runtimeBinding: "[user].[fullName]",
type: "context",
},
]
it("should convert a readable binding to a runtime one", () => {
const textWithBindings = `Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
Expand Down Expand Up @@ -185,6 +212,17 @@ describe("Builder dataBinding", () => {
)
).toEqual(`{{ [foo].[baz] }}`)
})

it("should convert fullName user bindings to runtime format", () => {
const textWithBindings = `{{ Current User.fullName }}`
expect(
readableToRuntimeBinding(
bindableProperties,
textWithBindings,
"runtimeBinding"
)
).toEqual(`{{ [user].[fullName] }}`)
})
})

describe("getSchemaForDatasource", () => {
Expand Down
12 changes: 10 additions & 2 deletions packages/server/src/api/routes/tests/row.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1080,12 +1080,17 @@ if (descriptions.length) {
InternalTables.USER_METADATA,
config.userMetadataId!
)
const { roles: _roles, ...userWithoutRoles } = config.getUser()
const expectedFullName =
[userWithoutRoles.firstName, userWithoutRoles.lastName]
.filter(part => !!part)
.join(" ") || userWithoutRoles.email

expect(res).toEqual({
...config.getUser(),
...userWithoutRoles,
fullName: expectedFullName,
_id: config.userMetadataId!,
_rev: expect.any(String),
roles: undefined,
roleId: "ADMIN",
tableId: InternalTables.USER_METADATA,
})
Expand Down Expand Up @@ -3048,6 +3053,9 @@ if (descriptions.length) {
email: row.email,
firstName: row.firstName,
lastName: row.lastName,
fullName:
[row.firstName, row.lastName].filter(part => !!part).join(" ") ||
row.email,
}),
],
])("links - %s", (__, relSchema, dataGenerator, resultMapper) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/api/routes/tests/viewV2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3747,6 +3747,10 @@ if (descriptions.length) {
})

const response = await config.api.viewV2.search(view.id)
const expectedFullName =
[user.firstName, user.lastName]
.filter(part => !!part)
.join(" ") || user.email

expect(response.rows).toEqual([
expect.objectContaining({
Expand All @@ -3759,6 +3763,7 @@ if (descriptions.length) {
_id: user._id,
email: user.email,
firstName: user.firstName,
fullName: expectedFullName,
lastName: user.lastName,
primaryDisplay: user.email,
},
Expand Down
8 changes: 8 additions & 0 deletions packages/server/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ export const USERS_TABLE_SCHEMA: Table = {
presence: false,
},
},
fullName: {
name: "fullName",
type: FieldType.STRING,
constraints: {
type: FieldType.STRING,
presence: false,
},
},
roleId: {
name: "roleId",
type: FieldType.OPTIONS,
Expand Down
106 changes: 104 additions & 2 deletions packages/server/src/sdk/users/tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { db, roles } from "@budibase/backend-core"
import { context, db, roles } from "@budibase/backend-core"
import { structures } from "@budibase/backend-core/tests"
import { sdk as proSdk } from "@budibase/pro"
import tk from "timekeeper"

import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { rawUserMetadata, syncGlobalUsers } from "../utils"
import { fetchMetadata, rawUserMetadata, syncGlobalUsers } from "../utils"

describe("syncGlobalUsers", () => {
const config = new TestConfiguration()
Expand Down Expand Up @@ -107,6 +107,7 @@ describe("syncGlobalUsers", () => {
email: user1.email,
firstName: user1.firstName,
lastName: user1.lastName,
fullName: `${user1.firstName} ${user1.lastName}`,
builder: { global: false },
admin: { global: false },

Expand All @@ -123,6 +124,78 @@ describe("syncGlobalUsers", () => {
})
})

it("uses email as fullName fallback when first and last names are empty", async () => {
const user = await config.createUser({
firstName: "",
lastName: "",
admin: { global: false },
builder: { global: false },
roles: {
[config.getProdWorkspaceId()]: roles.BUILTIN_ROLE_IDS.BASIC,
},
})

await config.doInContext(config.devWorkspaceId, async () => {
await syncGlobalUsers()

const metadata = await rawUserMetadata()
expect(metadata).toContainEqual(
expect.objectContaining({
_id: db.generateUserMetadataID(user._id!),
fullName: user.email,
})
)
})
})

it("uses single-name fullName fallback when only one name part exists", async () => {
const user = await config.createUser({
firstName: "Only",
lastName: "",
admin: { global: false },
builder: { global: false },
roles: {
[config.getProdWorkspaceId()]: roles.BUILTIN_ROLE_IDS.BASIC,
},
})

await config.doInContext(config.devWorkspaceId, async () => {
await syncGlobalUsers()

const metadata = await rawUserMetadata()
expect(metadata).toContainEqual(
expect.objectContaining({
_id: db.generateUserMetadataID(user._id!),
fullName: "Only",
})
)
})
})

it("uses single-name fullName fallback when only last name exists", async () => {
const user = await config.createUser({
firstName: "",
lastName: "SurnameOnly",
admin: { global: false },
builder: { global: false },
roles: {
[config.getProdWorkspaceId()]: roles.BUILTIN_ROLE_IDS.BASIC,
},
})

await config.doInContext(config.devWorkspaceId, async () => {
await syncGlobalUsers()

const metadata = await rawUserMetadata()
expect(metadata).toContainEqual(
expect.objectContaining({
_id: db.generateUserMetadataID(user._id!),
fullName: "SurnameOnly",
})
)
})
})

it("workspace users audit data is updated", async () => {
tk.freeze(new Date())
const user1 = await config.createUser({
Expand Down Expand Up @@ -158,6 +231,35 @@ describe("syncGlobalUsers", () => {
})
})

it("computes fullName in fetchMetadata when missing from persisted metadata", async () => {
const user = await config.createUser({
firstName: "Jane",
lastName: "Doe",
admin: { global: false },
builder: { global: false },
roles: {
[config.getProdWorkspaceId()]: roles.BUILTIN_ROLE_IDS.BASIC,
},
})

await config.doInContext(config.devWorkspaceId, async () => {
const workspaceDb = context.getWorkspaceDB()
const userMetadataId = db.generateUserMetadataID(user._id!)
await workspaceDb.put({
_id: userMetadataId,
tableId: "ta_users",
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
})

const metadata = await fetchMetadata()
const found = metadata.find(doc => doc._id === userMetadataId)
expect(found?.fullName).toEqual("Jane Doe")
})
})

it("workspace users are not synced if not specified", async () => {
const user = await config.createUser({
admin: { global: false },
Expand Down
9 changes: 8 additions & 1 deletion packages/server/src/sdk/users/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
InternalTables,
} from "../../db/utils"
import { getGlobalUsers } from "../../utilities/global"
import { getUserFullName } from "../../utilities/users"

export function combineMetadataAndUser(
user: ContextUser,
Expand All @@ -38,6 +39,7 @@ export function combineMetadataAndUser(
delete user._rev
const newDoc = {
...user,
fullName: getUserFullName(user),
_id: metadataId,
tableId: InternalTables.USER_METADATA,
}
Expand Down Expand Up @@ -83,12 +85,16 @@ export async function fetchMetadata(): Promise<ContextUserMetadata[]> {
// find the metadata that matches up to the global ID
const info = metadata.find(meta => meta._id!.includes(user._id!))
// remove these props, not for the correct DB
users.push({
const mergedUser = {
...user,
...info,
tableId: InternalTables.USER_METADATA,
// make sure the ID is always a local ID, not a global one
_id: generateUserMetadataID(user._id!),
}
users.push({
...mergedUser,
fullName: getUserFullName(mergedUser),
})
}
return users
Expand Down Expand Up @@ -141,6 +147,7 @@ export function getUserContextBindings(user: ContextUser): UserBindings {
_rev: user._rev,
firstName: user.firstName,
lastName: user.lastName,
fullName: getUserFullName(user),
email: user.email,
status: user.status,
roleId: user.roleId,
Expand Down
10 changes: 10 additions & 0 deletions packages/server/src/sdk/workspace/tables/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import datasources from "../datasources"
import { isV2 } from "../views"
import { ensureQueryUISet } from "../views/utils"
import { USERS_TABLE_SCHEMA } from "../../../constants"

export async function processTable(table: Table): Promise<Table> {
return await tracer.trace("processTable", async span => {
Expand Down Expand Up @@ -59,6 +60,15 @@ export async function processTable(table: Table): Promise<Table> {
sourceType: TableSourceType.INTERNAL,
sql: true,
}

if (processed._id === InternalTables.USER_METADATA) {
for (const [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) {
if (!processed.schema[key]) {
processed.schema[key] = schema
}
}
}

return processed
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
DocumentType,
SEPARATOR,
} from "@budibase/types"
import { getUserFullName } from "../users"
import { InvalidBBRefError } from "./errors"

const ROW_PREFIX = DocumentType.ROW + SEPARATOR
Expand Down Expand Up @@ -113,6 +114,7 @@ interface UserReferenceInfo {
email: string
firstName?: string
lastName?: string
fullName?: string
}

export async function processOutputBBReference(
Expand Down Expand Up @@ -145,6 +147,7 @@ export async function processOutputBBReference(
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: getUserFullName(user),
}
}
default:
Expand Down Expand Up @@ -176,6 +179,7 @@ export async function processOutputBBReferences(
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
fullName: getUserFullName(u),
}))
}
default:
Expand Down
Loading
Loading