@budibase/backend-core 2.24.2 → 2.26.0

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/src/cache/user.ts CHANGED
@@ -69,7 +69,7 @@ async function populateUsersFromDB(
69
69
  export async function getUser(
70
70
  userId: string,
71
71
  tenantId?: string,
72
- populateUser?: any
72
+ populateUser?: (userId: string, tenantId: string) => Promise<User>
73
73
  ) {
74
74
  if (!populateUser) {
75
75
  populateUser = populateFromDB
@@ -83,7 +83,7 @@ export async function getUser(
83
83
  }
84
84
  const client = await redis.getUserClient()
85
85
  // try cache
86
- let user = await client.get(userId)
86
+ let user: User = await client.get(userId)
87
87
  if (!user) {
88
88
  user = await populateUser(userId, tenantId)
89
89
  await client.store(userId, user, EXPIRY_SECONDS)
@@ -281,7 +281,7 @@ export function doInScimContext(task: any) {
281
281
  return newContext(updates, task)
282
282
  }
283
283
 
284
- export async function ensureSnippetContext() {
284
+ export async function ensureSnippetContext(enabled = !env.isTest()) {
285
285
  const ctx = getCurrentContext()
286
286
 
287
287
  // If we've already added snippets to context, continue
@@ -292,7 +292,7 @@ export async function ensureSnippetContext() {
292
292
  // Otherwise get snippets for this app and update context
293
293
  let snippets: Snippet[] | undefined
294
294
  const db = getAppDB()
295
- if (db && !env.isTest()) {
295
+ if (db && enabled) {
296
296
  const app = await db.get<App>(DocumentType.APP_METADATA)
297
297
  snippets = app.snippets
298
298
  }
@@ -3,11 +3,11 @@ import {
3
3
  AllDocsResponse,
4
4
  AnyDocument,
5
5
  Database,
6
- DatabaseOpts,
7
- DatabaseQueryOpts,
8
- DatabasePutOpts,
9
6
  DatabaseCreateIndexOpts,
10
7
  DatabaseDeleteIndexOpts,
8
+ DatabaseOpts,
9
+ DatabasePutOpts,
10
+ DatabaseQueryOpts,
11
11
  Document,
12
12
  isDocument,
13
13
  RowResponse,
@@ -17,7 +17,7 @@ import {
17
17
  import { getCouchInfo } from "./connections"
18
18
  import { directCouchUrlCall } from "./utils"
19
19
  import { getPouchDB } from "./pouchDB"
20
- import { WriteStream, ReadStream } from "fs"
20
+ import { ReadStream, WriteStream } from "fs"
21
21
  import { newid } from "../../docIds/newid"
22
22
  import { SQLITE_DESIGN_DOC_ID } from "../../constants"
23
23
  import { DDInstrumentedDatabase } from "../instrumentation"
@@ -38,6 +38,39 @@ function buildNano(couchInfo: { url: string; cookie: string }) {
38
38
 
39
39
  type DBCall<T> = () => Promise<T>
40
40
 
41
+ class CouchDBError extends Error {
42
+ status: number
43
+ statusCode: number
44
+ reason: string
45
+ name: string
46
+ errid: string
47
+ error: string
48
+ description: string
49
+
50
+ constructor(
51
+ message: string,
52
+ info: {
53
+ status: number | undefined
54
+ statusCode: number | undefined
55
+ name: string
56
+ errid: string
57
+ description: string
58
+ reason: string
59
+ error: string
60
+ }
61
+ ) {
62
+ super(message)
63
+ const statusCode = info.status || info.statusCode || 500
64
+ this.status = statusCode
65
+ this.statusCode = statusCode
66
+ this.reason = info.reason
67
+ this.name = info.name
68
+ this.errid = info.errid
69
+ this.description = info.description
70
+ this.error = info.error
71
+ }
72
+ }
73
+
41
74
  export function DatabaseWithConnection(
42
75
  dbName: string,
43
76
  connection: string,
@@ -119,7 +152,7 @@ export class DatabaseImpl implements Database {
119
152
  } catch (err: any) {
120
153
  // Handling race conditions
121
154
  if (err.statusCode !== 412) {
122
- throw err
155
+ throw new CouchDBError(err.message, err)
123
156
  }
124
157
  }
125
158
  }
@@ -138,10 +171,9 @@ export class DatabaseImpl implements Database {
138
171
  if (err.statusCode === 404 && err.reason === DATABASE_NOT_FOUND) {
139
172
  await this.checkAndCreateDb()
140
173
  return await this.performCall(call)
141
- } else if (err.statusCode) {
142
- err.status = err.statusCode
143
174
  }
144
- throw err
175
+ // stripping the error down the props which are safe/useful, drop everything else
176
+ throw new CouchDBError(`CouchDB error: ${err.message}`, err)
145
177
  }
146
178
  }
147
179
 
@@ -288,7 +320,7 @@ export class DatabaseImpl implements Database {
288
320
  if (err.statusCode === 404) {
289
321
  return
290
322
  } else {
291
- throw { ...err, status: err.statusCode }
323
+ throw new CouchDBError(err.message, err)
292
324
  }
293
325
  }
294
326
  }
@@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context"
13
13
  import { decrypt } from "../security/encryption"
14
14
  import * as identity from "../context/identity"
15
15
  import env from "../environment"
16
- import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
16
+ import { Ctx, EndpointMatcher, SessionCookie, User } from "@budibase/types"
17
17
  import { InvalidAPIKeyError, ErrorCode } from "../errors"
18
18
  import tracer from "dd-trace"
19
19
 
@@ -41,7 +41,10 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
41
41
  ctx.version = opts.version
42
42
  }
43
43
 
44
- async function checkApiKey(apiKey: string, populateUser?: Function) {
44
+ async function checkApiKey(
45
+ apiKey: string,
46
+ populateUser?: (userId: string, tenantId: string) => Promise<User>
47
+ ) {
45
48
  // check both the primary and the fallback internal api keys
46
49
  // this allows for rotation
47
50
  if (isValidInternalAPIKey(apiKey)) {
@@ -128,6 +131,7 @@ export default function (
128
131
  } else {
129
132
  user = await getUser(userId, session.tenantId)
130
133
  }
134
+ // @ts-ignore
131
135
  user.csrfToken = session.csrfToken
132
136
 
133
137
  if (session?.lastAccessedAt < timeMinusOneMinute()) {
@@ -167,19 +171,25 @@ export default function (
167
171
  authenticated = false
168
172
  }
169
173
 
170
- if (user) {
174
+ const isUser = (
175
+ user: any
176
+ ): user is User & { budibaseAccess?: string } => {
177
+ return user && user.email
178
+ }
179
+
180
+ if (isUser(user)) {
171
181
  tracer.setUser({
172
- id: user?._id,
173
- tenantId: user?.tenantId,
174
- budibaseAccess: user?.budibaseAccess,
175
- status: user?.status,
182
+ id: user._id!,
183
+ tenantId: user.tenantId,
184
+ budibaseAccess: user.budibaseAccess,
185
+ status: user.status,
176
186
  })
177
187
  }
178
188
 
179
189
  // isAuthenticated is a function, so use a variable to be able to check authed state
180
190
  finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
181
191
 
182
- if (user && user.email) {
192
+ if (isUser(user)) {
183
193
  return identity.doInUserContext(user, ctx, next)
184
194
  } else {
185
195
  return next()
@@ -13,13 +13,14 @@ import { bucketTTLConfig, budibaseTempDir } from "./utils"
13
13
  import { v4 } from "uuid"
14
14
  import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
15
15
  import fsp from "fs/promises"
16
+ import { HeadObjectOutput } from "aws-sdk/clients/s3"
16
17
 
17
18
  const streamPipeline = promisify(stream.pipeline)
18
19
  // use this as a temporary store of buckets that are being created
19
20
  const STATE = {
20
21
  bucketCreationPromises: {},
21
22
  }
22
- const signedFilePrefix = "/files/signed"
23
+ export const SIGNED_FILE_PREFIX = "/files/signed"
23
24
 
24
25
  type ListParams = {
25
26
  ContinuationToken?: string
@@ -40,8 +41,13 @@ type UploadParams = BaseUploadParams & {
40
41
  path?: string | PathLike
41
42
  }
42
43
 
43
- type StreamUploadParams = BaseUploadParams & {
44
- stream: ReadStream
44
+ export type StreamTypes =
45
+ | ReadStream
46
+ | NodeJS.ReadableStream
47
+ | ReadableStream<Uint8Array>
48
+
49
+ export type StreamUploadParams = BaseUploadParams & {
50
+ stream?: StreamTypes
45
51
  }
46
52
 
47
53
  const CONTENT_TYPE_MAP: any = {
@@ -174,11 +180,9 @@ export async function upload({
174
180
  const objectStore = ObjectStore(bucketName)
175
181
  const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
176
182
 
177
- if (ttl && (bucketCreated.created || bucketCreated.exists)) {
183
+ if (ttl && bucketCreated.created) {
178
184
  let ttlConfig = bucketTTLConfig(bucketName, ttl)
179
- if (objectStore.putBucketLifecycleConfiguration) {
180
- await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
181
- }
185
+ await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
182
186
  }
183
187
 
184
188
  let contentType = type
@@ -222,11 +226,9 @@ export async function streamUpload({
222
226
  const objectStore = ObjectStore(bucketName)
223
227
  const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
224
228
 
225
- if (ttl && (bucketCreated.created || bucketCreated.exists)) {
229
+ if (ttl && bucketCreated.created) {
226
230
  let ttlConfig = bucketTTLConfig(bucketName, ttl)
227
- if (objectStore.putBucketLifecycleConfiguration) {
228
- await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
229
- }
231
+ await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
230
232
  }
231
233
 
232
234
  // Set content type for certain known extensions
@@ -333,7 +335,7 @@ export function getPresignedUrl(
333
335
  const signedUrl = new URL(url)
334
336
  const path = signedUrl.pathname
335
337
  const query = signedUrl.search
336
- return `${signedFilePrefix}${path}${query}`
338
+ return `${SIGNED_FILE_PREFIX}${path}${query}`
337
339
  }
338
340
  }
339
341
 
@@ -521,6 +523,26 @@ export async function getReadStream(
521
523
  return client.getObject(params).createReadStream()
522
524
  }
523
525
 
526
+ export async function getObjectMetadata(
527
+ bucket: string,
528
+ path: string
529
+ ): Promise<HeadObjectOutput> {
530
+ bucket = sanitizeBucket(bucket)
531
+ path = sanitizeKey(path)
532
+
533
+ const client = ObjectStore(bucket)
534
+ const params = {
535
+ Bucket: bucket,
536
+ Key: path,
537
+ }
538
+
539
+ try {
540
+ return await client.headObject(params).promise()
541
+ } catch (err: any) {
542
+ throw new Error("Unable to retrieve metadata from object")
543
+ }
544
+ }
545
+
524
546
  /*
525
547
  Given a signed url like '/files/signed/tmp-files-attachments/app_123456/myfile.txt' extract
526
548
  the bucket and the path from it
@@ -530,7 +552,9 @@ export function extractBucketAndPath(
530
552
  ): { bucket: string; path: string } | null {
531
553
  const baseUrl = url.split("?")[0]
532
554
 
533
- const regex = new RegExp(`^${signedFilePrefix}/(?<bucket>[^/]+)/(?<path>.+)$`)
555
+ const regex = new RegExp(
556
+ `^${SIGNED_FILE_PREFIX}/(?<bucket>[^/]+)/(?<path>.+)$`
557
+ )
534
558
  const match = baseUrl.match(regex)
535
559
 
536
560
  if (match && match.groups) {
@@ -1,9 +1,14 @@
1
- import { join } from "path"
1
+ import path, { join } from "path"
2
2
  import { tmpdir } from "os"
3
3
  import fs from "fs"
4
4
  import env from "../environment"
5
5
  import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"
6
-
6
+ import * as objectStore from "./objectStore"
7
+ import {
8
+ AutomationAttachment,
9
+ AutomationAttachmentContent,
10
+ BucketedContent,
11
+ } from "@budibase/types"
7
12
  /****************************************************
8
13
  * NOTE: When adding a new bucket - name *
9
14
  * sure that S3 usages (like budibase-infra) *
@@ -55,3 +60,50 @@ export const bucketTTLConfig = (
55
60
 
56
61
  return params
57
62
  }
63
+
64
+ async function processUrlAttachment(
65
+ attachment: AutomationAttachment
66
+ ): Promise<AutomationAttachmentContent> {
67
+ const response = await fetch(attachment.url)
68
+ if (!response.ok || !response.body) {
69
+ throw new Error(`Unexpected response ${response.statusText}`)
70
+ }
71
+ const fallbackFilename = path.basename(new URL(attachment.url).pathname)
72
+ return {
73
+ filename: attachment.filename || fallbackFilename,
74
+ content: response.body,
75
+ }
76
+ }
77
+
78
+ export async function processObjectStoreAttachment(
79
+ attachment: AutomationAttachment
80
+ ): Promise<BucketedContent> {
81
+ const result = objectStore.extractBucketAndPath(attachment.url)
82
+
83
+ if (result === null) {
84
+ throw new Error("Invalid signed URL")
85
+ }
86
+
87
+ const { bucket, path: objectPath } = result
88
+ const readStream = await objectStore.getReadStream(bucket, objectPath)
89
+ const fallbackFilename = path.basename(objectPath)
90
+ return {
91
+ bucket,
92
+ path: objectPath,
93
+ filename: attachment.filename || fallbackFilename,
94
+ content: readStream,
95
+ }
96
+ }
97
+
98
+ export async function processAutomationAttachment(
99
+ attachment: AutomationAttachment
100
+ ): Promise<AutomationAttachmentContent | BucketedContent> {
101
+ const isFullyFormedUrl =
102
+ attachment.url?.startsWith("http://") ||
103
+ attachment.url?.startsWith("https://")
104
+ if (isFullyFormedUrl) {
105
+ return await processUrlAttachment(attachment)
106
+ } else {
107
+ return await processObjectStoreAttachment(attachment)
108
+ }
109
+ }