Update how snippets are fetched and enriched into context, because HBS helpers can't be async

This commit is contained in:
Andrew Kingston 2024-03-12 17:02:01 +00:00
parent 10c581c3be
commit 16ce5ac65e
8 changed files with 66 additions and 47 deletions

View File

@ -10,7 +10,7 @@ import {
StaticDatabases, StaticDatabases,
DEFAULT_TENANT_ID, DEFAULT_TENANT_ID,
} from "../constants" } from "../constants"
import { Database, IdentityContext } from "@budibase/types" import { Database, IdentityContext, Snippet, App } from "@budibase/types"
import { ContextMap } from "./types" import { ContextMap } from "./types"
let TEST_APP_ID: string | null = null let TEST_APP_ID: string | null = null
@ -281,6 +281,27 @@ export function doInScimContext(task: any) {
return newContext(updates, task) return newContext(updates, task)
} }
export async function ensureSnippetContext() {
const ctx = getCurrentContext()
// If we've already added snippets to context, continue
if (!ctx || ctx.snippets) {
return
}
// Otherwise get snippets for this app and update context
let snippets: Snippet[] | undefined
const db = getAppDB()
if (db) {
const app = await db.get<App>(DocumentType.APP_METADATA)
snippets = app.snippets
}
// Always set snippets to a non-null value so that we can tell we've attempted
// to load snippets
ctx.snippets = snippets || []
}
export function getEnvironmentVariables() { export function getEnvironmentVariables() {
const context = Context.get() const context = Context.get()
if (!context.environmentVariables) { if (!context.environmentVariables) {

View File

@ -1,4 +1,4 @@
import { IdentityContext, VM } from "@budibase/types" import { IdentityContext, Snippet, VM } from "@budibase/types"
import { ExecutionTimeTracker } from "../timers" import { ExecutionTimeTracker } from "../timers"
// keep this out of Budibase types, don't want to expose context info // keep this out of Budibase types, don't want to expose context info
@ -12,4 +12,5 @@ export type ContextMap = {
isMigrating?: boolean isMigrating?: boolean
jsExecutionTracker?: ExecutionTimeTracker jsExecutionTracker?: ExecutionTimeTracker
vm?: VM vm?: VM
snippets?: Snippet[]
} }

View File

@ -437,11 +437,11 @@ export class ExternalRequest<T extends Operation> {
return { row: newRow, manyRelationships } return { row: newRow, manyRelationships }
} }
processRelationshipFields( async processRelationshipFields(
table: Table, table: Table,
row: Row, row: Row,
relationships: RelationshipsJson[] relationships: RelationshipsJson[]
): Row { ): Promise<Row> {
for (let relationship of relationships) { for (let relationship of relationships) {
const linkedTable = this.tables[relationship.tableName] const linkedTable = this.tables[relationship.tableName]
if (!linkedTable || !row[relationship.column]) { if (!linkedTable || !row[relationship.column]) {
@ -457,7 +457,7 @@ export class ExternalRequest<T extends Operation> {
} }
// process additional types // process additional types
relatedRow = processDates(table, relatedRow) relatedRow = processDates(table, relatedRow)
relatedRow = processFormulas(linkedTable, relatedRow) relatedRow = await processFormulas(linkedTable, relatedRow)
row[relationship.column][key] = relatedRow row[relationship.column][key] = relatedRow
} }
} }
@ -521,7 +521,7 @@ export class ExternalRequest<T extends Operation> {
return rows return rows
} }
outputProcessing( async outputProcessing(
rows: Row[] = [], rows: Row[] = [],
table: Table, table: Table,
relationships: RelationshipsJson[] relationships: RelationshipsJson[]
@ -561,9 +561,12 @@ export class ExternalRequest<T extends Operation> {
} }
// make sure all related rows are correct // make sure all related rows are correct
let finalRowArray = Object.values(finalRows).map(row => let finalRowArray = []
this.processRelationshipFields(table, row, relationships) for (let row of Object.values(finalRows)) {
finalRowArray.push(
await this.processRelationshipFields(table, row, relationships)
) )
}
// process some additional types // process some additional types
finalRowArray = processDates(table, finalRowArray) finalRowArray = processDates(table, finalRowArray)
@ -934,7 +937,11 @@ export class ExternalRequest<T extends Operation> {
processed.manyRelationships processed.manyRelationships
) )
} }
const output = this.outputProcessing(responseRows, table, relationships) const output = await this.outputProcessing(
responseRows,
table,
relationships
)
// if reading it'll just be an array of rows, return whole thing // if reading it'll just be an array of rows, return whole thing
if (operation === Operation.READ) { if (operation === Operation.READ) {
return ( return (

View File

@ -110,7 +110,7 @@ export async function updateAllFormulasInTable(table: Table) {
(enriched: Row) => enriched._id === row._id (enriched: Row) => enriched._id === row._id
) )
if (enrichedRow) { if (enrichedRow) {
const processed = processFormulas(table, cloneDeep(row), { const processed = await processFormulas(table, cloneDeep(row), {
dynamic: false, dynamic: false,
contextRows: [enrichedRow], contextRows: [enrichedRow],
}) })
@ -143,7 +143,7 @@ export async function finaliseRow(
squash: false, squash: false,
})) as Row })) as Row
// use enriched row to generate formulas for saving, specifically only use as context // use enriched row to generate formulas for saving, specifically only use as context
row = processFormulas(table, row, { row = await processFormulas(table, row, {
dynamic: false, dynamic: false,
contextRows: [enrichedRow], contextRows: [enrichedRow],
}) })
@ -179,7 +179,7 @@ export async function finaliseRow(
const response = await db.put(row) const response = await db.put(row)
// for response, calculate the formulas for the enriched row // for response, calculate the formulas for the enriched row
enrichedRow._rev = response.rev enrichedRow._rev = response.rev
enrichedRow = processFormulas(table, enrichedRow, { enrichedRow = await processFormulas(table, enrichedRow, {
dynamic: false, dynamic: false,
}) })
// this updates the related formulas in other rows based on the relations to this row // this updates the related formulas in other rows based on the relations to this row

View File

@ -202,7 +202,8 @@ export async function attachFullLinkedDocs(
table => table._id === linkedTableId table => table._id === linkedTableId
) )
if (linkedTable) { if (linkedTable) {
row[link.fieldName].push(processFormulas(linkedTable, linkedRow)) const processed = await processFormulas(linkedTable, linkedRow)
row[link.fieldName].push(processed)
} }
} }
} }

