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:
commit
2d976bd439
|
@ -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
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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]: {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue