Merge pull request #14177 from Budibase/budi-8434-default-value-row-processing

[BUDI-8434] Default value implementation on the backend.
This commit is contained in:
Sam Rose 2024-07-19 16:26:54 +01:00 committed by GitHub
commit 2d976bd439
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 318 additions and 6 deletions

View File

@ -8,6 +8,7 @@ const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g
const ENCODED_SPACE = encodeURIComponent(" ")
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/
const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/
export function isExternalTableID(tableId: string) {
return tableId.startsWith(DocumentType.DATASOURCE + SEPARATOR)
@ -147,6 +148,10 @@ export function isValidFilter(value: any) {
return value != null && value !== ""
}
export function isValidTime(value: string) {
return TIME_REGEX.test(value)
}
export function sqlLog(client: string, query: string, values?: any[]) {
if (!environment.SQL_LOGGING_ENABLE) {
return

View File

@ -550,6 +550,242 @@ describe.each([
expect(row.name).toEqual(`{ "foo": "2023-01-26T11:48:57.000Z" }`)
})
describe("default values", () => {
let table: Table
describe("string column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
description: {
name: "description",
type: FieldType.STRING,
default: "default description",
constraints: {
presence: true,
},
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.description).toEqual("default description")
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
description: "specified description",
})
expect(row.description).toEqual("specified description")
})
it("uses the default value if value is null", async () => {
const row = await config.api.row.save(table._id!, {
description: null,
})
expect(row.description).toEqual("default description")
})
it("uses the default value if value is undefined", async () => {
const row = await config.api.row.save(table._id!, {
description: undefined,
})
expect(row.description).toEqual("default description")
})
})
describe("number column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
age: {
name: "age",
type: FieldType.NUMBER,
default: "25",
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.age).toEqual(25)
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
age: 30,
})
expect(row.age).toEqual(30)
})
})
describe("date column", () => {
it("creates a row with a default value successfully", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
date: {
name: "date",
type: FieldType.DATETIME,
default: "2023-01-26T11:48:57.000Z",
},
},
})
)
const row = await config.api.row.save(table._id!, {})
expect(row.date).toEqual("2023-01-26T11:48:57.000Z")
})
it("gives an error if the default value is invalid", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
date: {
name: "date",
type: FieldType.DATETIME,
default: "invalid",
},
},
})
)
await config.api.row.save(
table._id!,
{},
{
status: 400,
body: {
message: `Invalid default value for field 'date' - Invalid date value: "invalid"`,
},
}
)
})
})
describe("bindings", () => {
describe("string column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
description: {
name: "description",
type: FieldType.STRING,
default: `{{ date now "YYYY-MM-DDTHH:mm:ss" }}`,
},
},
})
)
})
it("can use bindings in default values", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.description).toMatch(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
)
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
description: "specified description",
})
expect(row.description).toEqual("specified description")
})
it("can bind the current user", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
user: {
name: "user",
type: FieldType.STRING,
default: `{{ [Current User]._id }}`,
},
},
})
)
const row = await config.api.row.save(table._id!, {})
expect(row.user).toEqual(config.getUser()._id)
})
it("cannot access current user password", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
user: {
name: "user",
type: FieldType.STRING,
default: `{{ user.password }}`,
},
},
})
)
const row = await config.api.row.save(table._id!, {})
// For some reason it's null for internal tables, and undefined for
// external.
expect(row.user == null).toBe(true)
})
})
describe("number column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
age: {
name: "age",
type: FieldType.NUMBER,
default: `{{ sum 10 10 5 }}`,
},
},
})
)
})
it("can use bindings in default values", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.age).toEqual(25)
})
describe("invalid default value", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
age: {
name: "age",
type: FieldType.NUMBER,
default: `{{ capitalize "invalid" }}`,
},
},
})
)
})
it("throws an error when invalid default value", async () => {
await config.api.row.save(
table._id!,
{},
{
status: 400,
body: {
message:
"Invalid default value for field 'age' - Invalid number value \"Invalid\"",
},
}
)
})
})
})
})
})
})
describe("get", () => {

View File

@ -1022,6 +1022,11 @@ describe.each([
schema: {
one: { type: FieldType.STRING, name: "one" },
two: { type: FieldType.STRING, name: "two" },
default: {
type: FieldType.STRING,
name: "default",
default: "default",
},
},
})
)
@ -1042,11 +1047,13 @@ describe.each([
_viewId: view.id,
one: "foo",
two: "bar",
default: "ohnoes",
})
const row = await config.api.row.get(table._id!, newRow._id!)
expect(row.one).toBeUndefined()
expect(row.two).toEqual("bar")
expect(row.default).toEqual("default")
})
it("can't persist readonly columns", async () => {

View File

@ -1,14 +1,22 @@
import * as linkRows from "../../db/linkedRows"
import { fixAutoColumnSubType, processFormulas } from "./utils"
import { objectStore, utils } from "@budibase/backend-core"
import {
cache,
context,
HTTPError,
objectStore,
utils,
} from "@budibase/backend-core"
import { InternalTables } from "../../db/utils"
import { TYPE_TRANSFORM_MAP } from "./map"
import {
AutoFieldSubType,
FieldType,
IdentityType,
Row,
RowAttachment,
Table,
User,
} from "@budibase/types"
import { cloneDeep } from "lodash/fp"
import {
@ -19,6 +27,7 @@ import {
} from "./bbReferenceProcessor"
import { isExternalTableID } from "../../integrations/utils"
import { helpers } from "@budibase/shared-core"
import { processString } from "@budibase/string-templates"
export * from "./utils"
export * from "./attachments"
@ -88,7 +97,34 @@ export async function processAutoColumn(
break
}
}
return { table, row }
}
async function processDefaultValues(table: Table, row: Row) {
const ctx: { ["Current User"]?: User; user?: User } = {}
const identity = context.getIdentity()
if (identity?._id && identity.type === IdentityType.USER) {
const user = await cache.user.getUser(identity._id)
delete user.password
ctx["Current User"] = user
ctx.user = user
}
for (let [key, schema] of Object.entries(table.schema)) {
if ("default" in schema && schema.default != null && row[key] == null) {
const processed = await processString(schema.default, ctx)
try {
row[key] = coerce(processed, schema.type)
} catch (err: any) {
throw new HTTPError(
`Invalid default value for field '${key}' - ${err.message}`,
400
)
}
}
}
}
/**
@ -182,8 +218,10 @@ export async function inputProcessing(
clonedRow._rev = row._rev
}
// handle auto columns - this returns an object like {table, row}
return processAutoColumn(userId, table, clonedRow, opts)
await processAutoColumn(userId, table, clonedRow, opts)
await processDefaultValues(table, clonedRow)
return { table, row: clonedRow }
}
/**

View File

@ -1,3 +1,4 @@
import { sql } from "@budibase/backend-core"
import { FieldType } from "@budibase/types"
const parseArrayString = (value: any) => {
@ -91,7 +92,13 @@ export const TYPE_TRANSFORM_MAP: any = {
[null]: null,
//@ts-ignore
[undefined]: undefined,
parse: (n: any) => parseFloat(n),
parse: (n: any) => {
const parsed = parseFloat(n)
if (isNaN(parsed)) {
throw new Error(`Invalid number value "${n}"`)
}
return parsed
},
},
[FieldType.BIGINT]: {
"": null,
@ -109,8 +116,15 @@ export const TYPE_TRANSFORM_MAP: any = {
parse: (date: any) => {
if (date instanceof Date) {
return date.toISOString()
} else if (typeof date === "string" && sql.utils.isValidTime(date)) {
return date
} else {
const parsed = new Date(date)
if (isNaN(parsed.getTime())) {
throw new Error(`Invalid date value: "${date}"`)
}
return date
}
return date
},
},
[FieldType.ATTACHMENTS]: {

View File

@ -81,11 +81,13 @@ export interface NumberFieldMetadata extends Omit<BaseFieldSchema, "subtype"> {
toTable: string
toKey: string
}
default?: string
}
export interface JsonFieldMetadata extends Omit<BaseFieldSchema, "subtype"> {
type: FieldType.JSON
subtype?: JsonFieldSubType.ARRAY
default?: string
}
export interface DateFieldMetadata extends Omit<BaseFieldSchema, "subtype"> {
@ -94,17 +96,25 @@ export interface DateFieldMetadata extends Omit<BaseFieldSchema, "subtype"> {
timeOnly?: boolean
dateOnly?: boolean
subtype?: AutoFieldSubType.CREATED_AT | AutoFieldSubType.UPDATED_AT
default?: string
}
export interface LongFormFieldMetadata extends BaseFieldSchema {
type: FieldType.LONGFORM
useRichText?: boolean | null
default?: string
}
export interface StringFieldMetadata extends BaseFieldSchema {
type: FieldType.STRING
default?: string
}
export interface FormulaFieldMetadata extends BaseFieldSchema {
type: FieldType.FORMULA
formula: string
formulaType?: FormulaType
default?: string
}
export interface BBReferenceFieldMetadata
@ -171,6 +181,7 @@ interface OtherFieldMetadata extends BaseFieldSchema {
| FieldType.BB_REFERENCE
| FieldType.BB_REFERENCE_SINGLE
| FieldType.ATTACHMENTS
| FieldType.STRING
>
}
@ -182,6 +193,7 @@ export type FieldSchema =
| FormulaFieldMetadata
| NumberFieldMetadata
| LongFormFieldMetadata
| StringFieldMetadata
| BBReferenceFieldMetadata
| JsonFieldMetadata
| AttachmentFieldMetadata