View File

@ -8,40 +8,24 @@ import {
import { context, logging } from "@budibase/backend-core" import { context, logging } from "@budibase/backend-core"
import tracer from "dd-trace" import tracer from "dd-trace"
import { IsolatedVM } from "./vm" import { IsolatedVM } from "./vm"
import { App, DocumentType, Snippet, VM } from "@budibase/types"
async function getIsolate(ctx: any): Promise<VM> { export function init() {
// Reuse the existing isolate if one exists setJSRunner((js: string, ctx: Record<string, any>) => {
if (ctx?.vm) { return tracer.trace("runJS", {}, span => {
return ctx.vm try {
} // Reuse an existing isolate from context, or make a new one
const bbCtx = context.getCurrentContext()
// Get snippets to build into new isolate, if inside app context const vm =
let snippets: Snippet[] | undefined bbCtx?.vm ||
const db = context.getAppDB() new IsolatedVM({
if (db) {
console.log("READ APP METADATA")
const app = await db.get<App>(DocumentType.APP_METADATA)
snippets = app.snippets
}
// Build a new isolate
return new IsolatedVM({
memoryLimit: env.JS_RUNNER_MEMORY_LIMIT, memoryLimit: env.JS_RUNNER_MEMORY_LIMIT,
invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS, invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS,
isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS, isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS,
}) })
.withHelpers() .withHelpers()
.withSnippets(snippets) .withSnippets(bbCtx?.snippets)
}
export function init() { // Persist isolate in context so we can reuse it
setJSRunner((js: string, ctx: Record<string, any>) => {
return tracer.trace("runJS", {}, async span => {
try {
// Reuse an existing isolate from context, or make a new one
const bbCtx = context.getCurrentContext()
const vm = await getIsolate(bbCtx)
if (bbCtx) { if (bbCtx) {
bbCtx.vm = vm bbCtx.vm = vm
} }
@ -49,7 +33,7 @@ export function init() {
// Strip helpers (an array) and snippets (a proxy isntance) as these // Strip helpers (an array) and snippets (a proxy isntance) as these
// will not survive the isolated-vm barrier // will not survive the isolated-vm barrier
const { helpers, snippets, ...rest } = ctx const { helpers, snippets, ...rest } = ctx
return vm.withContext(rest, () => vm!.execute(js)) return vm.withContext(rest, () => vm.execute(js))
} catch (error: any) { } catch (error: any) {
if (error.message === "Script execution timed out.") { if (error.message === "Script execution timed out.") {
throw new JsErrorTimeout() throw new JsErrorTimeout()

View File

@ -245,7 +245,7 @@ export async function outputProcessing<T extends Row[] | Row>(
} }
// process formulas after the complex types had been processed // process formulas after the complex types had been processed
enriched = processFormulas(table, enriched, { dynamic: true }) enriched = await processFormulas(table, enriched, { dynamic: true })
if (opts.squash) { if (opts.squash) {
enriched = (await linkRows.squashLinksToPrimaryDisplay( enriched = (await linkRows.squashLinksToPrimaryDisplay(

View File

@ -10,6 +10,8 @@ import {
FieldType, FieldType,
} from "@budibase/types" } from "@budibase/types"
import tracer from "dd-trace" import tracer from "dd-trace"
import { context } from "@budibase/backend-core"
import { getCurrentContext } from "@budibase/backend-core/src/context"
interface FormulaOpts { interface FormulaOpts {
dynamic?: boolean dynamic?: boolean
@ -44,16 +46,19 @@ export function fixAutoColumnSubType(
/** /**
* Looks through the rows provided and finds formulas - which it then processes. * Looks through the rows provided and finds formulas - which it then processes.
*/ */
export function processFormulas<T extends Row | Row[]>( export async function processFormulas<T extends Row | Row[]>(
table: Table, table: Table,
inputRows: T, inputRows: T,
{ dynamic, contextRows }: FormulaOpts = { dynamic: true } { dynamic, contextRows }: FormulaOpts = { dynamic: true }
): T { ): Promise<T> {
return tracer.trace("processFormulas", {}, span => { return tracer.trace("processFormulas", {}, async span => {
const numRows = Array.isArray(inputRows) ? inputRows.length : 1 const numRows = Array.isArray(inputRows) ? inputRows.length : 1
span?.addTags({ table_id: table._id, dynamic, numRows }) span?.addTags({ table_id: table._id, dynamic, numRows })
const rows = Array.isArray(inputRows) ? inputRows : [inputRows] const rows = Array.isArray(inputRows) ? inputRows : [inputRows]
if (rows) { if (rows) {
// Ensure we have snippet context
await context.ensureSnippetContext()
for (let [column, schema] of Object.entries(table.schema)) { for (let [column, schema] of Object.entries(table.schema)) {
if (schema.type !== FieldType.FORMULA) { if (schema.type !== FieldType.FORMULA) {
continue continue