@budibase/server 2.6.19-alpha.30 → 2.6.19-alpha.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/server",
3
3
  "email": "hi@budibase.com",
4
- "version": "2.6.19-alpha.30",
4
+ "version": "2.6.19-alpha.34",
5
5
  "description": "Budibase Web Server",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -46,12 +46,12 @@
46
46
  "license": "GPL-3.0",
47
47
  "dependencies": {
48
48
  "@apidevtools/swagger-parser": "10.0.3",
49
- "@budibase/backend-core": "2.6.19-alpha.30",
50
- "@budibase/client": "2.6.19-alpha.30",
51
- "@budibase/pro": "2.6.19-alpha.30",
52
- "@budibase/shared-core": "2.6.19-alpha.30",
53
- "@budibase/string-templates": "2.6.19-alpha.30",
54
- "@budibase/types": "2.6.19-alpha.30",
49
+ "@budibase/backend-core": "2.6.19-alpha.34",
50
+ "@budibase/client": "2.6.19-alpha.34",
51
+ "@budibase/pro": "2.6.19-alpha.34",
52
+ "@budibase/shared-core": "2.6.19-alpha.34",
53
+ "@budibase/string-templates": "2.6.19-alpha.34",
54
+ "@budibase/types": "2.6.19-alpha.34",
55
55
  "@bull-board/api": "3.7.0",
56
56
  "@bull-board/koa": "3.9.4",
57
57
  "@elastic/elasticsearch": "7.10.0",
@@ -100,6 +100,7 @@
100
100
  "mssql": "6.2.3",
101
101
  "mysql2": "2.3.3",
102
102
  "node-fetch": "2.6.7",
103
+ "object-sizeof": "2.6.1",
103
104
  "open": "8.4.0",
104
105
  "openai": "^3.2.1",
105
106
  "pg": "8.10.0",
@@ -195,5 +196,5 @@
195
196
  }
196
197
  }
197
198
  },
198
- "gitHead": "c62ae2d9ea7f29de54c4cb6ef9a8f22e82eb8460"
199
+ "gitHead": "4f0603ba8ea979e75596426fcc3e2f75c0d0a4a6"
199
200
  }
@@ -2,6 +2,23 @@ import env from "../../environment"
2
2
  import { AutomationResults, Automation, App } from "@budibase/types"
3
3
  import { automations } from "@budibase/pro"
4
4
  import { db as dbUtils } from "@budibase/backend-core"
5
+ import sizeof from "object-sizeof"
6
+
7
+ const MAX_LOG_SIZE_MB = 5
8
+ const MB_IN_BYTES = 1024 * 1024
9
+
10
+ function sanitiseResults(results: AutomationResults) {
11
+ const message = `[removed] - max results size of ${MAX_LOG_SIZE_MB}MB exceeded`
12
+ for (let step of results.steps) {
13
+ step.inputs = {
14
+ message,
15
+ }
16
+ step.outputs = {
17
+ message,
18
+ success: step.outputs.success,
19
+ }
20
+ }
21
+ }
5
22
 
