Merge branch 'master' into security-updates
This commit is contained in:
commit
40e75645b1
|
@ -371,6 +371,7 @@
|
||||||
delete editableColumn.relationshipType
|
delete editableColumn.relationshipType
|
||||||
delete editableColumn.formulaType
|
delete editableColumn.formulaType
|
||||||
delete editableColumn.constraints
|
delete editableColumn.constraints
|
||||||
|
delete editableColumn.responseType
|
||||||
|
|
||||||
// Add in defaults and initial definition
|
// Add in defaults and initial definition
|
||||||
const definition = fieldDefinitions[type?.toUpperCase()]
|
const definition = fieldDefinitions[type?.toUpperCase()]
|
||||||
|
@ -386,6 +387,7 @@
|
||||||
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||||
} else if (editableColumn.type === FieldType.FORMULA) {
|
} else if (editableColumn.type === FieldType.FORMULA) {
|
||||||
editableColumn.formulaType = "dynamic"
|
editableColumn.formulaType = "dynamic"
|
||||||
|
editableColumn.responseType = field.responseType || FIELDS.STRING.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -767,6 +769,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="split-label">
|
||||||
|
<div class="label-length">
|
||||||
|
<Label size="M">Response Type</Label>
|
||||||
|
</div>
|
||||||
|
<div class="input-length">
|
||||||
|
<Select
|
||||||
|
bind:value={editableColumn.responseType}
|
||||||
|
options={[
|
||||||
|
FIELDS.STRING,
|
||||||
|
FIELDS.NUMBER,
|
||||||
|
FIELDS.BOOLEAN,
|
||||||
|
FIELDS.DATETIME,
|
||||||
|
]}
|
||||||
|
getOptionLabel={option => option.name}
|
||||||
|
getOptionValue={option => option.type}
|
||||||
|
tooltip="Formulas by default will return a string - however if you need another type the response can be coerced."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="split-label">
|
<div class="split-label">
|
||||||
<div class="label-length">
|
<div class="label-length">
|
||||||
<Label size="M">Formula</Label>
|
<Label size="M">Formula</Label>
|
||||||
|
|
|
@ -1,5 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import TextCell from "./TextCell.svelte"
|
import TextCell from "./TextCell.svelte"
|
||||||
|
import DateCell from "./DateCell.svelte"
|
||||||
|
import NumberCell from "./NumberCell.svelte"
|
||||||
|
import BooleanCell from "./BooleanCell.svelte"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
|
|
||||||
|
export let schema
|
||||||
|
|
||||||
|
$: responseType = schema.responseType
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if responseType === FieldType.NUMBER}
|
||||||
|
<NumberCell {...$$props} readonly />
|
||||||
|
{:else if responseType === FieldType.BOOLEAN}
|
||||||
|
<BooleanCell {...$$props} readonly />
|
||||||
|
{:else if responseType === FieldType.DATETIME}
|
||||||
|
<DateCell {...$$props} readonly />
|
||||||
|
{:else}
|
||||||
<TextCell {...$$props} readonly />
|
<TextCell {...$$props} readonly />
|
||||||
|
{/if}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
JsonFieldSubType,
|
JsonFieldSubType,
|
||||||
RowExportFormat,
|
RowExportFormat,
|
||||||
RelationSchemaField,
|
RelationSchemaField,
|
||||||
|
FormulaResponseType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
import { generator, mocks } from "@budibase/backend-core/tests"
|
||||||
import _, { merge } from "lodash"
|
import _, { merge } from "lodash"
|
||||||
|
@ -40,6 +41,7 @@ import { Knex } from "knex"
|
||||||
import { InternalTables } from "../../../db/utils"
|
import { InternalTables } from "../../../db/utils"
|
||||||
import { withEnv } from "../../../environment"
|
import { withEnv } from "../../../environment"
|
||||||
import { JsTimeoutError } from "@budibase/string-templates"
|
import { JsTimeoutError } from "@budibase/string-templates"
|
||||||
|
import { isDate } from "../../../utilities"
|
||||||
|
|
||||||
jest.mock("@budibase/pro", () => ({
|
jest.mock("@budibase/pro", () => ({
|
||||||
...jest.requireActual("@budibase/pro"),
|
...jest.requireActual("@budibase/pro"),
|
||||||
|
@ -79,6 +81,10 @@ async function waitForEvent(
|
||||||
return await p
|
return await p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeJS(binding: string) {
|
||||||
|
return `{{ js "${Buffer.from(binding).toString("base64")}"}}`
|
||||||
|
}
|
||||||
|
|
||||||
datasourceDescribe(
|
datasourceDescribe(
|
||||||
{ name: "/rows (%s)", exclude: [DatabaseName.MONGODB] },
|
{ name: "/rows (%s)", exclude: [DatabaseName.MONGODB] },
|
||||||
({ config, dsProvider, isInternal, isMSSQL, isOracle }) => {
|
({ config, dsProvider, isInternal, isMSSQL, isOracle }) => {
|
||||||
|
@ -3199,7 +3205,7 @@ datasourceDescribe(
|
||||||
describe("Formula fields", () => {
|
describe("Formula fields", () => {
|
||||||
let table: Table
|
let table: Table
|
||||||
let otherTable: Table
|
let otherTable: Table
|
||||||
let relatedRow: Row
|
let relatedRow: Row, mainRow: Row
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
otherTable = await config.api.table.save(defaultTable())
|
otherTable = await config.api.table.save(defaultTable())
|
||||||
|
@ -3227,7 +3233,7 @@ datasourceDescribe(
|
||||||
name: generator.word(),
|
name: generator.word(),
|
||||||
description: generator.paragraph(),
|
description: generator.paragraph(),
|
||||||
})
|
})
|
||||||
await config.api.row.save(table._id!, {
|
mainRow = await config.api.row.save(table._id!, {
|
||||||
name: generator.word(),
|
name: generator.word(),
|
||||||
description: generator.paragraph(),
|
description: generator.paragraph(),
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
|
@ -3235,6 +3241,25 @@ datasourceDescribe(
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function updateFormulaColumn(
|
||||||
|
formula: string,
|
||||||
|
opts?: { responseType?: FormulaResponseType; formulaType?: FormulaType }
|
||||||
|
) {
|
||||||
|
table = await config.api.table.save({
|
||||||
|
...table,
|
||||||
|
schema: {
|
||||||
|
...table.schema,
|
||||||
|
formula: {
|
||||||
|
name: "formula",
|
||||||
|
type: FieldType.FORMULA,
|
||||||
|
formula: formula,
|
||||||
|
responseType: opts?.responseType,
|
||||||
|
formulaType: opts?.formulaType || FormulaType.DYNAMIC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
it("should be able to search for rows containing formulas", async () => {
|
it("should be able to search for rows containing formulas", async () => {
|
||||||
const { rows } = await config.api.row.search(table._id!)
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
expect(rows.length).toBe(1)
|
expect(rows.length).toBe(1)
|
||||||
|
@ -3242,12 +3267,72 @@ datasourceDescribe(
|
||||||
const row = rows[0]
|
const row = rows[0]
|
||||||
expect(row.formula).toBe(relatedRow.name)
|
expect(row.formula).toBe(relatedRow.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should coerce - number response type", async () => {
|
||||||
|
await updateFormulaColumn(encodeJS("return 1"), {
|
||||||
|
responseType: FieldType.NUMBER,
|
||||||
|
})
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows[0].formula).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should coerce - boolean response type", async () => {
|
||||||
|
await updateFormulaColumn(encodeJS("return true"), {
|
||||||
|
responseType: FieldType.BOOLEAN,
|
||||||
|
})
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows[0].formula).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should coerce - datetime response type", async () => {
|
||||||
|
await updateFormulaColumn(encodeJS("return new Date()"), {
|
||||||
|
responseType: FieldType.DATETIME,
|
||||||
|
})
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(isDate(rows[0].formula)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should coerce - datetime with invalid value", async () => {
|
||||||
|
await updateFormulaColumn(encodeJS("return 'a'"), {
|
||||||
|
responseType: FieldType.DATETIME,
|
||||||
|
})
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows[0].formula).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should coerce handlebars", async () => {
|
||||||
|
await updateFormulaColumn("{{ add 1 1 }}", {
|
||||||
|
responseType: FieldType.NUMBER,
|
||||||
|
})
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows[0].formula).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should coerce handlebars to string (default)", async () => {
|
||||||
|
await updateFormulaColumn("{{ add 1 1 }}", {
|
||||||
|
responseType: FieldType.STRING,
|
||||||
|
})
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows[0].formula).toBe("2")
|
||||||
|
})
|
||||||
|
|
||||||
|
isInternal &&
|
||||||
|
it("should coerce a static handlebars formula", async () => {
|
||||||
|
await updateFormulaColumn(encodeJS("return 1"), {
|
||||||
|
responseType: FieldType.NUMBER,
|
||||||
|
formulaType: FormulaType.STATIC,
|
||||||
|
})
|
||||||
|
// save the row to store the static value
|
||||||
|
await config.api.row.save(table._id!, mainRow)
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows[0].formula).toBe(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Formula JS protection", () => {
|
describe("Formula JS protection", () => {
|
||||||
it("should time out JS execution if a single cell takes too long", async () => {
|
it("should time out JS execution if a single cell takes too long", async () => {
|
||||||
await withEnv({ JS_PER_INVOCATION_TIMEOUT_MS: 40 }, async () => {
|
await withEnv({ JS_PER_INVOCATION_TIMEOUT_MS: 40 }, async () => {
|
||||||
const js = Buffer.from(
|
const js = encodeJS(
|
||||||
`
|
`
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -3255,7 +3340,7 @@ datasourceDescribe(
|
||||||
}
|
}
|
||||||
return i;
|
return i;
|
||||||
`
|
`
|
||||||
).toString("base64")
|
)
|
||||||
|
|
||||||
const table = await config.api.table.save(
|
const table = await config.api.table.save(
|
||||||
saveTableRequest({
|
saveTableRequest({
|
||||||
|
@ -3267,7 +3352,7 @@ datasourceDescribe(
|
||||||
formula: {
|
formula: {
|
||||||
name: "formula",
|
name: "formula",
|
||||||
type: FieldType.FORMULA,
|
type: FieldType.FORMULA,
|
||||||
formula: `{{ js "${js}"}}`,
|
formula: js,
|
||||||
formulaType: FormulaType.DYNAMIC,
|
formulaType: FormulaType.DYNAMIC,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3290,7 +3375,7 @@ datasourceDescribe(
|
||||||
JS_PER_REQUEST_TIMEOUT_MS: 80,
|
JS_PER_REQUEST_TIMEOUT_MS: 80,
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const js = Buffer.from(
|
const js = encodeJS(
|
||||||
`
|
`
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -3298,7 +3383,7 @@ datasourceDescribe(
|
||||||
}
|
}
|
||||||
return i;
|
return i;
|
||||||
`
|
`
|
||||||
).toString("base64")
|
)
|
||||||
|
|
||||||
const table = await config.api.table.save(
|
const table = await config.api.table.save(
|
||||||
saveTableRequest({
|
saveTableRequest({
|
||||||
|
@ -3310,7 +3395,7 @@ datasourceDescribe(
|
||||||
formula: {
|
formula: {
|
||||||
name: "formula",
|
name: "formula",
|
||||||
type: FieldType.FORMULA,
|
type: FieldType.FORMULA,
|
||||||
formula: `{{ js "${js}"}}`,
|
formula: js,
|
||||||
formulaType: FormulaType.DYNAMIC,
|
formulaType: FormulaType.DYNAMIC,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3352,7 +3437,7 @@ datasourceDescribe(
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not carry over context between formulas", async () => {
|
it("should not carry over context between formulas", async () => {
|
||||||
const js = Buffer.from(`return $("[text]");`).toString("base64")
|
const js = encodeJS(`return $("[text]");`)
|
||||||
const table = await config.api.table.save(
|
const table = await config.api.table.save(
|
||||||
saveTableRequest({
|
saveTableRequest({
|
||||||
schema: {
|
schema: {
|
||||||
|
@ -3363,7 +3448,7 @@ datasourceDescribe(
|
||||||
formula: {
|
formula: {
|
||||||
name: "formula",
|
name: "formula",
|
||||||
type: FieldType.FORMULA,
|
type: FieldType.FORMULA,
|
||||||
formula: `{{ js "${js}"}}`,
|
formula: js,
|
||||||
formulaType: FormulaType.DYNAMIC,
|
formulaType: FormulaType.DYNAMIC,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -161,33 +161,33 @@ async function processDefaultValues(table: Table, row: Row) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will coerce a value to the correct types based on the type transform map
|
* This will coerce a value to the correct types based on the type transform map
|
||||||
* @param row The value to coerce
|
* @param value The value to coerce
|
||||||
* @param type The type fo coerce to
|
* @param type The type fo coerce to
|
||||||
* @returns The coerced value
|
* @returns The coerced value
|
||||||
*/
|
*/
|
||||||
export function coerce(row: any, type: string) {
|
export function coerce(value: unknown, type: string) {
|
||||||
// no coercion specified for type, skip it
|
// no coercion specified for type, skip it
|
||||||
if (!TYPE_TRANSFORM_MAP[type]) {
|
if (!TYPE_TRANSFORM_MAP[type]) {
|
||||||
return row
|
return value
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
|
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(value)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return TYPE_TRANSFORM_MAP[type][row]
|
return TYPE_TRANSFORM_MAP[type][value]
|
||||||
} else if (TYPE_TRANSFORM_MAP[type].parse) {
|
} else if (TYPE_TRANSFORM_MAP[type].parse) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return TYPE_TRANSFORM_MAP[type].parse(row)
|
return TYPE_TRANSFORM_MAP[type].parse(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return row
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an input route this function will apply all the necessary pre-processing to it, such as coercion
|
* Given an input route this function will apply all the necessary pre-processing to it, such as coercion
|
||||||
* of column values or adding auto-column values.
|
* of column values or adding auto-column values.
|
||||||
* @param user the user which is performing the input.
|
* @param userId the ID of the user which is performing the input.
|
||||||
* @param row the row which is being created/updated.
|
* @param row the row which is being created/updated.
|
||||||
* @param table the table which the row is being saved to.
|
* @param source the table/view which the row is being saved to.
|
||||||
* @param opts some input processing options (like disabling auto-column relationships).
|
* @param opts some input processing options (like disabling auto-column relationships).
|
||||||
* @returns the row which has been prepared to be written to the DB.
|
* @returns the row which has been prepared to be written to the DB.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -10,11 +10,13 @@ import {
|
||||||
FieldType,
|
FieldType,
|
||||||
OperationFieldTypeEnum,
|
OperationFieldTypeEnum,
|
||||||
AIOperationEnum,
|
AIOperationEnum,
|
||||||
|
AIFieldMetadata,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { OperationFields } from "@budibase/shared-core"
|
import { OperationFields } from "@budibase/shared-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import * as pro from "@budibase/pro"
|
import * as pro from "@budibase/pro"
|
||||||
|
import { coerce } from "./index"
|
||||||
|
|
||||||
interface FormulaOpts {
|
interface FormulaOpts {
|
||||||
dynamic?: boolean
|
dynamic?: boolean
|
||||||
|
@ -67,7 +69,18 @@ export async function processFormulas<T extends Row | Row[]>(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responseType = schema.responseType
|
||||||
const isStatic = schema.formulaType === FormulaType.STATIC
|
const isStatic = schema.formulaType === FormulaType.STATIC
|
||||||
|
const formula = schema.formula
|
||||||
|
|
||||||
|
// coerce static values
|
||||||
|
if (isStatic) {
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (row[column] && responseType) {
|
||||||
|
row[column] = coerce(row[column], responseType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
schema.formula == null ||
|
schema.formula == null ||
|
||||||
|
@ -80,12 +93,18 @@ export async function processFormulas<T extends Row | Row[]>(
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
let row = rows[i]
|
let row = rows[i]
|
||||||
let context = contextRows ? contextRows[i] : row
|
let context = contextRows ? contextRows[i] : row
|
||||||
let formula = schema.formula
|
|
||||||
rows[i] = {
|
rows[i] = {
|
||||||
...row,
|
...row,
|
||||||
[column]: tracer.trace("processStringSync", {}, span => {
|
[column]: tracer.trace("processStringSync", {}, span => {
|
||||||
span?.addTags({ table_id: table._id, column, static: isStatic })
|
span?.addTags({ table_id: table._id, column, static: isStatic })
|
||||||
return processStringSync(formula, context)
|
const result = processStringSync(formula, context)
|
||||||
|
try {
|
||||||
|
return responseType ? coerce(result, responseType) : result
|
||||||
|
} catch (err: any) {
|
||||||
|
// if the coercion fails, we return empty row contents
|
||||||
|
span?.addTags({ coercionError: err.message })
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,12 +136,13 @@ export async function processAIColumns<T extends Row | Row[]>(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const operation = schema.operation
|
||||||
|
const aiSchema: AIFieldMetadata = schema
|
||||||
const rowUpdates = rows.map((row, i) => {
|
const rowUpdates = rows.map((row, i) => {
|
||||||
const contextRow = contextRows ? contextRows[i] : row
|
const contextRow = contextRows ? contextRows[i] : row
|
||||||
|
|
||||||
// Check if the type is bindable and pass through HBS if so
|
// Check if the type is bindable and pass through HBS if so
|
||||||
const operationField =
|
const operationField = OperationFields[operation as AIOperationEnum]
|
||||||
OperationFields[schema.operation as AIOperationEnum]
|
|
||||||
for (const key in schema) {
|
for (const key in schema) {
|
||||||
const fieldType = operationField[key as keyof typeof operationField]
|
const fieldType = operationField[key as keyof typeof operationField]
|
||||||
if (fieldType === OperationFieldTypeEnum.BINDABLE_TEXT) {
|
if (fieldType === OperationFieldTypeEnum.BINDABLE_TEXT) {
|
||||||
|
@ -131,7 +151,10 @@ export async function processAIColumns<T extends Row | Row[]>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = llm.buildPromptFromAIOperation({ schema, row })
|
const prompt = llm.buildPromptFromAIOperation({
|
||||||
|
schema: aiSchema,
|
||||||
|
row,
|
||||||
|
})
|
||||||
|
|
||||||
return tracer.trace("processAIColumn", {}, async span => {
|
return tracer.trace("processAIColumn", {}, async span => {
|
||||||
span?.addTags({ table_id: table._id, column })
|
span?.addTags({ table_id: table._id, column })
|
||||||
|
|
|
@ -134,6 +134,12 @@ export const JsonTypes = [
|
||||||
FieldType.ARRAY,
|
FieldType.ARRAY,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export type FormulaResponseType =
|
||||||
|
| FieldType.STRING
|
||||||
|
| FieldType.NUMBER
|
||||||
|
| FieldType.BOOLEAN
|
||||||
|
| FieldType.DATETIME
|
||||||
|
|
||||||
export const NumericTypes = [FieldType.NUMBER, FieldType.BIGINT]
|
export const NumericTypes = [FieldType.NUMBER, FieldType.BIGINT]
|
||||||
|
|
||||||
export function isNumeric(type: FieldType) {
|
export function isNumeric(type: FieldType) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// all added by grid/table when defining the
|
// all added by grid/table when defining the
|
||||||
// column size, position and whether it can be viewed
|
// column size, position and whether it can be viewed
|
||||||
import { FieldType } from "../row"
|
import { FieldType, FormulaResponseType } from "../row"
|
||||||
import {
|
import {
|
||||||
AutoFieldSubType,
|
AutoFieldSubType,
|
||||||
AutoReason,
|
AutoReason,
|
||||||
|
@ -115,6 +115,7 @@ export interface FormulaFieldMetadata extends BaseFieldSchema {
|
||||||
type: FieldType.FORMULA
|
type: FieldType.FORMULA
|
||||||
formula: string
|
formula: string
|
||||||
formulaType?: FormulaType
|
formulaType?: FormulaType
|
||||||
|
responseType?: FormulaResponseType
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIFieldMetadata extends BaseFieldSchema {
|
export interface AIFieldMetadata extends BaseFieldSchema {
|
||||||
|
|
Loading…
Reference in New Issue