From ec1e145eb54a919ea8c9052a0a2778d711840e6f Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 15 Jan 2025 18:06:49 +0000 Subject: [PATCH 01/39] Formatting for log lines. --- .../bindings/EvaluationSidePanel.svelte | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index c8bf5529ad..54f7dc7d01 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -11,11 +11,16 @@ export let expressionError: string | undefined = undefined export let evaluating = false export let expression: string | null = null + export let logging: { log: string; line?: number }[] = [] $: error = expressionError != null $: empty = expression == null || expression?.trim() === "" $: success = !error && !empty $: highlightedResult = highlight(expressionResult) + $: highlightedLogs = logging.map(l => ({ + log: highlight(l.log), + line: l.line, + })) const formatError = (err: any) => { if (err.code === UserScriptError.code) { @@ -25,14 +30,14 @@ } // json can be any primitive type - const highlight = (json?: any | null) => { + const highlight = (json?: JSONValue | null) => { if (json == null) { return "" } // Attempt to parse and then stringify, in case this is valid result try { - json = JSON.stringify(JSON.parse(json), null, 2) + json = JSON.stringify(JSON.parse(json as any), null, 2) } catch (err) { // couldn't parse/stringify, just treat it as the raw input } @@ -90,8 +95,21 @@ {:else if error} {formatError(expressionError)} {:else} - - {@html highlightedResult} +
+ {#each highlightedLogs as logLine} +
+ + {@html logLine.log} + {#if logLine.line} + line {logLine.line} + {/if} +
+ {/each} +
+ + {@html highlightedResult} +
+
{/if} @@ -142,8 +160,21 @@ font-size: 12px; overflow-y: scroll; overflow-x: hidden; - white-space: pre-wrap; + white-space: pre-line; word-wrap: break-word; height: 0; } + .output-lines { + display: flex; + gap: var(--spacing-s); + flex-direction: column; + } + .line { + border-bottom: var(--border-light); + padding-bottom: var(--spacing-s); + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: end; + } From 6f9f36f9eb8d1c13a0c060762698c2a9f167c028 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 15 Jan 2025 18:38:23 +0000 Subject: [PATCH 02/39] Getting string templates ready. --- .../src/helpers/javascript.ts | 22 +++++++++++++-- .../string-templates/src/processors/index.ts | 28 +++++++++++++++---- .../src/processors/postprocessor.ts | 28 +++++++++++-------- .../src/processors/preprocessor.ts | 7 +++-- packages/string-templates/src/types.ts | 5 ++++ 5 files changed, 69 insertions(+), 21 deletions(-) diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index 4fb1329196..a0fdd3cbe5 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -4,6 +4,7 @@ import { getJsHelperList } from "./list" import { iifeWrapper } from "../iife" import { JsTimeoutError, UserScriptError } from "../errors" import { cloneDeep } from "lodash/fp" +import { Log } from "../types" // The method of executing JS scripts depends on the bundle being built. // This setter is used in the entrypoint (either index.js or index.mjs). @@ -96,10 +97,9 @@ export function processJS(handlebars: string, context: any) { clonedContext = cloneDeep(context) } - const sandboxContext = { + const sandboxContext: Record = { $: (path: string) => getContextValue(path, clonedContext), helpers: getJsHelperList(), - // Proxy to evaluate snippets when running in the browser snippets: new Proxy( {}, @@ -114,8 +114,24 @@ export function processJS(handlebars: string, context: any) { ), } + const logs: Log[] = [] + // logging only supported on frontend + if (!isBackendService()) { + const log = (log: string) => logs.push({ log }) + sandboxContext.console = { + log: log, + info: log, + debug: log, + warn: log, + error: log, + // two below may need special cases + trace: log, + table: log, + } + } + // Create a sandbox with our context and run the JS - const res = { data: runJS(js, sandboxContext) } + const res = { data: runJS(js, sandboxContext), logs } return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}` } catch (error: any) { onErrorLog && onErrorLog(error) diff --git a/packages/string-templates/src/processors/index.ts b/packages/string-templates/src/processors/index.ts index 79085b0dfe..4454d02738 100644 --- a/packages/string-templates/src/processors/index.ts +++ b/packages/string-templates/src/processors/index.ts @@ -1,9 +1,16 @@ import { FIND_HBS_REGEX } from "../utilities" import * as preprocessor from "./preprocessor" +import type { Preprocessor } from "./preprocessor" import * as postprocessor from "./postprocessor" -import { ProcessOptions } from "../types" +import type { Postprocessor } from "./postprocessor" +import { Log, ProcessOptions } from "../types" -function process(output: string, processors: any[], opts?: ProcessOptions) { +function process( + output: string, + processors: (Preprocessor | Postprocessor)[], + opts?: ProcessOptions +) { + let logs: Log[] = [] for (let processor of processors) { // if a literal statement has occurred stop if (typeof output !== "string") { @@ -16,10 +23,16 @@ function process(output: string, processors: any[], opts?: ProcessOptions) { continue } for (let match of matches) { - output = processor.process(output, match, opts) + const res = processor.process(output, match, opts || {}) + if (typeof res === "object" && "logs" in res && res.logs) { + logs = logs.concat(res.logs) + output = res.result + } else { + output = res as string + } } } - return output + return { result: output, logs } } export function preprocess(string: string, opts: ProcessOptions) { @@ -30,8 +43,13 @@ export function preprocess(string: string, opts: ProcessOptions) { ) } - return process(string, processors, opts) + return process(string, processors, opts).result } + export function postprocess(string: string) { + return process(string, postprocessor.processors).result +} + +export function postprocessWithLogs(string: string) { return process(string, postprocessor.processors) } diff --git a/packages/string-templates/src/processors/postprocessor.ts b/packages/string-templates/src/processors/postprocessor.ts index 6ddc0e67cd..49d5f7d1cf 100644 --- a/packages/string-templates/src/processors/postprocessor.ts +++ b/packages/string-templates/src/processors/postprocessor.ts @@ -1,12 +1,16 @@ import { LITERAL_MARKER } from "../helpers/constants" +import { Log } from "../types" export enum PostProcessorNames { CONVERT_LITERALS = "convert-literals", } -type PostprocessorFn = (statement: string) => string +export type PostprocessorFn = (statement: string) => { + result: any + logs?: Log[] +} -class Postprocessor { +export class Postprocessor { name: PostProcessorNames private readonly fn: PostprocessorFn @@ -23,12 +27,12 @@ class Postprocessor { export const processors = [ new Postprocessor( PostProcessorNames.CONVERT_LITERALS, - (statement: string) => { + (statement: string): { result: any; logs?: Log[] } => { if ( typeof statement !== "string" || !statement.includes(LITERAL_MARKER) ) { - return statement + return { result: statement } } const splitMarkerIndex = statement.indexOf("-") const type = statement.substring(12, splitMarkerIndex) @@ -38,20 +42,22 @@ export const processors = [ ) switch (type) { case "string": - return value + return { result: value } case "number": - return parseFloat(value) + return { result: parseFloat(value) } case "boolean": - return value === "true" + return { result: value === "true" } case "object": - return JSON.parse(value) - case "js_result": + return { result: JSON.parse(value) } + case "js_result": { // We use the literal helper to process the result of JS expressions // as we want to be able to return any types. // We wrap the value in an abject to be able to use undefined properly. - return JSON.parse(value).data + const parsed = JSON.parse(value) + return { result: parsed.data, logs: parsed.logs } + } } - return value + return { result: value } } ), ] diff --git a/packages/string-templates/src/processors/preprocessor.ts b/packages/string-templates/src/processors/preprocessor.ts index 97e5c56fcc..37981f31a8 100644 --- a/packages/string-templates/src/processors/preprocessor.ts +++ b/packages/string-templates/src/processors/preprocessor.ts @@ -11,9 +11,12 @@ export enum PreprocessorNames { NORMALIZE_SPACES = "normalize-spaces", } -type PreprocessorFn = (statement: string, opts?: ProcessOptions) => string +export type PreprocessorFn = ( + statement: string, + opts?: ProcessOptions +) => string -class Preprocessor { +export class Preprocessor { name: string private readonly fn: PreprocessorFn diff --git a/packages/string-templates/src/types.ts b/packages/string-templates/src/types.ts index 2a7a430bee..c973142c93 100644 --- a/packages/string-templates/src/types.ts +++ b/packages/string-templates/src/types.ts @@ -8,3 +8,8 @@ export interface ProcessOptions { onlyFound?: boolean disabledHelpers?: string[] } + +export interface Log { + log: string + line?: number +} From 28958f5a1ce32b46bd6415de0c6d10d55f27a3eb Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 16 Jan 2025 15:29:50 +0000 Subject: [PATCH 03/39] Expose the ability to get logs. --- packages/string-templates/src/index.ts | 82 ++++++++++++++++++++------ 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 7246bc4942..fafff5d9fb 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -1,7 +1,7 @@ import { createContext, runInNewContext } from "vm" import { create, TemplateDelegate } from "handlebars" import { registerAll, registerMinimum } from "./helpers/index" -import { postprocess, preprocess } from "./processors" +import { postprocess, postprocessWithLogs, preprocess } from "./processors" import { atob, btoa, @@ -15,7 +15,7 @@ import { convertHBSBlock } from "./conversion" import { removeJSRunner, setJSRunner } from "./helpers/javascript" import manifest from "./manifest.json" -import { ProcessOptions } from "./types" +import { Log, ProcessOptions } from "./types" import { UserScriptError } from "./errors" export { helpersToRemoveForJs, getJsHelperList } from "./helpers/list" @@ -187,23 +187,27 @@ export function processObjectSync( return object } -/** - * This will process a single handlebars containing string. If the string passed in has no valid handlebars statements - * then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call. - * @param {string} string The template string which is the filled from the context object. - * @param {object} context An object of information which will be used to enrich the string. - * @param {object|undefined} [opts] optional - specify some options for processing. - * @returns {string} The enriched string, all templates should have been replaced if they can be. - */ -export function processStringSync( +// keep the logging function internal, don't want to add this to the process options directly +// as it can't be used for object processing etc. +function processStringSyncInternal( + str: string, + context?: object, + opts?: ProcessOptions & { logging: false } +): string +function processStringSyncInternal( + str: string, + context?: object, + opts?: ProcessOptions & { logging: true } +): { result: string; logs: Log[] } +function processStringSyncInternal( string: string, context?: object, - opts?: ProcessOptions -): string { + opts?: ProcessOptions & { logging: boolean } +): string | { result: string; logs: Log[] } { // Take a copy of input in case of error const input = string if (typeof string !== "string") { - throw "Cannot process non-string types." + throw new Error("Cannot process non-string types.") } function process(stringPart: string) { // context is needed to check for overlap between helpers and context @@ -217,16 +221,24 @@ export function processStringSync( }, ...context, }) - return postprocess(processedString) + return opts?.logging + ? postprocessWithLogs(processedString) + : postprocess(processedString) } try { if (opts && opts.onlyFound) { + let logs: Log[] = [] const blocks = findHBSBlocks(string) for (let block of blocks) { const outcome = process(block) - string = string.replace(block, outcome) + if (typeof outcome === "object" && "result" in outcome) { + logs = logs.concat(outcome.logs || []) + string = string.replace(block, outcome.result) + } else { + string = string.replace(block, outcome) + } } - return string + return opts?.logging ? string : { result: string, logs } } else { return process(string) } @@ -239,6 +251,42 @@ export function processStringSync( } } +/** + * This will process a single handlebars containing string. If the string passed in has no valid handlebars statements + * then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call. + * @param {string} string The template string which is the filled from the context object. + * @param {object} context An object of information which will be used to enrich the string. + * @param {object|undefined} [opts] optional - specify some options for processing. + * @returns {string} The enriched string, all templates should have been replaced if they can be. + */ +export function processStringSync( + string: string, + context?: object, + opts?: ProcessOptions +): string { + return processStringSyncInternal(string, context, { + ...opts, + logging: false, + }) +} + +/** + * Same as function above, but allows logging to be returned - this is only for JS bindings. + */ +export function processStringWithLogsSync( + string: string, + context?: object, + opts?: ProcessOptions +): { result: string; logs: Log[] } { + if (isBackendService()) { + throw new Error("Logging disabled for backend bindings") + } + return processStringSyncInternal(string, context, { + ...opts, + logging: true, + }) +} + /** * By default with expressions like {{ name }} handlebars will escape various * characters, which can be problematic. To fix this we use the syntax {{{ name }}}, From d3a2306787d15b9051571572c3b12fcdb6295c76 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 16 Jan 2025 15:48:49 +0000 Subject: [PATCH 04/39] Finishing link up of logs. --- .../src/components/common/bindings/BindingPanel.svelte | 9 +++++++-- .../common/bindings/EvaluationSidePanel.svelte | 5 +++-- packages/string-templates/src/index.ts | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 98df69bc06..ffb477012c 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -12,7 +12,7 @@ decodeJSBinding, encodeJSBinding, processObjectSync, - processStringSync, + processStringWithLogsSync, } from "@budibase/string-templates" import { readableToRuntimeBinding } from "@/dataBinding" import CodeEditor from "../CodeEditor/CodeEditor.svelte" @@ -41,6 +41,7 @@ InsertAtPositionFn, JSONValue, } from "@budibase/types" + import type { Log } from "@budibase/string-templates" import type { CompletionContext } from "@codemirror/autocomplete" const dispatch = createEventDispatcher() @@ -66,6 +67,7 @@ let insertAtPos: InsertAtPositionFn | undefined let targetMode: BindingMode | null = null let expressionResult: string | undefined + let expressionLogs: Log[] | undefined let expressionError: string | undefined let evaluating = false @@ -157,7 +159,7 @@ (expression: string | null, context: any, snippets: Snippet[]) => { try { expressionError = undefined - expressionResult = processStringSync( + const output = processStringWithLogsSync( expression || "", { ...context, @@ -167,6 +169,8 @@ noThrow: false, } ) + expressionResult = output.result + expressionLogs = output.logs } catch (err: any) { expressionResult = undefined expressionError = err @@ -421,6 +425,7 @@ diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index 54f7dc7d01..41245af4f9 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -4,20 +4,21 @@ import { Helpers } from "@budibase/bbui" import { fade } from "svelte/transition" import { UserScriptError } from "@budibase/string-templates" + import type { Log } from "@budibase/string-templates" import type { JSONValue } from "@budibase/types" // this can be essentially any primitive response from the JS function export let expressionResult: JSONValue | undefined = undefined export let expressionError: string | undefined = undefined + export let expressionLogs: Log[] = [] export let evaluating = false export let expression: string | null = null - export let logging: { log: string; line?: number }[] = [] $: error = expressionError != null $: empty = expression == null || expression?.trim() === "" $: success = !error && !empty $: highlightedResult = highlight(expressionResult) - $: highlightedLogs = logging.map(l => ({ + $: highlightedLogs = expressionLogs.map(l => ({ log: highlight(l.log), line: l.line, })) diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index fafff5d9fb..553c0e8861 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -18,6 +18,7 @@ import manifest from "./manifest.json" import { Log, ProcessOptions } from "./types" import { UserScriptError } from "./errors" +export type { Log } from "./types" export { helpersToRemoveForJs, getJsHelperList } from "./helpers/list" export { FIND_ANY_HBS_REGEX } from "./utilities" export { setJSRunner, setOnErrorLog } from "./helpers/javascript" From e6d536bcc83e172926fb753ea30ecf95ba2860a1 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 16 Jan 2025 18:15:54 +0000 Subject: [PATCH 05/39] Getting the line number calculated correctly, as well as adding some basic test cases. --- .../bindings/EvaluationSidePanel.svelte | 2 +- .../src/helpers/javascript.ts | 23 +++++++++++-- packages/string-templates/src/index.ts | 16 ++------- packages/string-templates/src/utilities.ts | 17 ++++++++++ .../string-templates/test/jsLogging.spec.ts | 33 +++++++++++++++++++ 5 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 packages/string-templates/test/jsLogging.spec.ts diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index 41245af4f9..984fba9b7a 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -102,7 +102,7 @@ {@html logLine.log} {#if logLine.line} - line {logLine.line} + :{logLine.line} {/if} {/each} diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index a0fdd3cbe5..a1bfb7a824 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -1,4 +1,9 @@ -import { atob, isBackendService, isJSAllowed } from "../utilities" +import { + atob, + frontendWrapJS, + isBackendService, + isJSAllowed, +} from "../utilities" import { LITERAL_MARKER } from "../helpers/constants" import { getJsHelperList } from "./list" import { iifeWrapper } from "../iife" @@ -117,7 +122,21 @@ export function processJS(handlebars: string, context: any) { const logs: Log[] = [] // logging only supported on frontend if (!isBackendService()) { - const log = (log: string) => logs.push({ log }) + // this counts the lines in the wrapped JS *before* the user's code, so that we can minus it + const jsLineCount = frontendWrapJS(js).split(js)[0].split("\n").length + const log = (log: string) => { + // quick way to find out what line this is being called from + // its an anonymous function and we look for the overall length to find the + // line number we care about (from the users function) + // JS stack traces are in the format function:line:column + const lineNumber = new Error().stack?.match( + /:(\d+):\d+/ + )?.[1] + logs.push({ + log, + line: lineNumber ? parseInt(lineNumber) - jsLineCount : undefined, + }) + } sandboxContext.console = { log: log, info: log, diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 553c0e8861..a21bfdb755 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -8,6 +8,7 @@ import { FIND_ANY_HBS_REGEX, FIND_HBS_REGEX, findDoubleHbsInstances, + frontendWrapJS, isBackendService, prefixStrings, } from "./utilities" @@ -511,20 +512,7 @@ export function browserJSSetup() { setJSRunner((js: string, context: Record) => { createContext(context) - const wrappedJs = ` - result = { - result: null, - error: null, - }; - - try { - result.result = ${js}; - } catch (e) { - result.error = e; - } - - result; - ` + const wrappedJs = frontendWrapJS(js) const result = runInNewContext(wrappedJs, context, { timeout: 1000 }) if (result.error) { diff --git a/packages/string-templates/src/utilities.ts b/packages/string-templates/src/utilities.ts index 779bef3735..dba1faab17 100644 --- a/packages/string-templates/src/utilities.ts +++ b/packages/string-templates/src/utilities.ts @@ -86,3 +86,20 @@ export const prefixStrings = ( const regexPattern = new RegExp(`\\b(${escapedStrings.join("|")})\\b`, "g") return baseString.replace(regexPattern, `${prefix}$1`) } + +export function frontendWrapJS(js: string) { + return ` + result = { + result: null, + error: null, + }; + + try { + result.result = ${js}; + } catch (e) { + result.error = e; + } + + result; + ` +} diff --git a/packages/string-templates/test/jsLogging.spec.ts b/packages/string-templates/test/jsLogging.spec.ts new file mode 100644 index 0000000000..9b2bb945d2 --- /dev/null +++ b/packages/string-templates/test/jsLogging.spec.ts @@ -0,0 +1,33 @@ +import { + processStringWithLogsSync, + encodeJSBinding, + defaultJSSetup, +} from "../src/index" + +const processJS = (js: string, context?: object) => { + return processStringWithLogsSync(encodeJSBinding(js), context) +} + +describe("Javascript", () => { + beforeAll(() => { + defaultJSSetup() + }) + + describe("Test logging in JS bindings", () => { + it("should execute a simple expression", () => { + const output = processJS( + `console.log("hello"); + console.log("world"); + console.log("foo"); + return "hello"` + ) + expect(output.result).toBe("hello") + expect(output.logs[0].log).toBe("hello") + expect(output.logs[0].line).toBe(1) + expect(output.logs[1].log).toBe("world") + expect(output.logs[1].line).toBe(2) + expect(output.logs[2].log).toBe("foo") + expect(output.logs[2].line).toBe(3) + }) + }) +}) From e146d995ebcd89c661e769b21743875ceddd81c0 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 17 Jan 2025 11:06:55 +0000 Subject: [PATCH 06/39] Adding in support for multi-parameter logs and actual logging to console. --- .../bindings/EvaluationSidePanel.svelte | 2 +- .../src/helpers/javascript.ts | 48 +++++++++++-------- packages/string-templates/src/types.ts | 2 +- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index 984fba9b7a..dc3f585033 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -19,7 +19,7 @@ $: success = !error && !empty $: highlightedResult = highlight(expressionResult) $: highlightedLogs = expressionLogs.map(l => ({ - log: highlight(l.log), + log: highlight(l.log.join(", ")), line: l.line, })) diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index a1bfb7a824..5a4f69de5b 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -124,28 +124,38 @@ export function processJS(handlebars: string, context: any) { if (!isBackendService()) { // this counts the lines in the wrapped JS *before* the user's code, so that we can minus it const jsLineCount = frontendWrapJS(js).split(js)[0].split("\n").length - const log = (log: string) => { - // quick way to find out what line this is being called from - // its an anonymous function and we look for the overall length to find the - // line number we care about (from the users function) - // JS stack traces are in the format function:line:column - const lineNumber = new Error().stack?.match( - /:(\d+):\d+/ - )?.[1] - logs.push({ - log, - line: lineNumber ? parseInt(lineNumber) - jsLineCount : undefined, - }) + const buildLogResponse = ( + type: "log" | "info" | "debug" | "warn" | "error" | "trace" | "table" + ) => { + return (...props: any[]) => { + console[type](...props) + props.forEach((prop, index) => { + if (typeof prop === "object") { + props[index] = JSON.stringify(prop) + } + }) + // quick way to find out what line this is being called from + // its an anonymous function and we look for the overall length to find the + // line number we care about (from the users function) + // JS stack traces are in the format function:line:column + const lineNumber = new Error().stack?.match( + /:(\d+):\d+/ + )?.[1] + logs.push({ + log: props, + line: lineNumber ? parseInt(lineNumber) - jsLineCount : undefined, + }) + } } sandboxContext.console = { - log: log, - info: log, - debug: log, - warn: log, - error: log, + log: buildLogResponse("log"), + info: buildLogResponse("info"), + debug: buildLogResponse("debug"), + warn: buildLogResponse("warn"), + error: buildLogResponse("error"), // two below may need special cases - trace: log, - table: log, + trace: buildLogResponse("trace"), + table: buildLogResponse("table"), } } diff --git a/packages/string-templates/src/types.ts b/packages/string-templates/src/types.ts index c973142c93..a32149c8bb 100644 --- a/packages/string-templates/src/types.ts +++ b/packages/string-templates/src/types.ts @@ -10,6 +10,6 @@ export interface ProcessOptions { } export interface Log { - log: string + log: any[] line?: number } From 272bbf5f8bbf4f59307baf525f02f338633b3391 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 17 Jan 2025 17:18:42 +0000 Subject: [PATCH 07/39] Logging with types - allows for coloured outputs. --- packages/bbui/src/bbui.css | 5 +++ .../bindings/EvaluationSidePanel.svelte | 36 ++++++++++++++----- .../src/helpers/javascript.ts | 11 +++--- packages/string-templates/src/index.ts | 2 +- packages/string-templates/src/types.ts | 3 ++ 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/packages/bbui/src/bbui.css b/packages/bbui/src/bbui.css index dd0588818e..810c5ff2c0 100644 --- a/packages/bbui/src/bbui.css +++ b/packages/bbui/src/bbui.css @@ -45,6 +45,11 @@ --purple: #806fde; --purple-dark: #130080; + --error-bg: rgba(226, 109, 105, 0.3); + --warning-bg: rgba(255, 210, 106, 0.3); + --error-content: rgba(226, 109, 105, 0.6); + --warning-content: rgba(255, 210, 106, 0.6); + --rounded-small: 4px; --rounded-medium: 8px; --rounded-large: 16px; diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index dc3f585033..fcd23bb816 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -21,6 +21,7 @@ $: highlightedLogs = expressionLogs.map(l => ({ log: highlight(l.log.join(", ")), line: l.line, + type: l.type, })) const formatError = (err: any) => { @@ -67,7 +68,7 @@
{#if error} - +
Error
{#if evaluating}
@@ -98,9 +99,24 @@ {:else}
{#each highlightedLogs as logLine} -
- - {@html logLine.log} +
+
+ {#if logLine.type === "error"} + + {:else if logLine.type === "warn"} + + {/if} + + {@html logLine.log} +
{#if logLine.line} :{logLine.line} {/if} @@ -149,10 +165,9 @@ height: 100%; z-index: 1; position: absolute; - opacity: 10%; } .header.error::before { - background: var(--spectrum-global-color-red-400); + background: var(--error-bg); } .body { flex: 1 1 auto; @@ -167,15 +182,20 @@ } .output-lines { display: flex; - gap: var(--spacing-s); flex-direction: column; + gap: var(--spacing-xs); } .line { border-bottom: var(--border-light); - padding-bottom: var(--spacing-s); display: flex; flex-direction: row; justify-content: space-between; align-items: end; + padding: var(--spacing-s); + } + .icon-log { + display: flex; + gap: var(--spacing-s); + align-items: start; } diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index 5a4f69de5b..997ca7b6ec 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -9,7 +9,7 @@ import { getJsHelperList } from "./list" import { iifeWrapper } from "../iife" import { JsTimeoutError, UserScriptError } from "../errors" import { cloneDeep } from "lodash/fp" -import { Log } from "../types" +import { Log, LogType } from "../types" // The method of executing JS scripts depends on the bundle being built. // This setter is used in the entrypoint (either index.js or index.mjs). @@ -124,9 +124,7 @@ export function processJS(handlebars: string, context: any) { if (!isBackendService()) { // this counts the lines in the wrapped JS *before* the user's code, so that we can minus it const jsLineCount = frontendWrapJS(js).split(js)[0].split("\n").length - const buildLogResponse = ( - type: "log" | "info" | "debug" | "warn" | "error" | "trace" | "table" - ) => { + const buildLogResponse = (type: LogType) => { return (...props: any[]) => { console[type](...props) props.forEach((prop, index) => { @@ -144,6 +142,7 @@ export function processJS(handlebars: string, context: any) { logs.push({ log: props, line: lineNumber ? parseInt(lineNumber) - jsLineCount : undefined, + type, }) } } @@ -153,8 +152,8 @@ export function processJS(handlebars: string, context: any) { debug: buildLogResponse("debug"), warn: buildLogResponse("warn"), error: buildLogResponse("error"), - // two below may need special cases - trace: buildLogResponse("trace"), + // table should be treated differently, but works the same + // as the rest of the logs for now table: buildLogResponse("table"), } } diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index a21bfdb755..67ccde727e 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -19,7 +19,7 @@ import manifest from "./manifest.json" import { Log, ProcessOptions } from "./types" import { UserScriptError } from "./errors" -export type { Log } from "./types" +export type { Log, LogType } from "./types" export { helpersToRemoveForJs, getJsHelperList } from "./helpers/list" export { FIND_ANY_HBS_REGEX } from "./utilities" export { setJSRunner, setOnErrorLog } from "./helpers/javascript" diff --git a/packages/string-templates/src/types.ts b/packages/string-templates/src/types.ts index a32149c8bb..f6ec7098f9 100644 --- a/packages/string-templates/src/types.ts +++ b/packages/string-templates/src/types.ts @@ -9,7 +9,10 @@ export interface ProcessOptions { disabledHelpers?: string[] } +export type LogType = "log" | "info" | "debug" | "warn" | "error" | "table" + export interface Log { log: any[] line?: number + type?: LogType } From bd5e55480e045886597a0c5d24938756954ec4be Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 11:16:19 +0000 Subject: [PATCH 08/39] Adding more test cases. --- .../string-templates/test/jsLogging.spec.ts | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/string-templates/test/jsLogging.spec.ts b/packages/string-templates/test/jsLogging.spec.ts index 9b2bb945d2..44b3b392ba 100644 --- a/packages/string-templates/test/jsLogging.spec.ts +++ b/packages/string-templates/test/jsLogging.spec.ts @@ -21,13 +21,33 @@ describe("Javascript", () => { console.log("foo"); return "hello"` ) - expect(output.result).toBe("hello") - expect(output.logs[0].log).toBe("hello") - expect(output.logs[0].line).toBe(1) - expect(output.logs[1].log).toBe("world") - expect(output.logs[1].line).toBe(2) - expect(output.logs[2].log).toBe("foo") - expect(output.logs[2].line).toBe(3) + expect(output.result).toEqual("hello") + expect(output.logs[0].log).toEqual(["hello"]) + expect(output.logs[0].line).toEqual(1) + expect(output.logs[1].log).toEqual(["world"]) + expect(output.logs[1].line).toEqual(2) + expect(output.logs[2].log).toEqual(["foo"]) + expect(output.logs[2].line).toEqual(3) }) }) + + it("should log comma separated values", () => { + const output = processJS(`console.log(1, { a: 1 }); return 1`) + expect(output.logs[0].log).toEqual([1, JSON.stringify({ a: 1 })]) + expect(output.logs[0].line).toEqual(1) + }) + + it("should return the type working with warn", () => { + const output = processJS(`console.warn("warning"); return 1`) + expect(output.logs[0].log).toEqual(["warning"]) + expect(output.logs[0].line).toEqual(1) + expect(output.logs[0].type).toEqual("warn") + }) + + it("should return the type working with error", () => { + const output = processJS(`console.error("error"); return 1`) + expect(output.logs[0].log).toEqual(["error"]) + expect(output.logs[0].line).toEqual(1) + expect(output.logs[0].type).toEqual("error") + }) }) From 3b03515253bd6f5203b65b67be2d9eb6ebd91111 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 15:23:00 +0000 Subject: [PATCH 09/39] Fixing test failure. --- packages/string-templates/src/processors/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/string-templates/src/processors/index.ts b/packages/string-templates/src/processors/index.ts index 4454d02738..0916c791cd 100644 --- a/packages/string-templates/src/processors/index.ts +++ b/packages/string-templates/src/processors/index.ts @@ -24,8 +24,10 @@ function process( } for (let match of matches) { const res = processor.process(output, match, opts || {}) - if (typeof res === "object" && "logs" in res && res.logs) { - logs = logs.concat(res.logs) + if (typeof res === "object") { + if ("logs" in res && res.logs) { + logs = logs.concat(res.logs) + } output = res.result } else { output = res as string From a920be3207f850c71a52758b7d675e86a2dc7643 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 15:33:06 +0000 Subject: [PATCH 10/39] Remove error. --- packages/string-templates/test/jsLogging.spec.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/string-templates/test/jsLogging.spec.ts b/packages/string-templates/test/jsLogging.spec.ts index 44b3b392ba..f328b76c7c 100644 --- a/packages/string-templates/test/jsLogging.spec.ts +++ b/packages/string-templates/test/jsLogging.spec.ts @@ -43,11 +43,4 @@ describe("Javascript", () => { expect(output.logs[0].line).toEqual(1) expect(output.logs[0].type).toEqual("warn") }) - - it("should return the type working with error", () => { - const output = processJS(`console.error("error"); return 1`) - expect(output.logs[0].log).toEqual(["error"]) - expect(output.logs[0].line).toEqual(1) - expect(output.logs[0].type).toEqual("error") - }) }) From ae73c0147ffc481206a4e7bc29e0d942cbba511e Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 15:41:08 +0000 Subject: [PATCH 11/39] Adding test checks. --- packages/string-templates/src/environment.ts | 11 +++++++++++ packages/string-templates/src/helpers/javascript.ts | 5 ++++- packages/string-templates/test/jsLogging.spec.ts | 7 +++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 packages/string-templates/src/environment.ts diff --git a/packages/string-templates/src/environment.ts b/packages/string-templates/src/environment.ts new file mode 100644 index 0000000000..ede52591b1 --- /dev/null +++ b/packages/string-templates/src/environment.ts @@ -0,0 +1,11 @@ +function isJest() { + return ( + process.env.NODE_ENV === "jest" || + (process.env.JEST_WORKER_ID != null && + process.env.JEST_WORKER_ID !== "null") + ) +} + +export function isTest() { + return isJest() +} diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index 997ca7b6ec..91f2f9a0ce 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -10,6 +10,7 @@ import { iifeWrapper } from "../iife" import { JsTimeoutError, UserScriptError } from "../errors" import { cloneDeep } from "lodash/fp" import { Log, LogType } from "../types" +import { isTest } from "../environment" // The method of executing JS scripts depends on the bundle being built. // This setter is used in the entrypoint (either index.js or index.mjs). @@ -126,7 +127,9 @@ export function processJS(handlebars: string, context: any) { const jsLineCount = frontendWrapJS(js).split(js)[0].split("\n").length const buildLogResponse = (type: LogType) => { return (...props: any[]) => { - console[type](...props) + if (!isTest()) { + console[type](...props) + } props.forEach((prop, index) => { if (typeof prop === "object") { props[index] = JSON.stringify(prop) diff --git a/packages/string-templates/test/jsLogging.spec.ts b/packages/string-templates/test/jsLogging.spec.ts index f328b76c7c..44b3b392ba 100644 --- a/packages/string-templates/test/jsLogging.spec.ts +++ b/packages/string-templates/test/jsLogging.spec.ts @@ -43,4 +43,11 @@ describe("Javascript", () => { expect(output.logs[0].line).toEqual(1) expect(output.logs[0].type).toEqual("warn") }) + + it("should return the type working with error", () => { + const output = processJS(`console.error("error"); return 1`) + expect(output.logs[0].log).toEqual(["error"]) + expect(output.logs[0].line).toEqual(1) + expect(output.logs[0].type).toEqual("error") + }) }) From 9c65f1ab41cfdb2e98282ed52d1af7e224152865 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 15:58:11 +0000 Subject: [PATCH 12/39] Another quick fix. --- packages/string-templates/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 67ccde727e..b2097b0d4c 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -240,7 +240,7 @@ function processStringSyncInternal( string = string.replace(block, outcome) } } - return opts?.logging ? string : { result: string, logs } + return !opts?.logging ? string : { result: string, logs } } else { return process(string) } From 98bd824d7ae01e59df1141ed2f68c8b7b3e233b0 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 16:33:21 +0000 Subject: [PATCH 13/39] Adding the ability to configure whether or not string templates is testing backend JS or frontend. --- packages/server/src/api/routes/tests/row.spec.ts | 2 ++ packages/string-templates/src/environment.ts | 12 ++++++++++++ packages/string-templates/src/index.ts | 1 + packages/string-templates/src/utilities.ts | 11 ++++++++--- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 576f0bb663..2a145e1ed9 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -50,9 +50,11 @@ import { JsTimeoutError } from "@budibase/string-templates" import { isDate } from "../../../utilities" import nock from "nock" import { mockChatGPTResponse } from "../../../tests/utilities/mocks/openai" +import { setTestingBackendJS } from "@budibase/string-templates" const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() tk.freeze(timestamp) +setTestingBackendJS() interface WaitOptions { name: string matchFn?: (event: any) => boolean diff --git a/packages/string-templates/src/environment.ts b/packages/string-templates/src/environment.ts index ede52591b1..6bee6fd3a9 100644 --- a/packages/string-templates/src/environment.ts +++ b/packages/string-templates/src/environment.ts @@ -9,3 +9,15 @@ function isJest() { export function isTest() { return isJest() } + +export const isJSAllowed = () => { + return process && !process.env.NO_JS +} + +export const isTestingBackendJS = () => { + return process && process.env.BACKEND_JS +} + +export const setTestingBackendJS = () => { + process.env.BACKEND_JS = "1" +} diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index b2097b0d4c..8dda8b71ab 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -20,6 +20,7 @@ import { Log, ProcessOptions } from "./types" import { UserScriptError } from "./errors" export type { Log, LogType } from "./types" +export { setTestingBackendJS } from "./environment" export { helpersToRemoveForJs, getJsHelperList } from "./helpers/list" export { FIND_ANY_HBS_REGEX } from "./utilities" export { setJSRunner, setOnErrorLog } from "./helpers/javascript" diff --git a/packages/string-templates/src/utilities.ts b/packages/string-templates/src/utilities.ts index dba1faab17..b05945f075 100644 --- a/packages/string-templates/src/utilities.ts +++ b/packages/string-templates/src/utilities.ts @@ -1,15 +1,20 @@ +import { isTest, isTestingBackendJS } from "./environment" + const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g export const FIND_HBS_REGEX = /{{([^{].*?)}}/g export const FIND_ANY_HBS_REGEX = /{?{{([^{].*?)}}}?/g export const FIND_TRIPLE_HBS_REGEX = /{{{([^{].*?)}}}/g -const isJest = () => typeof jest !== "undefined" - export const isBackendService = () => { + // allow configuring backend JS mode when testing - we default to assuming + // frontend, but need a method to control this + if (isTest() && isTestingBackendJS()) { + return true + } // We consider the tests for string-templates to be frontend, so that they // test the frontend JS functionality. - if (isJest()) { + if (isTest()) { return false } return typeof window === "undefined" From 68374bce29126c0f38da9f030acfdc82c2ed8e60 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 16:40:08 +0000 Subject: [PATCH 14/39] Testing backend JS further. --- packages/server/src/jsRunner/tests/jsRunner.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/server/src/jsRunner/tests/jsRunner.spec.ts b/packages/server/src/jsRunner/tests/jsRunner.spec.ts index 006df19fa6..e10e9c4d43 100644 --- a/packages/server/src/jsRunner/tests/jsRunner.spec.ts +++ b/packages/server/src/jsRunner/tests/jsRunner.spec.ts @@ -1,5 +1,9 @@ import { validate as isValidUUID } from "uuid" -import { processStringSync, encodeJSBinding } from "@budibase/string-templates" +import { + processStringSync, + encodeJSBinding, + setTestingBackendJS, +} from "@budibase/string-templates" import { runJsHelpersTests } from "@budibase/string-templates/test/utils" @@ -7,6 +11,7 @@ import tk from "timekeeper" import { init } from ".." import TestConfiguration from "../../tests/utilities/TestConfiguration" +setTestingBackendJS() const DATE = "2021-01-21T12:00:00" tk.freeze(DATE) From 04a7878ce9ad4d6ea5656ae6409eb6cbbb68bb53 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 17:02:57 +0000 Subject: [PATCH 15/39] Changing how we enforce backend JS. --- packages/server/src/api/routes/tests/row.spec.ts | 2 -- .../automations/tests/utilities/AutomationTestBuilder.ts | 1 + packages/server/src/jsRunner/index.ts | 5 +++++ packages/server/src/jsRunner/tests/jsRunner.spec.ts | 7 +------ packages/string-templates/src/helpers/javascript.ts | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 2a145e1ed9..576f0bb663 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -50,11 +50,9 @@ import { JsTimeoutError } from "@budibase/string-templates" import { isDate } from "../../../utilities" import nock from "nock" import { mockChatGPTResponse } from "../../../tests/utilities/mocks/openai" -import { setTestingBackendJS } from "@budibase/string-templates" const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() tk.freeze(timestamp) -setTestingBackendJS() interface WaitOptions { name: string matchFn?: (event: any) => boolean diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 830d2ee5ca..f89c815752 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -43,6 +43,7 @@ import { import TestConfiguration from "../../../tests/utilities/TestConfiguration" import * as setup from "../utilities" import { automations } from "@budibase/shared-core" +import { setTestingBackendJS } from "@budibase/string-templates" type TriggerOutputs = | RowCreatedTriggerOutputs diff --git a/packages/server/src/jsRunner/index.ts b/packages/server/src/jsRunner/index.ts index e17529a687..a29e952b6d 100644 --- a/packages/server/src/jsRunner/index.ts +++ b/packages/server/src/jsRunner/index.ts @@ -4,12 +4,17 @@ import { JsTimeoutError, setJSRunner, setOnErrorLog, + setTestingBackendJS, } from "@budibase/string-templates" import { context, logging } from "@budibase/backend-core" import tracer from "dd-trace" import { IsolatedVM } from "./vm" export function init() { + // enforce that if we're using isolated-VM runner then we are running backend JS + if (env.isTest()) { + setTestingBackendJS() + } setJSRunner((js: string, ctx: Record) => { return tracer.trace("runJS", {}, () => { try { diff --git a/packages/server/src/jsRunner/tests/jsRunner.spec.ts b/packages/server/src/jsRunner/tests/jsRunner.spec.ts index e10e9c4d43..006df19fa6 100644 --- a/packages/server/src/jsRunner/tests/jsRunner.spec.ts +++ b/packages/server/src/jsRunner/tests/jsRunner.spec.ts @@ -1,9 +1,5 @@ import { validate as isValidUUID } from "uuid" -import { - processStringSync, - encodeJSBinding, - setTestingBackendJS, -} from "@budibase/string-templates" +import { processStringSync, encodeJSBinding } from "@budibase/string-templates" import { runJsHelpersTests } from "@budibase/string-templates/test/utils" @@ -11,7 +7,6 @@ import tk from "timekeeper" import { init } from ".." import TestConfiguration from "../../tests/utilities/TestConfiguration" -setTestingBackendJS() const DATE = "2021-01-21T12:00:00" tk.freeze(DATE) diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index 91f2f9a0ce..6132adf892 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -88,7 +88,7 @@ export function processJS(handlebars: string, context: any) { let clonedContext: Record if (isBackendService()) { - // On the backned, values are copied across the isolated-vm boundary and + // On the backend, values are copied across the isolated-vm boundary and // so we don't need to do any cloning here. This does create a fundamental // difference in how JS executes on the frontend vs the backend, e.g. // consider this snippet: From d51491a19adc19b70fe80532d5bee7bad07c4f23 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Jan 2025 17:06:31 +0000 Subject: [PATCH 16/39] Linting. --- .../src/automations/tests/utilities/AutomationTestBuilder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index f89c815752..830d2ee5ca 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -43,7 +43,6 @@ import { import TestConfiguration from "../../../tests/utilities/TestConfiguration" import * as setup from "../utilities" import { automations } from "@budibase/shared-core" -import { setTestingBackendJS } from "@budibase/string-templates" type TriggerOutputs = | RowCreatedTriggerOutputs From 5bc316916ff6c9525f5882fdd11b89eb73d98cd8 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 Jan 2025 17:18:29 +0000 Subject: [PATCH 17/39] First iteration of single-step automation test endpoint. --- .../server/src/api/controllers/automation.ts | 91 +++++++++++++++---- packages/server/src/api/routes/automation.ts | 12 ++- packages/server/src/events/NoopEmitter.ts | 39 ++++++++ packages/server/src/events/index.ts | 1 + packages/server/src/threads/automation.ts | 36 +++++--- packages/server/src/utilities/index.ts | 41 ++------- packages/server/src/utilities/redis.ts | 17 ++-- packages/types/src/api/web/app/automation.ts | 7 ++ .../documents/app/automation/automation.ts | 4 +- 9 files changed, 172 insertions(+), 76 deletions(-) create mode 100644 packages/server/src/events/NoopEmitter.ts diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index abc0e492c0..a77014cf31 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -2,7 +2,7 @@ import * as triggers from "../../automations/triggers" import { sdk as coreSdk } from "@budibase/shared-core" import { DocumentType } from "../../db/utils" import { updateTestHistory, removeDeprecated } from "../../automations/utils" -import { setTestFlag, clearTestFlag } from "../../utilities/redis" +import { withTestFlag } from "../../utilities/redis" import { context, cache, events, db as dbCore } from "@budibase/backend-core" import { automations, features } from "@budibase/pro" import { @@ -28,11 +28,18 @@ import { TriggerAutomationResponse, TestAutomationRequest, TestAutomationResponse, + TestAutomationStepRequest, + TestAutomationStepResponse, } from "@budibase/types" -import { getActionDefinitions as actionDefs } from "../../automations/actions" +import { + getActionDefinitions as actionDefs, + getAction, +} from "../../automations/actions" import sdk from "../../sdk" import { builderSocket } from "../../websockets" import env from "../../environment" +import { NoopEmitter } from "../../events" +import { enrichBaseContext } from "../../threads/automation" async function getActionDefinitions() { return removeDeprecated(await actionDefs()) @@ -231,24 +238,68 @@ export async function test( ctx: UserCtx ) { const db = context.getAppDB() - let automation = await db.get(ctx.params.id) - await setTestFlag(automation._id!) - const testInput = prepareTestInput(ctx.request.body) - const response = await triggers.externalTrigger( - automation, - { - ...testInput, - appId: ctx.appId, - user: sdk.users.getUserContextBindings(ctx.user), - }, - { getResponses: true } - ) - // save a test history run - await updateTestHistory(ctx.appId, automation, { - ...ctx.request.body, - occurredAt: new Date().getTime(), + const automation = await db.tryGet(ctx.params.id) + if (!automation) { + ctx.throw(404, `Automation ${ctx.params.id} not found`) + } + + const { request, appId } = ctx + const { body } = request + + ctx.body = await withTestFlag(automation._id!, async () => { + const occurredAt = new Date().getTime() + await updateTestHistory(appId, automation, { ...body, occurredAt }) + + const user = sdk.users.getUserContextBindings(ctx.user) + return await triggers.externalTrigger( + automation, + { ...prepareTestInput(body), appId, user }, + { getResponses: true } + ) }) - await clearTestFlag(automation._id!) - ctx.body = response + await events.automation.tested(automation) } + +export async function testStep( + ctx: UserCtx +) { + const { id, stepId } = ctx.params + const db = context.getAppDB() + const automation = await db.tryGet(id) + if (!automation) { + ctx.throw(404, `Automation ${ctx.params.id} not found`) + } + + const step = automation.definition.steps.find(s => s.stepId === stepId) + if (!step) { + ctx.throw(404, `Step ${stepId} not found on automation ${id}`) + } + + if (step.stepId === AutomationActionStepId.BRANCH) { + ctx.throw(400, "Branch steps cannot be tested directly") + } + if (step.stepId === AutomationActionStepId.LOOP) { + ctx.throw(400, "Loop steps cannot be tested directly") + } + + const { body } = ctx.request + + const fn = await getAction(step.stepId) + if (!fn) { + ctx.throw(400, `Step ${stepId} is not a valid step`) + } + + const output = await withTestFlag( + automation._id!, + async () => + await fn({ + inputs: body.inputs, + context: await enrichBaseContext(body.context), + appId: ctx.appId, + emitter: new NoopEmitter(), + }) + ) + + ctx.body = output +} diff --git a/packages/server/src/api/routes/automation.ts b/packages/server/src/api/routes/automation.ts index 489487271c..ea905be0cd 100644 --- a/packages/server/src/api/routes/automation.ts +++ b/packages/server/src/api/routes/automation.ts @@ -1,6 +1,6 @@ import Router from "@koa/router" import * as controller from "../controllers/automation" -import authorized from "../../middleware/authorized" +import authorized, { authorizedResource } from "../../middleware/authorized" import { permissions } from "@budibase/backend-core" import { bodyResource, paramResource } from "../../middleware/resourceId" import { @@ -82,5 +82,15 @@ router ), controller.test ) + .post( + "/api/automations/:id/step/:stepId/test", + appInfoMiddleware({ appType: AppType.DEV }), + authorizedResource( + permissions.PermissionType.AUTOMATION, + permissions.PermissionLevel.EXECUTE, + "id" + ), + controller.testStep + ) export default router diff --git a/packages/server/src/events/NoopEmitter.ts b/packages/server/src/events/NoopEmitter.ts new file mode 100644 index 0000000000..ed87618ead --- /dev/null +++ b/packages/server/src/events/NoopEmitter.ts @@ -0,0 +1,39 @@ +import { EventEmitter } from "events" +import { + Table, + Row, + ContextEmitter, + EventType, + UserBindings, +} from "@budibase/types" + +export class NoopEmitter extends EventEmitter implements ContextEmitter { + emitRow(values: { + eventName: EventType.ROW_SAVE + appId: string + row: Row + table: Table + user: UserBindings + }): void + emitRow(values: { + eventName: EventType.ROW_UPDATE + appId: string + row: Row + table: Table + oldRow: Row + user: UserBindings + }): void + emitRow(values: { + eventName: EventType.ROW_DELETE + appId: string + row: Row + user: UserBindings + }): void + emitRow(_values: unknown): void { + return + } + + emitTable(_eventName: string, _appId: string, _table?: Table) { + return + } +} diff --git a/packages/server/src/events/index.ts b/packages/server/src/events/index.ts index 23c3f3e512..90bf932bcf 100644 --- a/packages/server/src/events/index.ts +++ b/packages/server/src/events/index.ts @@ -2,5 +2,6 @@ import BudibaseEmitter from "./BudibaseEmitter" const emitter = new BudibaseEmitter() +export { NoopEmitter } from "./NoopEmitter" export { init } from "./docUpdates" export default emitter diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 2d10f5d1fb..2790d8fda6 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -29,6 +29,7 @@ import { LoopStep, UserBindings, isBasicSearchOperator, + ContextEmitter, } from "@budibase/types" import { AutomationContext, @@ -71,6 +72,24 @@ function getLoopIterations(loopStep: LoopStep) { return 0 } +export async function enrichBaseContext(context: Record) { + context.env = await sdkUtils.getEnvironmentVariables() + + try { + const { config } = await configs.getSettingsConfigDoc() + context.settings = { + url: config.platformUrl, + logo: config.logoUrl, + company: config.company, + } + } catch (e) { + // if settings doc doesn't exist, make the settings blank + context.settings = {} + } + + return context +} + /** * The automation orchestrator is a class responsible for executing automations. * It handles the context of the automation and makes sure each step gets the correct @@ -80,7 +99,7 @@ class Orchestrator { private chainCount: number private appId: string private automation: Automation - private emitter: any + private emitter: ContextEmitter private context: AutomationContext private job: Job private loopStepOutputs: LoopStep[] @@ -270,20 +289,9 @@ class Orchestrator { appId: this.appId, automationId: this.automation._id, }) - this.context.env = await sdkUtils.getEnvironmentVariables() - this.context.user = this.currentUser - try { - const { config } = await configs.getSettingsConfigDoc() - this.context.settings = { - url: config.platformUrl, - logo: config.logoUrl, - company: config.company, - } - } catch (e) { - // if settings doc doesn't exist, make the settings blank - this.context.settings = {} - } + await enrichBaseContext(this.context) + this.context.user = this.currentUser let metadata diff --git a/packages/server/src/utilities/index.ts b/packages/server/src/utilities/index.ts index db57b4ec12..f1b32c81f3 100644 --- a/packages/server/src/utilities/index.ts +++ b/packages/server/src/utilities/index.ts @@ -58,30 +58,14 @@ export function checkSlashesInUrl(url: string) { export async function updateEntityMetadata( type: string, entityId: string, - updateFn: any + updateFn: (metadata: Document) => Document ) { const db = context.getAppDB() const id = generateMetadataID(type, entityId) - // read it to see if it exists, we'll overwrite it no matter what - let rev, metadata: Document - try { - const oldMetadata = await db.get(id) - rev = oldMetadata._rev - metadata = updateFn(oldMetadata) - } catch (err) { - rev = null - metadata = updateFn({}) - } + const metadata = updateFn((await db.tryGet(id)) || {}) metadata._id = id - if (rev) { - metadata._rev = rev - } const response = await db.put(metadata) - return { - ...metadata, - _id: id, - _rev: response.rev, - } + return { ...metadata, _id: id, _rev: response.rev } } export async function saveEntityMetadata( @@ -89,26 +73,17 @@ export async function saveEntityMetadata( entityId: string, metadata: Document ): Promise { - return updateEntityMetadata(type, entityId, () => { - return metadata - }) + return updateEntityMetadata(type, entityId, () => metadata) } export async function deleteEntityMetadata(type: string, entityId: string) { const db = context.getAppDB() const id = generateMetadataID(type, entityId) - let rev - try { - const metadata = await db.get(id) - if (metadata) { - rev = metadata._rev - } - } catch (err) { - // don't need to error if it doesn't exist - } - if (id && rev) { - await db.remove(id, rev) + const metadata = await db.tryGet(id) + if (!metadata) { + return } + await db.remove(metadata) } export function escapeDangerousCharacters(string: string) { diff --git a/packages/server/src/utilities/redis.ts b/packages/server/src/utilities/redis.ts index a4154b7b95..a3ce655316 100644 --- a/packages/server/src/utilities/redis.ts +++ b/packages/server/src/utilities/redis.ts @@ -89,17 +89,22 @@ export async function setDebounce(id: string, seconds: number) { await debounceClient.store(id, "debouncing", seconds) } -export async function setTestFlag(id: string) { - await flagClient.store(id, { testing: true }, AUTOMATION_TEST_FLAG_SECONDS) -} - export async function checkTestFlag(id: string) { const flag = await flagClient?.get(id) return !!(flag && flag.testing) } -export async function clearTestFlag(id: string) { - await devAppClient.delete(id) +export async function withTestFlag(id: string, fn: () => Promise) { + // TODO(samwho): this has a bit of a problem where if 2 automations are tested + // at the same time, the second one will overwrite the first one's flag. We + // should instead use an atomic counter and only clear the flag when the + // counter reaches 0. + await flagClient.store(id, { testing: true }, AUTOMATION_TEST_FLAG_SECONDS) + try { + return await fn() + } finally { + await devAppClient.delete(id) + } } export function getSocketPubSubClients() { diff --git a/packages/types/src/api/web/app/automation.ts b/packages/types/src/api/web/app/automation.ts index 40f69fc467..572e6499b6 100644 --- a/packages/types/src/api/web/app/automation.ts +++ b/packages/types/src/api/web/app/automation.ts @@ -75,3 +75,10 @@ export interface TestAutomationRequest { row?: Row } export interface TestAutomationResponse {} + +export interface TestAutomationStepRequest { + inputs: Record + context: Record +} + +export type TestAutomationStepResponse = any diff --git a/packages/types/src/documents/app/automation/automation.ts b/packages/types/src/documents/app/automation/automation.ts index d56f0de879..a7556c2ce3 100644 --- a/packages/types/src/documents/app/automation/automation.ts +++ b/packages/types/src/documents/app/automation/automation.ts @@ -1,10 +1,10 @@ import { Document } from "../../document" -import { EventEmitter } from "events" import { User } from "../../global" import { ReadStream } from "fs" import { Row } from "../row" import { Table } from "../table" import { AutomationStep, AutomationTrigger } from "./schema" +import { ContextEmitter } from "../../../sdk" export enum AutomationIOType { OBJECT = "object", @@ -218,7 +218,7 @@ export interface AutomationLogPage { export interface AutomationStepInputBase { context: Record - emitter: EventEmitter + emitter: ContextEmitter appId: string apiKey?: string } From 99cf4feb07dce88d97155de19c1e922731ba15b7 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 Jan 2025 17:30:36 +0000 Subject: [PATCH 18/39] Remove old automation test flag mechanism from Redis. --- .../server/src/api/controllers/automation.ts | 39 +++++++------------ packages/server/src/automations/triggers.ts | 11 +----- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index a77014cf31..8c58cd4a19 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -2,7 +2,6 @@ import * as triggers from "../../automations/triggers" import { sdk as coreSdk } from "@budibase/shared-core" import { DocumentType } from "../../db/utils" import { updateTestHistory, removeDeprecated } from "../../automations/utils" -import { withTestFlag } from "../../utilities/redis" import { context, cache, events, db as dbCore } from "@budibase/backend-core" import { automations, features } from "@budibase/pro" import { @@ -246,17 +245,15 @@ export async function test( const { request, appId } = ctx const { body } = request - ctx.body = await withTestFlag(automation._id!, async () => { - const occurredAt = new Date().getTime() - await updateTestHistory(appId, automation, { ...body, occurredAt }) + const occurredAt = new Date().getTime() + await updateTestHistory(appId, automation, { ...body, occurredAt }) - const user = sdk.users.getUserContextBindings(ctx.user) - return await triggers.externalTrigger( - automation, - { ...prepareTestInput(body), appId, user }, - { getResponses: true } - ) - }) + const user = sdk.users.getUserContextBindings(ctx.user) + ctx.body = await triggers.externalTrigger( + automation, + { ...prepareTestInput(body), appId, user }, + { getResponses: true } + ) await events.automation.tested(automation) } @@ -271,7 +268,7 @@ export async function testStep( ctx.throw(404, `Automation ${ctx.params.id} not found`) } - const step = automation.definition.steps.find(s => s.stepId === stepId) + const step = automation.definition.steps.find(s => s.id === stepId) if (!step) { ctx.throw(404, `Step ${stepId} not found on automation ${id}`) } @@ -290,16 +287,10 @@ export async function testStep( ctx.throw(400, `Step ${stepId} is not a valid step`) } - const output = await withTestFlag( - automation._id!, - async () => - await fn({ - inputs: body.inputs, - context: await enrichBaseContext(body.context), - appId: ctx.appId, - emitter: new NoopEmitter(), - }) - ) - - ctx.body = output + ctx.body = await fn({ + inputs: body.inputs, + context: await enrichBaseContext(body.context), + appId: ctx.appId, + emitter: new NoopEmitter(), + }) } diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index 67d2dcb911..10830a4046 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -82,11 +82,7 @@ async function queueRelevantRowAutomations( // don't queue events which are for dev apps, only way to test automations is // running tests on them, in production the test flag will never // be checked due to lazy evaluation (first always false) - if ( - !env.ALLOW_DEV_AUTOMATIONS && - isDevAppID(event.appId) && - !(await checkTestFlag(automation._id!)) - ) { + if (!env.ALLOW_DEV_AUTOMATIONS && isDevAppID(event.appId)) { continue } @@ -170,10 +166,7 @@ export async function externalTrigger( throw new Error("Automation is disabled") } - if ( - sdk.automations.isAppAction(automation) && - !(await checkTestFlag(automation._id!)) - ) { + if (sdk.automations.isAppAction(automation) && !isDevAppID(params.appId)) { // values are likely to be submitted as strings, so we shall convert to correct type const coercedFields: any = {} const fields = automation.definition.trigger.inputs.fields From 0670c89e83946323f0ff014a5a5687e6166fac71 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 Jan 2025 17:30:49 +0000 Subject: [PATCH 19/39] Remove unused import. --- packages/server/src/automations/triggers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index 10830a4046..f2082e5c0c 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -4,7 +4,6 @@ import { coerce } from "../utilities/rowProcessor" import { definitions } from "./triggerInfo" // need this to call directly, so we can get a response import { automationQueue } from "./bullboard" -import { checkTestFlag } from "../utilities/redis" import * as utils from "./utils" import env from "../environment" import { context, logging, db as dbCore } from "@budibase/backend-core" From f96c4f352d3dbefb8ad28331b24c2f4d8c42b19e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 Jan 2025 17:38:28 +0000 Subject: [PATCH 20/39] Revert "Remove unused import." This reverts commit 0670c89e83946323f0ff014a5a5687e6166fac71. --- packages/server/src/automations/triggers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index f2082e5c0c..10830a4046 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -4,6 +4,7 @@ import { coerce } from "../utilities/rowProcessor" import { definitions } from "./triggerInfo" // need this to call directly, so we can get a response import { automationQueue } from "./bullboard" +import { checkTestFlag } from "../utilities/redis" import * as utils from "./utils" import env from "../environment" import { context, logging, db as dbCore } from "@budibase/backend-core" From 5afab49e18d12f0b1c007e80dd0cc158fd312740 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 Jan 2025 17:38:38 +0000 Subject: [PATCH 21/39] Revert "Remove old automation test flag mechanism from Redis." This reverts commit 99cf4feb07dce88d97155de19c1e922731ba15b7. --- .../server/src/api/controllers/automation.ts | 39 ++++++++++++------- packages/server/src/automations/triggers.ts | 11 +++++- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index 8c58cd4a19..a77014cf31 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -2,6 +2,7 @@ import * as triggers from "../../automations/triggers" import { sdk as coreSdk } from "@budibase/shared-core" import { DocumentType } from "../../db/utils" import { updateTestHistory, removeDeprecated } from "../../automations/utils" +import { withTestFlag } from "../../utilities/redis" import { context, cache, events, db as dbCore } from "@budibase/backend-core" import { automations, features } from "@budibase/pro" import { @@ -245,15 +246,17 @@ export async function test( const { request, appId } = ctx const { body } = request - const occurredAt = new Date().getTime() - await updateTestHistory(appId, automation, { ...body, occurredAt }) + ctx.body = await withTestFlag(automation._id!, async () => { + const occurredAt = new Date().getTime() + await updateTestHistory(appId, automation, { ...body, occurredAt }) - const user = sdk.users.getUserContextBindings(ctx.user) - ctx.body = await triggers.externalTrigger( - automation, - { ...prepareTestInput(body), appId, user }, - { getResponses: true } - ) + const user = sdk.users.getUserContextBindings(ctx.user) + return await triggers.externalTrigger( + automation, + { ...prepareTestInput(body), appId, user }, + { getResponses: true } + ) + }) await events.automation.tested(automation) } @@ -268,7 +271,7 @@ export async function testStep( ctx.throw(404, `Automation ${ctx.params.id} not found`) } - const step = automation.definition.steps.find(s => s.id === stepId) + const step = automation.definition.steps.find(s => s.stepId === stepId) if (!step) { ctx.throw(404, `Step ${stepId} not found on automation ${id}`) } @@ -287,10 +290,16 @@ export async function testStep( ctx.throw(400, `Step ${stepId} is not a valid step`) } - ctx.body = await fn({ - inputs: body.inputs, - context: await enrichBaseContext(body.context), - appId: ctx.appId, - emitter: new NoopEmitter(), - }) + const output = await withTestFlag( + automation._id!, + async () => + await fn({ + inputs: body.inputs, + context: await enrichBaseContext(body.context), + appId: ctx.appId, + emitter: new NoopEmitter(), + }) + ) + + ctx.body = output } diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index 10830a4046..67d2dcb911 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -82,7 +82,11 @@ async function queueRelevantRowAutomations( // don't queue events which are for dev apps, only way to test automations is // running tests on them, in production the test flag will never // be checked due to lazy evaluation (first always false) - if (!env.ALLOW_DEV_AUTOMATIONS && isDevAppID(event.appId)) { + if ( + !env.ALLOW_DEV_AUTOMATIONS && + isDevAppID(event.appId) && + !(await checkTestFlag(automation._id!)) + ) { continue } @@ -166,7 +170,10 @@ export async function externalTrigger( throw new Error("Automation is disabled") } - if (sdk.automations.isAppAction(automation) && !isDevAppID(params.appId)) { + if ( + sdk.automations.isAppAction(automation) && + !(await checkTestFlag(automation._id!)) + ) { // values are likely to be submitted as strings, so we shall convert to correct type const coercedFields: any = {} const fields = automation.definition.trigger.inputs.fields From 31fc2e45c9252a442f5dc7e77613493523ad629b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 Jan 2025 18:08:14 +0000 Subject: [PATCH 22/39] Improve some typing around automation testing. --- .../tests/utilities/AutomationTestBuilder.ts | 28 +++++++------- packages/server/src/automations/triggers.ts | 33 +++++++++------- .../src/tests/utilities/api/automation.ts | 38 ++++++++++++++++++- packages/types/src/api/web/app/automation.ts | 10 ++++- .../documents/app/automation/automation.ts | 8 ++++ 5 files changed, 86 insertions(+), 31 deletions(-) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 830d2ee5ca..50527d97af 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -1,5 +1,4 @@ import { v4 as uuidv4 } from "uuid" -import { testAutomation } from "../../../api/routes/tests/utilities/TestFunctions" import { BUILTIN_ACTION_DEFINITIONS } from "../../actions" import { TRIGGER_DEFINITIONS } from "../../triggers" import { @@ -7,7 +6,6 @@ import { AppActionTriggerOutputs, Automation, AutomationActionStepId, - AutomationResults, AutomationStep, AutomationStepInputs, AutomationTrigger, @@ -24,6 +22,7 @@ import { ExecuteQueryStepInputs, ExecuteScriptStepInputs, FilterStepInputs, + isDidNotTriggerResponse, LoopStepInputs, OpenAIStepInputs, QueryRowsStepInputs, @@ -36,6 +35,7 @@ import { SearchFilters, ServerLogStepInputs, SmtpEmailStepInputs, + TestAutomationRequest, UpdateRowStepInputs, WebhookTriggerInputs, WebhookTriggerOutputs, @@ -279,7 +279,7 @@ class StepBuilder extends BaseStepBuilder { class AutomationBuilder extends BaseStepBuilder { private automationConfig: Automation private config: TestConfiguration - private triggerOutputs: any + private triggerOutputs: TriggerOutputs private triggerSet = false constructor( @@ -398,21 +398,19 @@ class AutomationBuilder extends BaseStepBuilder { async run() { const automation = await this.save() - const results = await testAutomation( - this.config, - automation, - this.triggerOutputs + const response = await this.config.api.automation.test( + automation._id!, + this.triggerOutputs as TestAutomationRequest ) - return this.processResults(results) - } - private processResults(results: { - body: AutomationResults - }): AutomationResults { - results.body.steps.shift() + if (isDidNotTriggerResponse(response)) { + throw new Error(response.message) + } + + response.steps.shift() return { - trigger: results.body.trigger, - steps: results.body.steps, + trigger: response.trigger, + steps: response.steps, } } } diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index 67d2dcb911..a9317772d9 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -21,6 +21,7 @@ import { AutomationRowEvent, UserBindings, AutomationResults, + DidNotTriggerResponse, } from "@budibase/types" import { executeInThread } from "../threads/automation" import { dataFilters, sdk } from "@budibase/shared-core" @@ -33,14 +34,6 @@ const JOB_OPTS = { import * as automationUtils from "../automations/automationUtils" import { doesTableExist } from "../sdk/app/tables/getters" -type DidNotTriggerResponse = { - outputs: { - success: false - status: AutomationStatus.STOPPED - } - message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET -} - async function getAllAutomations() { const db = context.getAppDB() let automations = await db.allDocs( @@ -156,14 +149,26 @@ export function isAutomationResults( ) } +interface AutomationTriggerParams { + fields: Record + timeout?: number + appId?: string + user?: UserBindings +} + export async function externalTrigger( automation: Automation, - params: { - fields: Record - timeout?: number - appId?: string - user?: UserBindings - }, + params: AutomationTriggerParams, + options: { getResponses: true } +): Promise +export async function externalTrigger( + automation: Automation, + params: AutomationTriggerParams, + options?: { getResponses: false } +): Promise +export async function externalTrigger( + automation: Automation, + params: AutomationTriggerParams, { getResponses }: { getResponses?: boolean } = {} ): Promise { if (automation.disabled) { diff --git a/packages/server/src/tests/utilities/api/automation.ts b/packages/server/src/tests/utilities/api/automation.ts index 9d9a27e891..6041664999 100644 --- a/packages/server/src/tests/utilities/api/automation.ts +++ b/packages/server/src/tests/utilities/api/automation.ts @@ -1,4 +1,11 @@ -import { Automation, FetchAutomationResponse } from "@budibase/types" +import { + Automation, + FetchAutomationResponse, + TestAutomationRequest, + TestAutomationResponse, + TestAutomationStepRequest, + TestAutomationStepResponse, +} from "@budibase/types" import { Expectations, TestAPI } from "./base" export class AutomationAPI extends TestAPI { @@ -33,4 +40,33 @@ export class AutomationAPI extends TestAPI { }) return result } + + test = async ( + id: string, + body: TestAutomationRequest, + expectations?: Expectations + ): Promise => { + return await this._post( + `/api/automations/${id}/test`, + { + body, + expectations, + } + ) + } + + testStep = async ( + id: string, + stepId: string, + body: TestAutomationStepRequest, + expectations?: Expectations + ): Promise => { + return await this._post( + `/api/automations/${id}/steps/${stepId}/test`, + { + body, + expectations, + } + ) + } } diff --git a/packages/types/src/api/web/app/automation.ts b/packages/types/src/api/web/app/automation.ts index 572e6499b6..edff4b5eaf 100644 --- a/packages/types/src/api/web/app/automation.ts +++ b/packages/types/src/api/web/app/automation.ts @@ -2,10 +2,12 @@ import { Automation, AutomationActionStepId, AutomationLogPage, + AutomationResults, AutomationStatus, AutomationStepDefinition, AutomationTriggerDefinition, AutomationTriggerStepId, + DidNotTriggerResponse, Row, } from "../../../documents" import { DocumentDestroyResponse } from "@budibase/nano" @@ -74,7 +76,13 @@ export interface TestAutomationRequest { fields: Record row?: Row } -export interface TestAutomationResponse {} +export type TestAutomationResponse = AutomationResults | DidNotTriggerResponse + +export function isDidNotTriggerResponse( + response: TestAutomationResponse +): response is DidNotTriggerResponse { + return !!("message" in response && response.message) +} export interface TestAutomationStepRequest { inputs: Record diff --git a/packages/types/src/documents/app/automation/automation.ts b/packages/types/src/documents/app/automation/automation.ts index a7556c2ce3..0314701d72 100644 --- a/packages/types/src/documents/app/automation/automation.ts +++ b/packages/types/src/documents/app/automation/automation.ts @@ -205,6 +205,14 @@ export interface AutomationResults { }[] } +export interface DidNotTriggerResponse { + outputs: { + success: false + status: AutomationStatus.STOPPED + } + message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET +} + export interface AutomationLog extends AutomationResults, Document { automationName: string _rev?: string From c5e4edcc9713f6286644fe10f65c19ac02a5c66c Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 21 Jan 2025 11:54:27 +0000 Subject: [PATCH 23/39] Setting overflow-y in evaluation panel to auto. --- .../src/components/common/bindings/EvaluationSidePanel.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index fcd23bb816..c47840ea83 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -174,7 +174,7 @@ padding: var(--spacing-m) var(--spacing-l); font-family: var(--font-mono); font-size: 12px; - overflow-y: scroll; + overflow-y: auto; overflow-x: hidden; white-space: pre-line; word-wrap: break-word; From 56f666f15a3a9b9c8793b1b69e01ca5cead8abbd Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 Jan 2025 12:57:30 +0100 Subject: [PATCH 24/39] Display TableSelect the same way we do for DataSourceSelect --- .../settings/controls/TableSelect.svelte | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/TableSelect.svelte b/packages/builder/src/components/design/settings/controls/TableSelect.svelte index 706c4ca74e..a91bde74ba 100644 --- a/packages/builder/src/components/design/settings/controls/TableSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/TableSelect.svelte @@ -1,22 +1,30 @@ - +
+ + + From ece99aa751ed9c473d90e1842dc41954b4cfb431 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 21 Jan 2025 13:15:53 +0100 Subject: [PATCH 25/39] DRY --- .../DataSourceSelect/DataSourceSelect.svelte | 16 +------- .../settings/controls/TableSelect.svelte | 8 ++-- .../builder/src/stores/builder/builder.ts | 39 +++++++++++++++++-- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte index b23ef5348d..20ba4c8552 100644 --- a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte @@ -18,7 +18,6 @@ } from "@budibase/bbui" import { createEventDispatcher } from "svelte" import { - tables as tablesStore, queries as queriesStore, viewsV2 as viewsV2Store, views as viewsStore, @@ -26,6 +25,7 @@ componentStore, datasources, integrations, + builderStore, } from "@/stores/builder" import BindingBuilder from "@/components/integration/QueryBindingBuilder.svelte" import IntegrationQueryEditor from "@/components/integration/index.svelte" @@ -51,19 +51,7 @@ let modal $: text = value?.label ?? "Choose an option" - $: tables = $tablesStore.list - .map(table => format.table(table, $datasources.list)) - .sort((a, b) => { - // sort tables alphabetically, grouped by datasource - const dsA = a.datasourceName ?? "" - const dsB = b.datasourceName ?? "" - - const dsComparison = dsA.localeCompare(dsB) - if (dsComparison !== 0) { - return dsComparison - } - return a.label.localeCompare(b.label) - }) + $: tables = $builderStore.formatedTableNames $: viewsV1 = $viewsStore.list.map(view => ({ ...view, label: view.name, diff --git a/packages/builder/src/components/design/settings/controls/TableSelect.svelte b/packages/builder/src/components/design/settings/controls/TableSelect.svelte index a91bde74ba..4c7c59037c 100644 --- a/packages/builder/src/components/design/settings/controls/TableSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/TableSelect.svelte @@ -1,8 +1,8 @@ {#if dividerState} @@ -29,7 +32,9 @@ on:click={() => onSelect(data)} > - {data.datasourceName ? `${data.datasourceName} - ` : ""}{data.label} + {data.datasourceName && displayDatasourceName + ? `${data.datasourceName} - ` + : ""}{data.label} Date: Tue, 21 Jan 2025 15:02:37 +0100 Subject: [PATCH 35/39] Fix paddings --- .../design/settings/controls/TableSelect.svelte | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/builder/src/components/design/settings/controls/TableSelect.svelte b/packages/builder/src/components/design/settings/controls/TableSelect.svelte index 146decda91..85209a92a0 100644 --- a/packages/builder/src/components/design/settings/controls/TableSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/TableSelect.svelte @@ -66,3 +66,20 @@ {/if}
+ + From 0e6ad4db930d7a50cd23264e9a2be3ef6c853585 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 21 Jan 2025 17:06:09 +0000 Subject: [PATCH 36/39] Remove testStep endpoint and fix types. --- .../server/src/api/controllers/automation.ts | 52 +------------------ packages/server/src/api/routes/automation.ts | 12 +---- .../server/src/automations/steps/createRow.ts | 9 ++-- .../server/src/automations/steps/deleteRow.ts | 9 ++-- .../src/automations/steps/executeQuery.ts | 4 +- .../src/automations/steps/executeScript.ts | 4 +- .../server/src/automations/steps/updateRow.ts | 9 ++-- .../server/src/automations/steps/utils.ts | 4 +- packages/server/src/events/NoopEmitter.ts | 39 -------------- packages/server/src/events/index.ts | 1 - .../src/tests/utilities/api/automation.ts | 17 ------ packages/types/src/api/web/app/automation.ts | 7 --- 12 files changed, 26 insertions(+), 141 deletions(-) delete mode 100644 packages/server/src/events/NoopEmitter.ts diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index a77014cf31..13d057ebb7 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -28,18 +28,11 @@ import { TriggerAutomationResponse, TestAutomationRequest, TestAutomationResponse, - TestAutomationStepRequest, - TestAutomationStepResponse, } from "@budibase/types" -import { - getActionDefinitions as actionDefs, - getAction, -} from "../../automations/actions" +import { getActionDefinitions as actionDefs } from "../../automations/actions" import sdk from "../../sdk" import { builderSocket } from "../../websockets" import env from "../../environment" -import { NoopEmitter } from "../../events" -import { enrichBaseContext } from "../../threads/automation" async function getActionDefinitions() { return removeDeprecated(await actionDefs()) @@ -260,46 +253,3 @@ export async function test( await events.automation.tested(automation) } - -export async function testStep( - ctx: UserCtx -) { - const { id, stepId } = ctx.params - const db = context.getAppDB() - const automation = await db.tryGet(id) - if (!automation) { - ctx.throw(404, `Automation ${ctx.params.id} not found`) - } - - const step = automation.definition.steps.find(s => s.stepId === stepId) - if (!step) { - ctx.throw(404, `Step ${stepId} not found on automation ${id}`) - } - - if (step.stepId === AutomationActionStepId.BRANCH) { - ctx.throw(400, "Branch steps cannot be tested directly") - } - if (step.stepId === AutomationActionStepId.LOOP) { - ctx.throw(400, "Loop steps cannot be tested directly") - } - - const { body } = ctx.request - - const fn = await getAction(step.stepId) - if (!fn) { - ctx.throw(400, `Step ${stepId} is not a valid step`) - } - - const output = await withTestFlag( - automation._id!, - async () => - await fn({ - inputs: body.inputs, - context: await enrichBaseContext(body.context), - appId: ctx.appId, - emitter: new NoopEmitter(), - }) - ) - - ctx.body = output -} diff --git a/packages/server/src/api/routes/automation.ts b/packages/server/src/api/routes/automation.ts index ea905be0cd..489487271c 100644 --- a/packages/server/src/api/routes/automation.ts +++ b/packages/server/src/api/routes/automation.ts @@ -1,6 +1,6 @@ import Router from "@koa/router" import * as controller from "../controllers/automation" -import authorized, { authorizedResource } from "../../middleware/authorized" +import authorized from "../../middleware/authorized" import { permissions } from "@budibase/backend-core" import { bodyResource, paramResource } from "../../middleware/resourceId" import { @@ -82,15 +82,5 @@ router ), controller.test ) - .post( - "/api/automations/:id/step/:stepId/test", - appInfoMiddleware({ appType: AppType.DEV }), - authorizedResource( - permissions.PermissionType.AUTOMATION, - permissions.PermissionLevel.EXECUTE, - "id" - ), - controller.testStep - ) export default router diff --git a/packages/server/src/automations/steps/createRow.ts b/packages/server/src/automations/steps/createRow.ts index 24dada422d..cf915dd300 100644 --- a/packages/server/src/automations/steps/createRow.ts +++ b/packages/server/src/automations/steps/createRow.ts @@ -5,8 +5,11 @@ import { sendAutomationAttachmentsToStorage, } from "../automationUtils" import { buildCtx } from "./utils" -import { CreateRowStepInputs, CreateRowStepOutputs } from "@budibase/types" -import { EventEmitter } from "events" +import { + ContextEmitter, + CreateRowStepInputs, + CreateRowStepOutputs, +} from "@budibase/types" export async function run({ inputs, @@ -15,7 +18,7 @@ export async function run({ }: { inputs: CreateRowStepInputs appId: string - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.row == null || inputs.row.tableId == null) { return { diff --git a/packages/server/src/automations/steps/deleteRow.ts b/packages/server/src/automations/steps/deleteRow.ts index 7c50fe4dcb..2498a4e4de 100644 --- a/packages/server/src/automations/steps/deleteRow.ts +++ b/packages/server/src/automations/steps/deleteRow.ts @@ -1,8 +1,11 @@ -import { EventEmitter } from "events" import { destroy } from "../../api/controllers/row" import { buildCtx } from "./utils" import { getError } from "../automationUtils" -import { DeleteRowStepInputs, DeleteRowStepOutputs } from "@budibase/types" +import { + ContextEmitter, + DeleteRowStepInputs, + DeleteRowStepOutputs, +} from "@budibase/types" export async function run({ inputs, @@ -11,7 +14,7 @@ export async function run({ }: { inputs: DeleteRowStepInputs appId: string - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.id == null) { return { diff --git a/packages/server/src/automations/steps/executeQuery.ts b/packages/server/src/automations/steps/executeQuery.ts index 9816e31b1e..ad99240eb8 100644 --- a/packages/server/src/automations/steps/executeQuery.ts +++ b/packages/server/src/automations/steps/executeQuery.ts @@ -1,8 +1,8 @@ -import { EventEmitter } from "events" import * as queryController from "../../api/controllers/query" import { buildCtx } from "./utils" import * as automationUtils from "../automationUtils" import { + ContextEmitter, ExecuteQueryStepInputs, ExecuteQueryStepOutputs, } from "@budibase/types" @@ -14,7 +14,7 @@ export async function run({ }: { inputs: ExecuteQueryStepInputs appId: string - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.query == null) { return { diff --git a/packages/server/src/automations/steps/executeScript.ts b/packages/server/src/automations/steps/executeScript.ts index 105543d34c..db05d0937a 100644 --- a/packages/server/src/automations/steps/executeScript.ts +++ b/packages/server/src/automations/steps/executeScript.ts @@ -2,10 +2,10 @@ import * as scriptController from "../../api/controllers/script" import { buildCtx } from "./utils" import * as automationUtils from "../automationUtils" import { + ContextEmitter, ExecuteScriptStepInputs, ExecuteScriptStepOutputs, } from "@budibase/types" -import { EventEmitter } from "events" export async function run({ inputs, @@ -16,7 +16,7 @@ export async function run({ inputs: ExecuteScriptStepInputs appId: string context: object - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.code == null) { return { diff --git a/packages/server/src/automations/steps/updateRow.ts b/packages/server/src/automations/steps/updateRow.ts index 46ae2a5c74..7a62e40706 100644 --- a/packages/server/src/automations/steps/updateRow.ts +++ b/packages/server/src/automations/steps/updateRow.ts @@ -1,8 +1,11 @@ -import { EventEmitter } from "events" import * as rowController from "../../api/controllers/row" import * as automationUtils from "../automationUtils" import { buildCtx } from "./utils" -import { UpdateRowStepInputs, UpdateRowStepOutputs } from "@budibase/types" +import { + ContextEmitter, + UpdateRowStepInputs, + UpdateRowStepOutputs, +} from "@budibase/types" export async function run({ inputs, @@ -11,7 +14,7 @@ export async function run({ }: { inputs: UpdateRowStepInputs appId: string - emitter: EventEmitter + emitter: ContextEmitter }): Promise { if (inputs.rowId == null || inputs.row == null) { return { diff --git a/packages/server/src/automations/steps/utils.ts b/packages/server/src/automations/steps/utils.ts index 8b99044303..20f1e67589 100644 --- a/packages/server/src/automations/steps/utils.ts +++ b/packages/server/src/automations/steps/utils.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from "events" +import { ContextEmitter } from "@budibase/types" export async function getFetchResponse(fetched: any) { let status = fetched.status, @@ -22,7 +22,7 @@ export async function getFetchResponse(fetched: any) { // opts can contain, body, params and version export function buildCtx( appId: string, - emitter?: EventEmitter | null, + emitter?: ContextEmitter | null, opts: any = {} ) { const ctx: any = { diff --git a/packages/server/src/events/NoopEmitter.ts b/packages/server/src/events/NoopEmitter.ts deleted file mode 100644 index ed87618ead..0000000000 --- a/packages/server/src/events/NoopEmitter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { EventEmitter } from "events" -import { - Table, - Row, - ContextEmitter, - EventType, - UserBindings, -} from "@budibase/types" - -export class NoopEmitter extends EventEmitter implements ContextEmitter { - emitRow(values: { - eventName: EventType.ROW_SAVE - appId: string - row: Row - table: Table - user: UserBindings - }): void - emitRow(values: { - eventName: EventType.ROW_UPDATE - appId: string - row: Row - table: Table - oldRow: Row - user: UserBindings - }): void - emitRow(values: { - eventName: EventType.ROW_DELETE - appId: string - row: Row - user: UserBindings - }): void - emitRow(_values: unknown): void { - return - } - - emitTable(_eventName: string, _appId: string, _table?: Table) { - return - } -} diff --git a/packages/server/src/events/index.ts b/packages/server/src/events/index.ts index 90bf932bcf..23c3f3e512 100644 --- a/packages/server/src/events/index.ts +++ b/packages/server/src/events/index.ts @@ -2,6 +2,5 @@ import BudibaseEmitter from "./BudibaseEmitter" const emitter = new BudibaseEmitter() -export { NoopEmitter } from "./NoopEmitter" export { init } from "./docUpdates" export default emitter diff --git a/packages/server/src/tests/utilities/api/automation.ts b/packages/server/src/tests/utilities/api/automation.ts index 6041664999..3f51385251 100644 --- a/packages/server/src/tests/utilities/api/automation.ts +++ b/packages/server/src/tests/utilities/api/automation.ts @@ -3,8 +3,6 @@ import { FetchAutomationResponse, TestAutomationRequest, TestAutomationResponse, - TestAutomationStepRequest, - TestAutomationStepResponse, } from "@budibase/types" import { Expectations, TestAPI } from "./base" @@ -54,19 +52,4 @@ export class AutomationAPI extends TestAPI { } ) } - - testStep = async ( - id: string, - stepId: string, - body: TestAutomationStepRequest, - expectations?: Expectations - ): Promise => { - return await this._post( - `/api/automations/${id}/steps/${stepId}/test`, - { - body, - expectations, - } - ) - } } diff --git a/packages/types/src/api/web/app/automation.ts b/packages/types/src/api/web/app/automation.ts index edff4b5eaf..b97dee0baf 100644 --- a/packages/types/src/api/web/app/automation.ts +++ b/packages/types/src/api/web/app/automation.ts @@ -83,10 +83,3 @@ export function isDidNotTriggerResponse( ): response is DidNotTriggerResponse { return !!("message" in response && response.message) } - -export interface TestAutomationStepRequest { - inputs: Record - context: Record -} - -export type TestAutomationStepResponse = any From 1294f83ccb9d80f7bcacaf89e9cab7b43f6c4d70 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 22 Jan 2025 11:09:31 +0000 Subject: [PATCH 37/39] Bump version to 3.3.0 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 50582f0a95..a7d534ff01 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.47", + "version": "3.3.0", "npmClient": "yarn", "concurrency": 20, "command": { From 2a5865ecaf6eb9de7a5a6e808bb0580e792ae630 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 22 Jan 2025 16:38:29 +0100 Subject: [PATCH 38/39] Fix creating new table screen modal --- packages/builder/src/helpers/data/format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/helpers/data/format.js b/packages/builder/src/helpers/data/format.js index 428ce273b2..ba274d5464 100644 --- a/packages/builder/src/helpers/data/format.js +++ b/packages/builder/src/helpers/data/format.js @@ -11,7 +11,7 @@ export const datasourceSelect = { }, viewV2: (view, datasources) => { const datasource = datasources - .filter(f => f.entities) + ?.filter(f => f.entities) .flatMap(d => d.entities) .find(ds => ds._id === view.tableId) return { From abc1ba33356b3a2a8cb67ca586e53c8e712d74dd Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 22 Jan 2025 15:46:49 +0000 Subject: [PATCH 39/39] Bump version to 3.3.1 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index a7d534ff01..13040cb50c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.3.0", + "version": "3.3.1", "npmClient": "yarn", "concurrency": 20, "command": {