6
23
  export async function storeLog(
7
24
  automation: Automation,
@@ -11,6 +28,10 @@ export async function storeLog(
11
28
  if (env.DISABLE_AUTOMATION_LOGS) {
12
29
  return
13
30
  }
31
+ const bytes = sizeof(results)
32
+ if (bytes / MB_IN_BYTES > MAX_LOG_SIZE_MB) {
33
+ sanitiseResults(results)
34
+ }
14
35
  await automations.logs.storeLog(automation, results)
15
36
  }
16
37
 
@@ -80,6 +80,7 @@ const environment = {
80
80
  ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
81
81
  SELF_HOSTED: process.env.SELF_HOSTED,
82
82
  HTTP_MB_LIMIT: process.env.HTTP_MB_LIMIT,
83
+ FORKED_PROCESS_NAME: process.env.FORKED_PROCESS_NAME || "main",
83
84
  // old
84
85
  CLIENT_ID: process.env.CLIENT_ID,
85
86
  _set(key: string, value: any) {
@@ -9,8 +9,8 @@ import {
9
9
  checkDebounce,
10
10
  setDebounce,
11
11
  } from "../utilities/redis"
12
- import { db as dbCore, cache, permissions } from "@budibase/backend-core"
13
- import { BBContext, Database } from "@budibase/types"
12
+ import { db as dbCore, cache } from "@budibase/backend-core"
13
+ import { UserCtx, Database } from "@budibase/types"
14
14
 
15
15
  const DEBOUNCE_TIME_SEC = 30
16
16
 
@@ -23,7 +23,7 @@ const DEBOUNCE_TIME_SEC = 30
23
23
  * through the authorized middleware *
24
24
  ****************************************************/
25
25
 
26
- async function checkDevAppLocks(ctx: BBContext) {
26
+ async function checkDevAppLocks(ctx: UserCtx) {
27
27
  const appId = ctx.appId
28
28
 
29
29
  // if any public usage, don't proceed
@@ -42,7 +42,7 @@ async function checkDevAppLocks(ctx: BBContext) {
42
42
  }
43
43
  }
44
44
 
45
- async function updateAppUpdatedAt(ctx: BBContext) {
45
+ async function updateAppUpdatedAt(ctx: UserCtx) {
46
46
  const appId = ctx.appId
47
47
  // if debouncing skip this update
48
48
  // get methods also aren't updating
@@ -50,20 +50,29 @@ async function updateAppUpdatedAt(ctx: BBContext) {
50
50
  return
51
51
  }
52
52
  await dbCore.doWithDB(appId, async (db: Database) => {
53
- const metadata = await db.get(DocumentType.APP_METADATA)
54
- metadata.updatedAt = new Date().toISOString()
53
+ try {
54
+ const metadata = await db.get(DocumentType.APP_METADATA)
55
+ metadata.updatedAt = new Date().toISOString()
55
56
 
56
- metadata.updatedBy = getGlobalIDFromUserMetadataID(ctx.user?.userId!)
57
+ metadata.updatedBy = getGlobalIDFromUserMetadataID(ctx.user?.userId!)
57
58
 
58
- const response = await db.put(metadata)
59
- metadata._rev = response.rev
60
- await cache.app.invalidateAppMetadata(appId, metadata)
61
- // set a new debounce record with a short TTL
62
- await setDebounce(appId, DEBOUNCE_TIME_SEC)
59
+ const response = await db.put(metadata)
60
+ metadata._rev = response.rev
61
+ await cache.app.invalidateAppMetadata(appId, metadata)
62
+ // set a new debounce record with a short TTL
63
+ await setDebounce(appId, DEBOUNCE_TIME_SEC)
64
+ } catch (err: any) {
65
+ // if a 409 occurs, then multiple clients connected at the same time - ignore
66
+ if (err?.status === 409) {
67
+ return
68
+ } else {
69
+ throw err
70
+ }
71
+ }
63
72
  })
64
73
  }
65
74
 
66
- export default async function builder(ctx: BBContext) {
75
+ export default async function builder(ctx: UserCtx) {
67
76
  const appId = ctx.appId
68
77
  // this only functions within an app context
69
78
  if (!appId) {
@@ -19,6 +19,7 @@ import {
19
19
  AutomationStatus,
20
20
  AutomationMetadata,
21
21
  AutomationJob,
22
+ AutomationData,
22
23
  } from "@budibase/types"
23
24
  import {
24
25
  LoopStep,
@@ -30,6 +31,7 @@ import { WorkerCallback } from "./definitions"
30
31
  import { context, logging } from "@budibase/backend-core"
31
32
  import { processObject } from "@budibase/string-templates"
32
33
  import { cloneDeep } from "lodash/fp"
34
+ import { performance } from "perf_hooks"
33
35
  import * as sdkUtils from "../sdk/utils"
34
36
  import env from "../environment"
35
37
  const FILTER_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.FILTER.stepId
@@ -37,8 +39,8 @@ const LOOP_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.LOOP.stepId
37
39
  const CRON_STEP_ID = triggerDefs.CRON.stepId
38
40
  const STOPPED_STATUS = { success: true, status: AutomationStatus.STOPPED }
39
41
 
40
- function getLoopIterations(loopStep: LoopStep, input: LoopInput) {
41
- const binding = automationUtils.typecastForLooping(loopStep, input)
42
+ function getLoopIterations(loopStep: LoopStep) {
43
+ let binding = loopStep.inputs.binding
42
44
  if (!binding) {
43
45
  return 0
44
46
  }
@@ -68,7 +70,6 @@ class Orchestrator {
68
70
  constructor(job: AutomationJob) {
69
71
  let automation = job.data.automation
70
72
  let triggerOutput = job.data.event
71
- let timeout = job.data.event.timeout
72
73
  const metadata = triggerOutput.metadata
73
74
  this._chainCount = metadata ? metadata.automationChainCount! : 0
74
75
  this._appId = triggerOutput.appId as string
@@ -252,7 +253,7 @@ class Orchestrator {
252
253
  return
253
254
  }
254
255
  }
255
-
256
+ const start = performance.now()
256
257
  for (let step of automation.definition.steps) {
257
258
  if (timeoutFlag) {
258
259
  break
@@ -277,22 +278,17 @@ class Orchestrator {
277
278
 
278
279
  if (loopStep) {
279
280
  input = await processObject(loopStep.inputs, this._context)
280
- iterations = getLoopIterations(loopStep as LoopStep, input)
281
+ iterations = getLoopIterations(loopStep as LoopStep)
281
282
  }
282
283
  for (let index = 0; index < iterations; index++) {
283
284
  let originalStepInput = cloneDeep(step.inputs)
284
285
  // Handle if the user has set a max iteration count or if it reaches the max limit set by us
285
286
  if (loopStep && input.binding) {
286
- let newInput: any = await processObject(
287
- loopStep.inputs,
288
- cloneDeep(this._context)
289
- )
290
-
291
287
  let tempOutput = { items: loopSteps, iterations: iterationCount }
292
288
  try {
293
- newInput.binding = automationUtils.typecastForLooping(
289
+ loopStep.inputs.binding = automationUtils.typecastForLooping(
294
290
  loopStep as LoopStep,
295
- newInput
291
+ loopStep.inputs as LoopInput
296
292
  )
297
293
  } catch (err) {
298
294
  this.updateContextAndOutput(loopStepNumber, step, tempOutput, {
@@ -303,13 +299,12 @@ class Orchestrator {
303
299
  loopStep = undefined
304
300
  break
305
301
  }
306
-
307
302
  let item = []
308
303
  if (
309
304
  typeof loopStep.inputs.binding === "string" &&
310
305
  loopStep.inputs.option === "String"
311
306
  ) {
312
- item = automationUtils.stringSplit(newInput.binding)
307
+ item = automationUtils.stringSplit(loopStep.inputs.binding)
313
308
  } else if (Array.isArray(loopStep.inputs.binding)) {
314
309
  item = loopStep.inputs.binding
315
310
  }
@@ -351,6 +346,7 @@ class Orchestrator {
351
346
  }
352
347
  }
353
348
  }
349
+
354
350
  if (
355
351
  index === env.AUTOMATION_MAX_ITERATIONS ||
356
352
  index === parseInt(loopStep.inputs.iterations)
@@ -479,8 +475,25 @@ class Orchestrator {
479
475
  }
480
476
  }
481
477
 
478
+ const end = performance.now()
479
+ const executionTime = end - start
480
+
481
+ console.info(`Execution time: ${executionTime} milliseconds`, {
482
+ _logKey: "automation",
483
+ executionTime,
484
+ })
485
+
482
486
  // store the logs for the automation run
483
- await storeLog(this._automation, this.executionOutput)
487
+ try {
488
+ await storeLog(this._automation, this.executionOutput)
489
+ } catch (e: any) {
490
+ if (e.status === 413 && e.request?.data) {
491
+ // if content is too large we shouldn't log it
492
+ delete e.request.data
493
+ e.request.data = { message: "removed due to large size" }
494
+ }
495
+ logging.logAlert("Error writing automation log", e)
496
+ }
484
497
  if (isProdAppID(this._appId) && isRecurring(automation) && metadata) {
485
498
  await this.updateMetadata(metadata)
486
499
  }
@@ -488,23 +501,31 @@ class Orchestrator {
488
501
  }
489
502
  }
490
503
 
491
- export function execute(job: Job, callback: WorkerCallback) {
504
+ export function execute(job: Job<AutomationData>, callback: WorkerCallback) {
492
505
  const appId = job.data.event.appId
506
+ const automationId = job.data.automation._id
493
507
  if (!appId) {
494
508
  throw new Error("Unable to execute, event doesn't contain app ID.")
495
509
  }
496
- return context.doInAppContext(appId, async () => {
497
- const envVars = await sdkUtils.getEnvironmentVariables()
498
- // put into automation thread for whole context
499
- await context.doInEnvironmentContext(envVars, async () => {
500
- const automationOrchestrator = new Orchestrator(job)
501
- try {
502
- const response = await automationOrchestrator.execute()
503
- callback(null, response)
504
- } catch (err) {
505
- callback(err)
506
- }
507
- })
510
+ if (!automationId) {
511
+ throw new Error("Unable to execute, event doesn't contain automation ID.")
512
+ }
513
+ return context.doInAutomationContext({
514
+ appId,
515
+ automationId,
516
+ task: async () => {
517
+ const envVars = await sdkUtils.getEnvironmentVariables()
518
+ // put into automation thread for whole context
519
+ await context.doInEnvironmentContext(envVars, async () => {
520
+ const automationOrchestrator = new Orchestrator(job)
521
+ try {
522
+ const response = await automationOrchestrator.execute()
523
+ callback(null, response)
524
+ } catch (err) {
525
+ callback(err)
526
+ }
527
+ })
528
+ },
508
529
  })
509
530
  }
510
531
 
@@ -38,6 +38,9 @@ export class Thread {
38
38
  this.count = opts.count ? opts.count : 1
39
39
  this.disableThreading = this.shouldDisableThreading()
40
40
  if (!this.disableThreading) {
41
+ console.debug(
42
+ `[${env.FORKED_PROCESS_NAME}] initialising worker farm type=${type}`
43
+ )
41
44
  const workerOpts: any = {
42
45
  autoStart: true,
43
46
  maxConcurrentWorkers: this.count,
@@ -45,6 +48,7 @@ export class Thread {
45
48
  env: {
46
49
  ...process.env,
47
50
  FORKED_PROCESS: "1",
51
+ FORKED_PROCESS_NAME: type,
48
52
  },
49
53
  },
50
54
  }
@@ -54,6 +58,10 @@ export class Thread {
54
58
  }
55
59
  this.workers = workerFarm(workerOpts, typeToFile(type), ["execute"])
56
60
  Thread.workerRefs.push(this.workers)
61
+ } else {
62
+ console.debug(
63
+ `[${env.FORKED_PROCESS_NAME}] skipping worker farm type=${type}`
64
+ )
57
65
  }
58
66
  }
59
67
 
@@ -72,9 +80,7 @@ export class Thread {
72
80
  function fire(worker: any) {
73
81
  worker.execute(job, (err: any, response: any) => {
74
82
  if (err && err.type === "TimeoutError") {
75
- reject(
76
- new Error(`Query response time exceeded ${timeout}ms timeout.`)
77
- )
83
+ reject(new Error(`Thread timeout exceeded ${timeout}ms timeout.`))
78
84
  } else if (err) {
79
85
  reject(err)
80
86
  } else {
@@ -26,8 +26,10 @@ function makeVariableKey(queryId: string, variable: string) {
26
26
  export function threadSetup() {
27
27
  // don't run this if not threading
28
28
  if (env.isTest() || env.DISABLE_THREADING || !env.isInThread()) {
29
+ console.debug(`[${env.FORKED_PROCESS_NAME}] thread setup skipped`)
29
30
  return
30
31
  }
32
+ console.debug(`[${env.FORKED_PROCESS_NAME}] thread setup running`)
31
33
  db.init()
32
34
  }
33
35
 
@@ -35,10 +35,20 @@ export const getComponentLibraryManifest = async (library: string) => {
35
35
  const filename = "manifest.json"
36
36
 
37
37
  if (env.isDev() || env.isTest()) {
38
- const path = join(TOP_LEVEL_PATH, "packages/client", filename)
39
- // always load from new so that updates are refreshed
40
- delete require.cache[require.resolve(path)]
41
- return require(path)
38
+ const paths = [
39
+ join(TOP_LEVEL_PATH, "packages/client", filename),
40
+ join(process.cwd(), "client", filename),
41
+ ]
42
+ for (let path of paths) {
43
+ if (fs.existsSync(path)) {
44
+ // always load from new so that updates are refreshed
45
+ delete require.cache[require.resolve(path)]
46
+ return require(path)
47
+ }
48
+ }
49
+ throw new Error(
50
+ `Unable to find ${filename} in development environment (may need to build).`
51
+ )
42
52
  }
43
53
 
44
54
  if (!appId) {