Merge master.
This commit is contained in:
commit
8bfa47b5a3
|
@ -202,6 +202,9 @@ jobs:
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build client library - necessary for component tests
|
||||||
|
run: yarn build:client
|
||||||
|
|
||||||
- name: Set up PostgreSQL 16
|
- name: Set up PostgreSQL 16
|
||||||
if: matrix.datasource == 'postgres'
|
if: matrix.datasource == 'postgres'
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.4.5",
|
"version": "3.4.6",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages",
|
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages",
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||||
"lint:fix": "yarn run lint:fix:eslint && yarn run lint:fix:prettier",
|
"lint:fix": "yarn run lint:fix:eslint && yarn run lint:fix:prettier",
|
||||||
|
"build:client": "lerna run --stream build --scope @budibase/client",
|
||||||
"build:specs": "lerna run --stream specs",
|
"build:specs": "lerna run --stream specs",
|
||||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
export class S3 {
|
||||||
|
headBucket() {
|
||||||
|
return jest.fn().mockReturnThis()
|
||||||
|
}
|
||||||
|
deleteObject() {
|
||||||
|
return jest.fn().mockReturnThis()
|
||||||
|
}
|
||||||
|
deleteObjects() {
|
||||||
|
return jest.fn().mockReturnThis()
|
||||||
|
}
|
||||||
|
createBucket() {
|
||||||
|
return jest.fn().mockReturnThis()
|
||||||
|
}
|
||||||
|
getObject() {
|
||||||
|
return jest.fn().mockReturnThis()
|
||||||
|
}
|
||||||
|
listObject() {
|
||||||
|
return jest.fn().mockReturnThis()
|
||||||
|
}
|
||||||
|
promise() {
|
||||||
|
return jest.fn().mockReturnThis()
|
||||||
|
}
|
||||||
|
catch() {
|
||||||
|
return jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetObjectCommand = jest.fn(inputs => ({ inputs }))
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const getSignedUrl = jest.fn((_, cmd) => {
|
||||||
|
const { inputs } = cmd
|
||||||
|
return `http://s3.example.com/${inputs?.Bucket}/${inputs?.Key}`
|
||||||
|
})
|
|
@ -1,19 +0,0 @@
|
||||||
const mockS3 = {
|
|
||||||
headBucket: jest.fn().mockReturnThis(),
|
|
||||||
deleteObject: jest.fn().mockReturnThis(),
|
|
||||||
deleteObjects: jest.fn().mockReturnThis(),
|
|
||||||
createBucket: jest.fn().mockReturnThis(),
|
|
||||||
getObject: jest.fn().mockReturnThis(),
|
|
||||||
listObject: jest.fn().mockReturnThis(),
|
|
||||||
getSignedUrl: jest.fn((operation: string, params: any) => {
|
|
||||||
return `http://s3.example.com/${params.Bucket}/${params.Key}`
|
|
||||||
}),
|
|
||||||
promise: jest.fn().mockReturnThis(),
|
|
||||||
catch: jest.fn(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const AWS = {
|
|
||||||
S3: jest.fn(() => mockS3),
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AWS
|
|
|
@ -30,6 +30,9 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "3.709.0",
|
||||||
|
"@aws-sdk/lib-storage": "3.709.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "3.709.0",
|
||||||
"@budibase/nano": "10.1.5",
|
"@budibase/nano": "10.1.5",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.11",
|
"@budibase/pouchdb-replication-stream": "1.2.11",
|
||||||
"@budibase/shared-core": "*",
|
"@budibase/shared-core": "*",
|
||||||
|
@ -71,11 +74,13 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
"@shopify/jest-koa-mocks": "5.1.1",
|
"@shopify/jest-koa-mocks": "5.1.1",
|
||||||
|
"@smithy/types": "4.0.0",
|
||||||
"@swc/core": "1.3.71",
|
"@swc/core": "1.3.71",
|
||||||
"@swc/jest": "0.2.27",
|
"@swc/jest": "0.2.27",
|
||||||
"@types/chance": "1.1.3",
|
"@types/chance": "1.1.3",
|
||||||
"@types/cookies": "0.7.8",
|
"@types/cookies": "0.7.8",
|
||||||
"@types/jest": "29.5.5",
|
"@types/jest": "29.5.5",
|
||||||
|
"@types/koa": "2.13.4",
|
||||||
"@types/lodash": "4.14.200",
|
"@types/lodash": "4.14.200",
|
||||||
"@types/node-fetch": "2.6.4",
|
"@types/node-fetch": "2.6.4",
|
||||||
"@types/pouchdb": "6.4.2",
|
"@types/pouchdb": "6.4.2",
|
||||||
|
@ -83,7 +88,6 @@
|
||||||
"@types/semver": "7.3.7",
|
"@types/semver": "7.3.7",
|
||||||
"@types/tar-fs": "2.0.1",
|
"@types/tar-fs": "2.0.1",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@types/koa": "2.13.4",
|
|
||||||
"chance": "1.1.8",
|
"chance": "1.1.8",
|
||||||
"ioredis-mock": "8.9.0",
|
"ioredis-mock": "8.9.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
|
|
|
@ -8,6 +8,10 @@ import {
|
||||||
import { getProdAppID } from "./conversions"
|
import { getProdAppID } from "./conversions"
|
||||||
import { DatabaseQueryOpts, VirtualDocumentType } from "@budibase/types"
|
import { DatabaseQueryOpts, VirtualDocumentType } from "@budibase/types"
|
||||||
|
|
||||||
|
const EXTERNAL_TABLE_ID_REGEX = new RegExp(
|
||||||
|
`^${DocumentType.DATASOURCE_PLUS}_(.+)__(.+)$`
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||||
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
||||||
|
@ -64,6 +68,11 @@ export function getQueryIndex(viewName: ViewName) {
|
||||||
return `database/${viewName}`
|
return `database/${viewName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isExternalTableId = (id: string): boolean => {
|
||||||
|
const matches = id.match(EXTERNAL_TABLE_ID_REGEX)
|
||||||
|
return !!id && matches !== null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a given ID is that of a table.
|
* Check if a given ID is that of a table.
|
||||||
*/
|
*/
|
||||||
|
@ -72,7 +81,7 @@ export const isTableId = (id: string): boolean => {
|
||||||
return (
|
return (
|
||||||
!!id &&
|
!!id &&
|
||||||
(id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) ||
|
(id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) ||
|
||||||
id.startsWith(`${DocumentType.DATASOURCE_PLUS}${SEPARATOR}`))
|
isExternalTableId(id))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,7 @@ const environment = {
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN,
|
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN,
|
||||||
AWS_REGION: process.env.AWS_REGION,
|
AWS_REGION: process.env.AWS_REGION || "eu-west-1",
|
||||||
MINIO_URL: process.env.MINIO_URL,
|
MINIO_URL: process.env.MINIO_URL,
|
||||||
MINIO_ENABLED: process.env.MINIO_ENABLED || 1,
|
MINIO_ENABLED: process.env.MINIO_ENABLED || 1,
|
||||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
|
|
|
@ -13,7 +13,7 @@ export function clientLibraryPath(appId: string) {
|
||||||
* due to issues with the domain we were unable to continue doing this - keeping
|
* due to issues with the domain we were unable to continue doing this - keeping
|
||||||
* incase we are able to switch back to CDN path again in future.
|
* incase we are able to switch back to CDN path again in future.
|
||||||
*/
|
*/
|
||||||
export function clientLibraryCDNUrl(appId: string, version: string) {
|
export async function clientLibraryCDNUrl(appId: string, version: string) {
|
||||||
let file = clientLibraryPath(appId)
|
let file = clientLibraryPath(appId)
|
||||||
if (env.CLOUDFRONT_CDN) {
|
if (env.CLOUDFRONT_CDN) {
|
||||||
// append app version to bust the cache
|
// append app version to bust the cache
|
||||||
|
@ -24,7 +24,7 @@ export function clientLibraryCDNUrl(appId: string, version: string) {
|
||||||
// file is public
|
// file is public
|
||||||
return cloudfront.getUrl(file)
|
return cloudfront.getUrl(file)
|
||||||
} else {
|
} else {
|
||||||
return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, file)
|
return await objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,10 +44,10 @@ export function clientLibraryUrl(appId: string, version: string) {
|
||||||
return `/api/assets/client?${qs.encode(qsParams)}`
|
return `/api/assets/client?${qs.encode(qsParams)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAppFileUrl(s3Key: string) {
|
export async function getAppFileUrl(s3Key: string) {
|
||||||
if (env.CLOUDFRONT_CDN) {
|
if (env.CLOUDFRONT_CDN) {
|
||||||
return cloudfront.getPresignedUrl(s3Key)
|
return cloudfront.getPresignedUrl(s3Key)
|
||||||
} else {
|
} else {
|
||||||
return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, s3Key)
|
return await objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, s3Key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,11 @@ import * as cloudfront from "../cloudfront"
|
||||||
|
|
||||||
// URLs
|
// URLs
|
||||||
|
|
||||||
export const getGlobalFileUrl = (type: string, name: string, etag?: string) => {
|
export const getGlobalFileUrl = async (
|
||||||
|
type: string,
|
||||||
|
name: string,
|
||||||
|
etag?: string
|
||||||
|
) => {
|
||||||
let file = getGlobalFileS3Key(type, name)
|
let file = getGlobalFileS3Key(type, name)
|
||||||
if (env.CLOUDFRONT_CDN) {
|
if (env.CLOUDFRONT_CDN) {
|
||||||
if (etag) {
|
if (etag) {
|
||||||
|
@ -13,7 +17,7 @@ export const getGlobalFileUrl = (type: string, name: string, etag?: string) => {
|
||||||
}
|
}
|
||||||
return cloudfront.getPresignedUrl(file)
|
return cloudfront.getPresignedUrl(file)
|
||||||
} else {
|
} else {
|
||||||
return objectStore.getPresignedUrl(env.GLOBAL_BUCKET_NAME, file)
|
return await objectStore.getPresignedUrl(env.GLOBAL_BUCKET_NAME, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,23 +6,25 @@ import { Plugin } from "@budibase/types"
|
||||||
|
|
||||||
// URLS
|
// URLS
|
||||||
|
|
||||||
export function enrichPluginURLs(plugins?: Plugin[]): Plugin[] {
|
export async function enrichPluginURLs(plugins?: Plugin[]): Promise<Plugin[]> {
|
||||||
if (!plugins || !plugins.length) {
|
if (!plugins || !plugins.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return plugins.map(plugin => {
|
return await Promise.all(
|
||||||
const jsUrl = getPluginJSUrl(plugin)
|
plugins.map(async plugin => {
|
||||||
const iconUrl = getPluginIconUrl(plugin)
|
const jsUrl = await getPluginJSUrl(plugin)
|
||||||
|
const iconUrl = await getPluginIconUrl(plugin)
|
||||||
return { ...plugin, jsUrl, iconUrl }
|
return { ...plugin, jsUrl, iconUrl }
|
||||||
})
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginJSUrl(plugin: Plugin) {
|
async function getPluginJSUrl(plugin: Plugin) {
|
||||||
const s3Key = getPluginJSKey(plugin)
|
const s3Key = getPluginJSKey(plugin)
|
||||||
return getPluginUrl(s3Key)
|
return getPluginUrl(s3Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginIconUrl(plugin: Plugin): string | undefined {
|
async function getPluginIconUrl(plugin: Plugin) {
|
||||||
const s3Key = getPluginIconKey(plugin)
|
const s3Key = getPluginIconKey(plugin)
|
||||||
if (!s3Key) {
|
if (!s3Key) {
|
||||||
return
|
return
|
||||||
|
@ -30,11 +32,11 @@ function getPluginIconUrl(plugin: Plugin): string | undefined {
|
||||||
return getPluginUrl(s3Key)
|
return getPluginUrl(s3Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginUrl(s3Key: string) {
|
async function getPluginUrl(s3Key: string) {
|
||||||
if (env.CLOUDFRONT_CDN) {
|
if (env.CLOUDFRONT_CDN) {
|
||||||
return cloudfront.getPresignedUrl(s3Key)
|
return cloudfront.getPresignedUrl(s3Key)
|
||||||
} else {
|
} else {
|
||||||
return objectStore.getPresignedUrl(env.PLUGIN_BUCKET_NAME, s3Key)
|
return await objectStore.getPresignedUrl(env.PLUGIN_BUCKET_NAME, s3Key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,25 +93,25 @@ describe("app", () => {
|
||||||
testEnv.multiTenant()
|
testEnv.multiTenant()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with embedded minio", () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with custom S3", () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
url.includes("http://cf.example.com/app_123/attachments/image.jpeg?")
|
url.includes("http://cf.example.com/app_123/attachments/image.jpeg?")
|
||||||
|
@ -126,8 +126,8 @@ describe("app", () => {
|
||||||
|
|
||||||
it("gets url with embedded minio", async () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
await testEnv.withTenant(() => {
|
await testEnv.withTenant(async () => {
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
||||||
)
|
)
|
||||||
|
@ -136,8 +136,8 @@ describe("app", () => {
|
||||||
|
|
||||||
it("gets url with custom S3", async () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
await testEnv.withTenant(() => {
|
await testEnv.withTenant(async () => {
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
||||||
)
|
)
|
||||||
|
@ -146,8 +146,8 @@ describe("app", () => {
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", async () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
await testEnv.withTenant(() => {
|
await testEnv.withTenant(async () => {
|
||||||
const url = getAppFileUrl()
|
const url = await getAppFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
url.includes(
|
url.includes(
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { testEnv } from "../../../../tests/extra"
|
||||||
|
|
||||||
describe("global", () => {
|
describe("global", () => {
|
||||||
describe("getGlobalFileUrl", () => {
|
describe("getGlobalFileUrl", () => {
|
||||||
function getGlobalFileUrl() {
|
async function getGlobalFileUrl() {
|
||||||
return global.getGlobalFileUrl("settings", "logoUrl", "etag")
|
return global.getGlobalFileUrl("settings", "logoUrl", "etag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,21 +12,21 @@ describe("global", () => {
|
||||||
testEnv.singleTenant()
|
testEnv.singleTenant()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with embedded minio", () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
expect(url).toBe("/files/signed/global/settings/logoUrl")
|
expect(url).toBe("/files/signed/global/settings/logoUrl")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with custom S3", () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
expect(url).toBe("http://s3.example.com/global/settings/logoUrl")
|
expect(url).toBe("http://s3.example.com/global/settings/logoUrl")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
url.includes("http://cf.example.com/settings/logoUrl?etag=etag&")
|
url.includes("http://cf.example.com/settings/logoUrl?etag=etag&")
|
||||||
|
@ -41,16 +41,16 @@ describe("global", () => {
|
||||||
|
|
||||||
it("gets url with embedded minio", async () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
expect(url).toBe(`/files/signed/global/${tenantId}/settings/logoUrl`)
|
expect(url).toBe(`/files/signed/global/${tenantId}/settings/logoUrl`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with custom S3", async () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
`http://s3.example.com/global/${tenantId}/settings/logoUrl`
|
`http://s3.example.com/global/${tenantId}/settings/logoUrl`
|
||||||
)
|
)
|
||||||
|
@ -59,8 +59,8 @@ describe("global", () => {
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", async () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const url = getGlobalFileUrl()
|
const url = await getGlobalFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
url.includes(
|
url.includes(
|
||||||
|
|
|
@ -6,8 +6,8 @@ describe("plugins", () => {
|
||||||
describe("enrichPluginURLs", () => {
|
describe("enrichPluginURLs", () => {
|
||||||
const plugin = structures.plugins.plugin()
|
const plugin = structures.plugins.plugin()
|
||||||
|
|
||||||
function getEnrichedPluginUrls() {
|
async function getEnrichedPluginUrls() {
|
||||||
const enriched = plugins.enrichPluginURLs([plugin])[0]
|
const enriched = (await plugins.enrichPluginURLs([plugin]))[0]
|
||||||
return {
|
return {
|
||||||
jsUrl: enriched.jsUrl!,
|
jsUrl: enriched.jsUrl!,
|
||||||
iconUrl: enriched.iconUrl!,
|
iconUrl: enriched.iconUrl!,
|
||||||
|
@ -19,9 +19,9 @@ describe("plugins", () => {
|
||||||
testEnv.singleTenant()
|
testEnv.singleTenant()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with embedded minio", () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
expect(urls.jsUrl).toBe(
|
expect(urls.jsUrl).toBe(
|
||||||
`/files/signed/plugins/${plugin.name}/plugin.min.js`
|
`/files/signed/plugins/${plugin.name}/plugin.min.js`
|
||||||
)
|
)
|
||||||
|
@ -30,9 +30,9 @@ describe("plugins", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with custom S3", () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
expect(urls.jsUrl).toBe(
|
expect(urls.jsUrl).toBe(
|
||||||
`http://s3.example.com/plugins/${plugin.name}/plugin.min.js`
|
`http://s3.example.com/plugins/${plugin.name}/plugin.min.js`
|
||||||
)
|
)
|
||||||
|
@ -41,9 +41,9 @@ describe("plugins", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
urls.jsUrl.includes(
|
urls.jsUrl.includes(
|
||||||
|
@ -65,8 +65,8 @@ describe("plugins", () => {
|
||||||
|
|
||||||
it("gets url with embedded minio", async () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
expect(urls.jsUrl).toBe(
|
expect(urls.jsUrl).toBe(
|
||||||
`/files/signed/plugins/${tenantId}/${plugin.name}/plugin.min.js`
|
`/files/signed/plugins/${tenantId}/${plugin.name}/plugin.min.js`
|
||||||
)
|
)
|
||||||
|
@ -78,8 +78,8 @@ describe("plugins", () => {
|
||||||
|
|
||||||
it("gets url with custom S3", async () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
expect(urls.jsUrl).toBe(
|
expect(urls.jsUrl).toBe(
|
||||||
`http://s3.example.com/plugins/${tenantId}/${plugin.name}/plugin.min.js`
|
`http://s3.example.com/plugins/${tenantId}/${plugin.name}/plugin.min.js`
|
||||||
)
|
)
|
||||||
|
@ -91,8 +91,8 @@ describe("plugins", () => {
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", async () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(async tenantId => {
|
||||||
const urls = getEnrichedPluginUrls()
|
const urls = await getEnrichedPluginUrls()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
urls.jsUrl.includes(
|
urls.jsUrl.includes(
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
const sanitize = require("sanitize-s3-objectkey")
|
const sanitize = require("sanitize-s3-objectkey")
|
||||||
|
|
||||||
import AWS from "aws-sdk"
|
import {
|
||||||
|
HeadObjectCommandOutput,
|
||||||
|
PutObjectCommandInput,
|
||||||
|
S3,
|
||||||
|
S3ClientConfig,
|
||||||
|
GetObjectCommand,
|
||||||
|
_Object as S3Object,
|
||||||
|
} from "@aws-sdk/client-s3"
|
||||||
|
import { Upload } from "@aws-sdk/lib-storage"
|
||||||
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||||
import stream, { Readable } from "stream"
|
import stream, { Readable } from "stream"
|
||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import tar from "tar-fs"
|
import tar from "tar-fs"
|
||||||
|
@ -13,8 +22,8 @@ import { bucketTTLConfig, budibaseTempDir } from "./utils"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
||||||
import fsp from "fs/promises"
|
import fsp from "fs/promises"
|
||||||
import { HeadObjectOutput } from "aws-sdk/clients/s3"
|
|
||||||
import { ReadableStream } from "stream/web"
|
import { ReadableStream } from "stream/web"
|
||||||
|
import { NodeJsClient } from "@smithy/types"
|
||||||
|
|
||||||
const streamPipeline = promisify(stream.pipeline)
|
const streamPipeline = promisify(stream.pipeline)
|
||||||
// use this as a temporary store of buckets that are being created
|
// use this as a temporary store of buckets that are being created
|
||||||
|
@ -84,26 +93,24 @@ export function sanitizeBucket(input: string) {
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export function ObjectStore(
|
export function ObjectStore(
|
||||||
bucket: string,
|
|
||||||
opts: { presigning: boolean } = { presigning: false }
|
opts: { presigning: boolean } = { presigning: false }
|
||||||
) {
|
) {
|
||||||
const config: AWS.S3.ClientConfiguration = {
|
const config: S3ClientConfig = {
|
||||||
s3ForcePathStyle: true,
|
forcePathStyle: true,
|
||||||
signatureVersion: "v4",
|
credentials: {
|
||||||
apiVersion: "2006-03-01",
|
accessKeyId: env.MINIO_ACCESS_KEY!,
|
||||||
accessKeyId: env.MINIO_ACCESS_KEY,
|
secretAccessKey: env.MINIO_SECRET_KEY!,
|
||||||
secretAccessKey: env.MINIO_SECRET_KEY,
|
},
|
||||||
region: env.AWS_REGION,
|
region: env.AWS_REGION,
|
||||||
}
|
}
|
||||||
if (bucket) {
|
|
||||||
config.params = {
|
|
||||||
Bucket: sanitizeBucket(bucket),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for AWS Credentials using temporary session token
|
// for AWS Credentials using temporary session token
|
||||||
if (!env.MINIO_ENABLED && env.AWS_SESSION_TOKEN) {
|
if (!env.MINIO_ENABLED && env.AWS_SESSION_TOKEN) {
|
||||||
config.sessionToken = env.AWS_SESSION_TOKEN
|
config.credentials = {
|
||||||
|
accessKeyId: env.MINIO_ACCESS_KEY!,
|
||||||
|
secretAccessKey: env.MINIO_SECRET_KEY!,
|
||||||
|
sessionToken: env.AWS_SESSION_TOKEN,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// custom S3 is in use i.e. minio
|
// custom S3 is in use i.e. minio
|
||||||
|
@ -113,13 +120,13 @@ export function ObjectStore(
|
||||||
// Normally a signed url will need to be generated with a specified host in mind.
|
// Normally a signed url will need to be generated with a specified host in mind.
|
||||||
// To support dynamic hosts, e.g. some unknown self-hosted installation url,
|
// To support dynamic hosts, e.g. some unknown self-hosted installation url,
|
||||||
// use a predefined host. The host 'minio-service' is also forwarded to minio requests via nginx
|
// use a predefined host. The host 'minio-service' is also forwarded to minio requests via nginx
|
||||||
config.endpoint = "minio-service"
|
config.endpoint = "http://minio-service"
|
||||||
} else {
|
} else {
|
||||||
config.endpoint = env.MINIO_URL
|
config.endpoint = env.MINIO_URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AWS.S3(config)
|
return new S3(config) as NodeJsClient<S3>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,26 +139,25 @@ export async function createBucketIfNotExists(
|
||||||
): Promise<{ created: boolean; exists: boolean }> {
|
): Promise<{ created: boolean; exists: boolean }> {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
try {
|
try {
|
||||||
await client
|
await client.headBucket({
|
||||||
.headBucket({
|
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
return { created: false, exists: true }
|
return { created: false, exists: true }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const promises: any = STATE.bucketCreationPromises
|
const statusCode = err.statusCode || err.$response?.statusCode
|
||||||
const doesntExist = err.statusCode === 404,
|
const promises: Record<string, Promise<any> | undefined> =
|
||||||
noAccess = err.statusCode === 403
|
STATE.bucketCreationPromises
|
||||||
|
const doesntExist = statusCode === 404,
|
||||||
|
noAccess = statusCode === 403
|
||||||
if (promises[bucketName]) {
|
if (promises[bucketName]) {
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
return { created: false, exists: true }
|
return { created: false, exists: true }
|
||||||
} else if (doesntExist || noAccess) {
|
} else if (doesntExist || noAccess) {
|
||||||
if (doesntExist) {
|
if (doesntExist) {
|
||||||
promises[bucketName] = client
|
promises[bucketName] = client.createBucket({
|
||||||
.createBucket({
|
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
delete promises[bucketName]
|
delete promises[bucketName]
|
||||||
return { created: true, exists: false }
|
return { created: true, exists: false }
|
||||||
|
@ -180,25 +186,26 @@ export async function upload({
|
||||||
|
|
||||||
const fileBytes = path ? (await fsp.open(path)).createReadStream() : body
|
const fileBytes = path ? (await fsp.open(path)).createReadStream() : body
|
||||||
|
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore()
|
||||||
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
|
||||||
if (ttl && bucketCreated.created) {
|
if (ttl && bucketCreated.created) {
|
||||||
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||||
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
await objectStore.putBucketLifecycleConfiguration(ttlConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentType = type
|
let contentType = type
|
||||||
if (!contentType) {
|
const finalContentType = contentType
|
||||||
contentType = extension
|
? contentType
|
||||||
|
: extension
|
||||||
? CONTENT_TYPE_MAP[extension.toLowerCase()]
|
? CONTENT_TYPE_MAP[extension.toLowerCase()]
|
||||||
: CONTENT_TYPE_MAP.txt
|
: CONTENT_TYPE_MAP.txt
|
||||||
}
|
const config: PutObjectCommandInput = {
|
||||||
const config: any = {
|
|
||||||
// windows file paths need to be converted to forward slashes for s3
|
// windows file paths need to be converted to forward slashes for s3
|
||||||
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Key: sanitizeKey(filename),
|
Key: sanitizeKey(filename),
|
||||||
Body: fileBytes,
|
Body: fileBytes as stream.Readable | Buffer,
|
||||||
ContentType: contentType,
|
ContentType: finalContentType,
|
||||||
}
|
}
|
||||||
if (metadata && typeof metadata === "object") {
|
if (metadata && typeof metadata === "object") {
|
||||||
// remove any nullish keys from the metadata object, as these may be considered invalid
|
// remove any nullish keys from the metadata object, as these may be considered invalid
|
||||||
|
@ -207,10 +214,15 @@ export async function upload({
|
||||||
delete metadata[key]
|
delete metadata[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.Metadata = metadata
|
config.Metadata = metadata as Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectStore.upload(config).promise()
|
const upload = new Upload({
|
||||||
|
client: objectStore,
|
||||||
|
params: config,
|
||||||
|
})
|
||||||
|
|
||||||
|
return upload.done()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -229,12 +241,12 @@ export async function streamUpload({
|
||||||
throw new Error("Stream to upload is invalid/undefined")
|
throw new Error("Stream to upload is invalid/undefined")
|
||||||
}
|
}
|
||||||
const extension = filename.split(".").pop()
|
const extension = filename.split(".").pop()
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore()
|
||||||
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
|
||||||
if (ttl && bucketCreated.created) {
|
if (ttl && bucketCreated.created) {
|
||||||
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||||
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
await objectStore.putBucketLifecycleConfiguration(ttlConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set content type for certain known extensions
|
// Set content type for certain known extensions
|
||||||
|
@ -267,13 +279,15 @@ export async function streamUpload({
|
||||||
...extra,
|
...extra,
|
||||||
}
|
}
|
||||||
|
|
||||||
const details = await objectStore.upload(params).promise()
|
const upload = new Upload({
|
||||||
const headDetails = await objectStore
|
client: objectStore,
|
||||||
.headObject({
|
params,
|
||||||
|
})
|
||||||
|
const details = await upload.done()
|
||||||
|
const headDetails = await objectStore.headObject({
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: objKey,
|
Key: objKey,
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
return {
|
return {
|
||||||
...details,
|
...details,
|
||||||
ContentLength: headDetails.ContentLength,
|
ContentLength: headDetails.ContentLength,
|
||||||
|
@ -284,35 +298,46 @@ export async function streamUpload({
|
||||||
* retrieves the contents of a file from the object store, if it is a known content type it
|
* retrieves the contents of a file from the object store, if it is a known content type it
|
||||||
* will be converted, otherwise it will be returned as a buffer stream.
|
* will be converted, otherwise it will be returned as a buffer stream.
|
||||||
*/
|
*/
|
||||||
export async function retrieve(bucketName: string, filepath: string) {
|
export async function retrieve(
|
||||||
const objectStore = ObjectStore(bucketName)
|
bucketName: string,
|
||||||
|
filepath: string
|
||||||
|
): Promise<string | stream.Readable> {
|
||||||
|
const objectStore = ObjectStore()
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Key: sanitizeKey(filepath),
|
Key: sanitizeKey(filepath),
|
||||||
}
|
}
|
||||||
const response: any = await objectStore.getObject(params).promise()
|
const response = await objectStore.getObject(params)
|
||||||
// currently these are all strings
|
if (!response.Body) {
|
||||||
|
throw new Error("Unable to retrieve object")
|
||||||
|
}
|
||||||
if (STRING_CONTENT_TYPES.includes(response.ContentType)) {
|
if (STRING_CONTENT_TYPES.includes(response.ContentType)) {
|
||||||
return response.Body.toString("utf8")
|
return response.Body.transformToString()
|
||||||
} else {
|
} else {
|
||||||
return response.Body
|
// this typecast is required - for some reason the AWS SDK V3 defines its own "ReadableStream"
|
||||||
|
// found in the @aws-sdk/types package which is meant to be the Node type, but due to the SDK
|
||||||
|
// supporting both the browser and Nodejs it is a polyfill which causes a type clash with Node.
|
||||||
|
const readableStream =
|
||||||
|
response.Body.transformToWebStream() as ReadableStream
|
||||||
|
return stream.Readable.fromWeb(readableStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listAllObjects(bucketName: string, path: string) {
|
export async function listAllObjects(
|
||||||
const objectStore = ObjectStore(bucketName)
|
bucketName: string,
|
||||||
|
path: string
|
||||||
|
): Promise<S3Object[]> {
|
||||||
|
const objectStore = ObjectStore()
|
||||||
const list = (params: ListParams = {}) => {
|
const list = (params: ListParams = {}) => {
|
||||||
return objectStore
|
return objectStore.listObjectsV2({
|
||||||
.listObjectsV2({
|
|
||||||
...params,
|
...params,
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Prefix: sanitizeKey(path),
|
Prefix: sanitizeKey(path),
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
}
|
}
|
||||||
let isTruncated = false,
|
let isTruncated = false,
|
||||||
token,
|
token,
|
||||||
objects: AWS.S3.Types.Object[] = []
|
objects: Object[] = []
|
||||||
do {
|
do {
|
||||||
let params: ListParams = {}
|
let params: ListParams = {}
|
||||||
if (token) {
|
if (token) {
|
||||||
|
@ -331,18 +356,19 @@ export async function listAllObjects(bucketName: string, path: string) {
|
||||||
/**
|
/**
|
||||||
* Generate a presigned url with a default TTL of 1 hour
|
* Generate a presigned url with a default TTL of 1 hour
|
||||||
*/
|
*/
|
||||||
export function getPresignedUrl(
|
export async function getPresignedUrl(
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
key: string,
|
key: string,
|
||||||
durationSeconds = 3600
|
durationSeconds = 3600
|
||||||
) {
|
) {
|
||||||
const objectStore = ObjectStore(bucketName, { presigning: true })
|
const objectStore = ObjectStore({ presigning: true })
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Key: sanitizeKey(key),
|
Key: sanitizeKey(key),
|
||||||
Expires: durationSeconds,
|
|
||||||
}
|
}
|
||||||
const url = objectStore.getSignedUrl("getObject", params)
|
const url = await getSignedUrl(objectStore, new GetObjectCommand(params), {
|
||||||
|
expiresIn: durationSeconds,
|
||||||
|
})
|
||||||
|
|
||||||
if (!env.MINIO_ENABLED) {
|
if (!env.MINIO_ENABLED) {
|
||||||
// return the full URL to the client
|
// return the full URL to the client
|
||||||
|
@ -366,7 +392,11 @@ export async function retrieveToTmp(bucketName: string, filepath: string) {
|
||||||
filepath = sanitizeKey(filepath)
|
filepath = sanitizeKey(filepath)
|
||||||
const data = await retrieve(bucketName, filepath)
|
const data = await retrieve(bucketName, filepath)
|
||||||
const outputPath = join(budibaseTempDir(), v4())
|
const outputPath = join(budibaseTempDir(), v4())
|
||||||
|
if (data instanceof stream.Readable) {
|
||||||
|
data.pipe(fs.createWriteStream(outputPath))
|
||||||
|
} else {
|
||||||
fs.writeFileSync(outputPath, data)
|
fs.writeFileSync(outputPath, data)
|
||||||
|
}
|
||||||
return outputPath
|
return outputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,17 +438,17 @@ export async function retrieveDirectory(bucketName: string, path: string) {
|
||||||
* Delete a single file.
|
* Delete a single file.
|
||||||
*/
|
*/
|
||||||
export async function deleteFile(bucketName: string, filepath: string) {
|
export async function deleteFile(bucketName: string, filepath: string) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore()
|
||||||
await createBucketIfNotExists(objectStore, bucketName)
|
await createBucketIfNotExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Key: sanitizeKey(filepath),
|
Key: sanitizeKey(filepath),
|
||||||
}
|
}
|
||||||
return objectStore.deleteObject(params).promise()
|
return objectStore.deleteObject(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore()
|
||||||
await createBucketIfNotExists(objectStore, bucketName)
|
await createBucketIfNotExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
|
@ -426,7 +456,7 @@ export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
||||||
Objects: filepaths.map((path: any) => ({ Key: sanitizeKey(path) })),
|
Objects: filepaths.map((path: any) => ({ Key: sanitizeKey(path) })),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return objectStore.deleteObjects(params).promise()
|
return objectStore.deleteObjects(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -438,13 +468,13 @@ export async function deleteFolder(
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
folder = sanitizeKey(folder)
|
folder = sanitizeKey(folder)
|
||||||
const client = ObjectStore(bucketName)
|
const client = ObjectStore()
|
||||||
const listParams = {
|
const listParams = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Prefix: folder,
|
Prefix: folder,
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingObjectsResponse = await client.listObjects(listParams).promise()
|
const existingObjectsResponse = await client.listObjects(listParams)
|
||||||
if (existingObjectsResponse.Contents?.length === 0) {
|
if (existingObjectsResponse.Contents?.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -459,7 +489,7 @@ export async function deleteFolder(
|
||||||
deleteParams.Delete.Objects.push({ Key: content.Key })
|
deleteParams.Delete.Objects.push({ Key: content.Key })
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteResponse = await client.deleteObjects(deleteParams).promise()
|
const deleteResponse = await client.deleteObjects(deleteParams)
|
||||||
// can only empty 1000 items at once
|
// can only empty 1000 items at once
|
||||||
if (deleteResponse.Deleted?.length === 1000) {
|
if (deleteResponse.Deleted?.length === 1000) {
|
||||||
return deleteFolder(bucketName, folder)
|
return deleteFolder(bucketName, folder)
|
||||||
|
@ -534,29 +564,33 @@ export async function getReadStream(
|
||||||
): Promise<Readable> {
|
): Promise<Readable> {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
path = sanitizeKey(path)
|
path = sanitizeKey(path)
|
||||||
const client = ObjectStore(bucketName)
|
const client = ObjectStore()
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Key: path,
|
Key: path,
|
||||||
}
|
}
|
||||||
return client.getObject(params).createReadStream()
|
const response = await client.getObject(params)
|
||||||
|
if (!response.Body || !(response.Body instanceof stream.Readable)) {
|
||||||
|
throw new Error("Unable to retrieve stream - invalid response")
|
||||||
|
}
|
||||||
|
return response.Body
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getObjectMetadata(
|
export async function getObjectMetadata(
|
||||||
bucket: string,
|
bucket: string,
|
||||||
path: string
|
path: string
|
||||||
): Promise<HeadObjectOutput> {
|
): Promise<HeadObjectCommandOutput> {
|
||||||
bucket = sanitizeBucket(bucket)
|
bucket = sanitizeBucket(bucket)
|
||||||
path = sanitizeKey(path)
|
path = sanitizeKey(path)
|
||||||
|
|
||||||
const client = ObjectStore(bucket)
|
const client = ObjectStore()
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: path,
|
Key: path,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await client.headObject(params).promise()
|
return await client.headObject(params)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw new Error("Unable to retrieve metadata from object")
|
throw new Error("Unable to retrieve metadata from object")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,10 @@ import path, { join } from "path"
|
||||||
import { tmpdir } from "os"
|
import { tmpdir } from "os"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"
|
import {
|
||||||
|
LifecycleRule,
|
||||||
|
PutBucketLifecycleConfigurationCommandInput,
|
||||||
|
} from "@aws-sdk/client-s3"
|
||||||
import * as objectStore from "./objectStore"
|
import * as objectStore from "./objectStore"
|
||||||
import {
|
import {
|
||||||
AutomationAttachment,
|
AutomationAttachment,
|
||||||
|
@ -43,8 +46,8 @@ export function budibaseTempDir() {
|
||||||
export const bucketTTLConfig = (
|
export const bucketTTLConfig = (
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
days: number
|
days: number
|
||||||
): PutBucketLifecycleConfigurationRequest => {
|
): PutBucketLifecycleConfigurationCommandInput => {
|
||||||
const lifecycleRule = {
|
const lifecycleRule: LifecycleRule = {
|
||||||
ID: `${bucketName}-ExpireAfter${days}days`,
|
ID: `${bucketName}-ExpireAfter${days}days`,
|
||||||
Prefix: "",
|
Prefix: "",
|
||||||
Status: "Enabled",
|
Status: "Enabled",
|
||||||
|
|
|
@ -93,7 +93,10 @@ const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
|
||||||
// Handle iframe clicks by detecting a loss of focus on the main window
|
// Handle iframe clicks by detecting a loss of focus on the main window
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
if (document.activeElement?.tagName === "IFRAME") {
|
if (
|
||||||
|
document.activeElement &&
|
||||||
|
["IFRAME", "BODY"].includes(document.activeElement.tagName)
|
||||||
|
) {
|
||||||
handleClick(
|
handleClick(
|
||||||
new MouseEvent("click", { relatedTarget: document.activeElement })
|
new MouseEvent("click", { relatedTarget: document.activeElement })
|
||||||
)
|
)
|
||||||
|
|
|
@ -151,6 +151,8 @@
|
||||||
const screenCount = affectedScreens.length
|
const screenCount = affectedScreens.length
|
||||||
let message = `Removing ${source?.name} `
|
let message = `Removing ${source?.name} `
|
||||||
let initialLength = message.length
|
let initialLength = message.length
|
||||||
|
const hasChanged = () => message.length !== initialLength
|
||||||
|
|
||||||
if (sourceType === SourceType.TABLE) {
|
if (sourceType === SourceType.TABLE) {
|
||||||
const views = "views" in source ? Object.values(source?.views ?? []) : []
|
const views = "views" in source ? Object.values(source?.views ?? []) : []
|
||||||
message += `will delete its data${
|
message += `will delete its data${
|
||||||
|
@ -169,10 +171,10 @@
|
||||||
initialLength !== message.length
|
initialLength !== message.length
|
||||||
? ", and break connected screens:"
|
? ", and break connected screens:"
|
||||||
: "will break connected screens:"
|
: "will break connected screens:"
|
||||||
} else {
|
} else if (hasChanged()) {
|
||||||
message += "."
|
message += "."
|
||||||
}
|
}
|
||||||
return message.length !== initialLength ? message : ""
|
return hasChanged() ? message : ""
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import fs from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { TEMP_DIR, MINIO_DIR } from "./utils"
|
import { TEMP_DIR, MINIO_DIR } from "./utils"
|
||||||
import { progressBar } from "../utils"
|
import { progressBar } from "../utils"
|
||||||
|
import * as stream from "node:stream"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ObjectStoreBuckets,
|
ObjectStoreBuckets,
|
||||||
|
@ -20,15 +21,21 @@ export async function exportObjects() {
|
||||||
let fullList: any[] = []
|
let fullList: any[] = []
|
||||||
let errorCount = 0
|
let errorCount = 0
|
||||||
for (let bucket of bucketList) {
|
for (let bucket of bucketList) {
|
||||||
const client = ObjectStore(bucket)
|
const client = ObjectStore()
|
||||||
try {
|
try {
|
||||||
await client.headBucket().promise()
|
await client.headBucket({
|
||||||
|
Bucket: bucket,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorCount++
|
errorCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const list = (await client.listObjectsV2().promise()) as { Contents: any[] }
|
const list = await client.listObjectsV2({
|
||||||
fullList = fullList.concat(list.Contents.map(el => ({ ...el, bucket })))
|
Bucket: bucket,
|
||||||
|
})
|
||||||
|
fullList = fullList.concat(
|
||||||
|
list.Contents?.map(el => ({ ...el, bucket })) || []
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (errorCount === bucketList.length) {
|
if (errorCount === bucketList.length) {
|
||||||
throw new Error("Unable to access MinIO/S3 - check environment config.")
|
throw new Error("Unable to access MinIO/S3 - check environment config.")
|
||||||
|
@ -43,7 +50,13 @@ export async function exportObjects() {
|
||||||
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
||||||
fs.mkdirSync(join(path, object.bucket, ...dirs), { recursive: true })
|
fs.mkdirSync(join(path, object.bucket, ...dirs), { recursive: true })
|
||||||
}
|
}
|
||||||
|
if (data instanceof stream.Readable) {
|
||||||
|
data.pipe(
|
||||||
|
fs.createWriteStream(join(path, object.bucket, ...possiblePath))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
fs.writeFileSync(join(path, object.bucket, ...possiblePath), data)
|
fs.writeFileSync(join(path, object.bucket, ...possiblePath), data)
|
||||||
|
}
|
||||||
bar.update(++count)
|
bar.update(++count)
|
||||||
}
|
}
|
||||||
bar.stop()
|
bar.stop()
|
||||||
|
@ -60,7 +73,7 @@ export async function importObjects() {
|
||||||
const bar = progressBar(total)
|
const bar = progressBar(total)
|
||||||
let count = 0
|
let count = 0
|
||||||
for (let bucket of buckets) {
|
for (let bucket of buckets) {
|
||||||
const client = ObjectStore(bucket)
|
const client = ObjectStore()
|
||||||
await createBucketIfNotExists(client, bucket)
|
await createBucketIfNotExists(client, bucket)
|
||||||
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
||||||
count += files.length
|
count += files.length
|
||||||
|
|
|
@ -50,6 +50,10 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
|
"@aws-sdk/client-dynamodb": "3.709.0",
|
||||||
|
"@aws-sdk/client-s3": "3.709.0",
|
||||||
|
"@aws-sdk/lib-dynamodb": "3.709.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "3.709.0",
|
||||||
"@azure/msal-node": "^2.5.1",
|
"@azure/msal-node": "^2.5.1",
|
||||||
"@budibase/backend-core": "*",
|
"@budibase/backend-core": "*",
|
||||||
"@budibase/client": "*",
|
"@budibase/client": "*",
|
||||||
|
@ -70,7 +74,6 @@
|
||||||
"airtable": "0.12.2",
|
"airtable": "0.12.2",
|
||||||
"arangojs": "7.2.0",
|
"arangojs": "7.2.0",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"aws-sdk": "2.1692.0",
|
|
||||||
"bcrypt": "5.1.0",
|
"bcrypt": "5.1.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"bson": "^6.9.0",
|
"bson": "^6.9.0",
|
||||||
|
|
|
@ -6,8 +6,8 @@ import {
|
||||||
} from "../../db/views/staticViews"
|
} from "../../db/views/staticViews"
|
||||||
import {
|
import {
|
||||||
backupClientLibrary,
|
backupClientLibrary,
|
||||||
createApp,
|
uploadAppFiles,
|
||||||
deleteApp,
|
deleteAppFiles,
|
||||||
revertClientLibrary,
|
revertClientLibrary,
|
||||||
updateClientLibrary,
|
updateClientLibrary,
|
||||||
} from "../../utilities/fileSystem"
|
} from "../../utilities/fileSystem"
|
||||||
|
@ -228,7 +228,7 @@ export async function fetchAppPackage(
|
||||||
const license = await licensing.cache.getCachedLicense()
|
const license = await licensing.cache.getCachedLicense()
|
||||||
|
|
||||||
// Enrich plugin URLs
|
// Enrich plugin URLs
|
||||||
application.usedPlugins = objectStore.enrichPluginURLs(
|
application.usedPlugins = await objectStore.enrichPluginURLs(
|
||||||
application.usedPlugins
|
application.usedPlugins
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -375,9 +375,8 @@ async function performAppCreate(
|
||||||
const response = await db.put(newApplication, { force: true })
|
const response = await db.put(newApplication, { force: true })
|
||||||
newApplication._rev = response.rev
|
newApplication._rev = response.rev
|
||||||
|
|
||||||
/* istanbul ignore next */
|
if (!env.USE_LOCAL_COMPONENT_LIBS) {
|
||||||
if (!env.isTest()) {
|
await uploadAppFiles(appId)
|
||||||
await createApp(appId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestMigrationId = appMigrations.getLatestEnabledMigrationId()
|
const latestMigrationId = appMigrations.getLatestEnabledMigrationId()
|
||||||
|
@ -656,7 +655,7 @@ async function destroyApp(ctx: UserCtx) {
|
||||||
await events.app.deleted(app)
|
await events.app.deleted(app)
|
||||||
|
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
await deleteApp(appId)
|
await deleteAppFiles(appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
await removeAppFromUserRoles(ctx, appId)
|
await removeAppFromUserRoles(ctx, appId)
|
||||||
|
|
|
@ -18,7 +18,8 @@ import {
|
||||||
objectStore,
|
objectStore,
|
||||||
utils,
|
utils,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import AWS from "aws-sdk"
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||||
|
import { PutObjectCommand, S3 } from "@aws-sdk/client-s3"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as pro from "@budibase/pro"
|
import * as pro from "@budibase/pro"
|
||||||
|
@ -128,9 +129,9 @@ export const uploadFile = async function (
|
||||||
return {
|
return {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
url: objectStore.getAppFileUrl(s3Key),
|
url: await objectStore.getAppFileUrl(s3Key),
|
||||||
extension,
|
extension,
|
||||||
key: response.Key,
|
key: response.Key!,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -210,11 +211,11 @@ export const serveApp = async function (ctx: UserCtx<void, ServeAppResponse>) {
|
||||||
usedPlugins: plugins,
|
usedPlugins: plugins,
|
||||||
favicon:
|
favicon:
|
||||||
branding.faviconUrl !== ""
|
branding.faviconUrl !== ""
|
||||||
? objectStore.getGlobalFileUrl("settings", "faviconUrl")
|
? await objectStore.getGlobalFileUrl("settings", "faviconUrl")
|
||||||
: "",
|
: "",
|
||||||
logo:
|
logo:
|
||||||
config?.logoUrl !== ""
|
config?.logoUrl !== ""
|
||||||
? objectStore.getGlobalFileUrl("settings", "logoUrl")
|
? await objectStore.getGlobalFileUrl("settings", "logoUrl")
|
||||||
: "",
|
: "",
|
||||||
appMigrating: needMigrations,
|
appMigrating: needMigrations,
|
||||||
nonce: ctx.state.nonce,
|
nonce: ctx.state.nonce,
|
||||||
|
@ -243,7 +244,7 @@ export const serveApp = async function (ctx: UserCtx<void, ServeAppResponse>) {
|
||||||
metaDescription: branding?.metaDescription || "",
|
metaDescription: branding?.metaDescription || "",
|
||||||
favicon:
|
favicon:
|
||||||
branding.faviconUrl !== ""
|
branding.faviconUrl !== ""
|
||||||
? objectStore.getGlobalFileUrl("settings", "faviconUrl")
|
? await objectStore.getGlobalFileUrl("settings", "faviconUrl")
|
||||||
: "",
|
: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -334,16 +335,17 @@ export const getSignedUploadURL = async function (
|
||||||
ctx.throw(400, "bucket and key values are required")
|
ctx.throw(400, "bucket and key values are required")
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const s3 = new AWS.S3({
|
const s3 = new S3({
|
||||||
region: awsRegion,
|
region: awsRegion,
|
||||||
endpoint: datasource?.config?.endpoint || undefined,
|
endpoint: datasource?.config?.endpoint || undefined,
|
||||||
|
|
||||||
|
credentials: {
|
||||||
accessKeyId: datasource?.config?.accessKeyId as string,
|
accessKeyId: datasource?.config?.accessKeyId as string,
|
||||||
secretAccessKey: datasource?.config?.secretAccessKey as string,
|
secretAccessKey: datasource?.config?.secretAccessKey as string,
|
||||||
apiVersion: "2006-03-01",
|
},
|
||||||
signatureVersion: "v4",
|
|
||||||
})
|
})
|
||||||
const params = { Bucket: bucket, Key: key }
|
const params = { Bucket: bucket, Key: key }
|
||||||
signedUrl = s3.getSignedUrl("putObject", params)
|
signedUrl = await getSignedUrl(s3, new PutObjectCommand(params))
|
||||||
if (datasource?.config?.endpoint) {
|
if (datasource?.config?.endpoint) {
|
||||||
publicUrl = `${datasource.config.endpoint}/${bucket}/${key}`
|
publicUrl = `${datasource.config.endpoint}/${bucket}/${key}`
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
|
||||||
const setup = require("./utilities")
|
|
||||||
|
|
||||||
describe("/component", () => {
|
|
||||||
let request = setup.getRequest()
|
|
||||||
let config = setup.getConfig()
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("fetch definitions", () => {
|
|
||||||
it("should be able to fetch definitions", async () => {
|
|
||||||
const res = await request
|
|
||||||
.get(`/api/${config.getAppId()}/components/definitions`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body["@budibase/standard-components/container"]).toBeDefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
|
||||||
await checkBuilderEndpoint({
|
|
||||||
config,
|
|
||||||
method: "GET",
|
|
||||||
url: `/api/${config.getAppId()}/components/definitions`,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
|
import * as env from "../../../environment"
|
||||||
|
import * as setup from "./utilities"
|
||||||
|
|
||||||
|
describe("/component", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("fetch definitions", () => {
|
||||||
|
it("should be able to fetch definitions locally", async () => {
|
||||||
|
await env.withEnv(
|
||||||
|
{
|
||||||
|
USE_LOCAL_COMPONENT_LIBS: "1",
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/${config.getAppId()}/components/definitions`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(
|
||||||
|
res.body["@budibase/standard-components/container"]
|
||||||
|
).toBeDefined()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to fetch definitions from object store", async () => {
|
||||||
|
await env.withEnv(
|
||||||
|
{
|
||||||
|
USE_LOCAL_COMPONENT_LIBS: "0",
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
// init again to make an app with a real component lib
|
||||||
|
await config.init()
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/${config.getAppId()}/components/definitions`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(
|
||||||
|
res.body["@budibase/standard-components/container"]
|
||||||
|
).toBeDefined()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should apply authorization to endpoint", async () => {
|
||||||
|
await checkBuilderEndpoint({
|
||||||
|
config,
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/${config.getAppId()}/components/definitions`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -8,6 +8,7 @@ import {
|
||||||
SourceType,
|
SourceType,
|
||||||
UsageInScreensResponse,
|
UsageInScreensResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { basicDatasourcePlus } from "../../../tests/utilities/structures"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
basicScreen,
|
basicScreen,
|
||||||
|
@ -17,7 +18,6 @@ const {
|
||||||
basicTable,
|
basicTable,
|
||||||
viewV2,
|
viewV2,
|
||||||
basicQuery,
|
basicQuery,
|
||||||
basicDatasource,
|
|
||||||
} = setup.structures
|
} = setup.structures
|
||||||
|
|
||||||
describe("/screens", () => {
|
describe("/screens", () => {
|
||||||
|
@ -225,7 +225,7 @@ describe("/screens", () => {
|
||||||
|
|
||||||
it("should find datasource/query usage", async () => {
|
it("should find datasource/query usage", async () => {
|
||||||
const datasource = await config.api.datasource.create(
|
const datasource = await config.api.datasource.create(
|
||||||
basicDatasource().datasource
|
basicDatasourcePlus().datasource
|
||||||
)
|
)
|
||||||
const query = await config.api.query.save(basicQuery(datasource._id!))
|
const query = await config.api.query.save(basicQuery(datasource._id!))
|
||||||
const screen = await config.api.screen.save(
|
const screen = await config.api.screen.save(
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
// Directly mock the AWS SDK
|
// Directly mock the AWS SDK
|
||||||
jest.mock("aws-sdk", () => ({
|
jest.mock("@aws-sdk/s3-request-presigner", () => ({
|
||||||
S3: jest.fn(() => ({
|
getSignedUrl: jest.fn(() => {
|
||||||
getSignedUrl: jest.fn(
|
return `http://example.com`
|
||||||
(operation, params) => `http://example.com/${params.Bucket}/${params.Key}`
|
}),
|
||||||
),
|
|
||||||
upload: jest.fn(() => ({ Contents: {} })),
|
|
||||||
})),
|
|
||||||
}))
|
}))
|
||||||
|
jest.mock("@aws-sdk/client-s3")
|
||||||
|
|
||||||
import { Datasource, SourceName } from "@budibase/types"
|
import { Datasource, SourceName } from "@budibase/types"
|
||||||
import { setEnv } from "../../../environment"
|
import { setEnv } from "../../../environment"
|
||||||
|
@ -77,7 +75,10 @@ describe("/static", () => {
|
||||||
type: "datasource",
|
type: "datasource",
|
||||||
name: "Test",
|
name: "Test",
|
||||||
source: SourceName.S3,
|
source: SourceName.S3,
|
||||||
config: {},
|
config: {
|
||||||
|
accessKeyId: "bb",
|
||||||
|
secretAccessKey: "bb",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -91,7 +92,7 @@ describe("/static", () => {
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.signedUrl).toEqual("http://example.com/foo/bar")
|
expect(res.body.signedUrl).toEqual("http://example.com")
|
||||||
expect(res.body.publicUrl).toEqual(
|
expect(res.body.publicUrl).toEqual(
|
||||||
`https://${bucket}.s3.eu-west-1.amazonaws.com/${key}`
|
`https://${bucket}.s3.eu-west-1.amazonaws.com/${key}`
|
||||||
)
|
)
|
||||||
|
|
|
@ -146,11 +146,12 @@ describe("test the create row action", () => {
|
||||||
expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key")
|
expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key")
|
||||||
let s3Key = result.steps[1].outputs.row.file_attachment[0].key
|
let s3Key = result.steps[1].outputs.row.file_attachment[0].key
|
||||||
|
|
||||||
const client = objectStore.ObjectStore(objectStore.ObjectStoreBuckets.APPS)
|
const client = objectStore.ObjectStore()
|
||||||
|
|
||||||
const objectData = await client
|
const objectData = await client.headObject({
|
||||||
.headObject({ Bucket: objectStore.ObjectStoreBuckets.APPS, Key: s3Key })
|
Bucket: objectStore.ObjectStoreBuckets.APPS,
|
||||||
.promise()
|
Key: s3Key,
|
||||||
|
})
|
||||||
|
|
||||||
expect(objectData).toBeDefined()
|
expect(objectData).toBeDefined()
|
||||||
expect(objectData.ContentLength).toBeGreaterThan(0)
|
expect(objectData.ContentLength).toBeGreaterThan(0)
|
||||||
|
@ -217,11 +218,12 @@ describe("test the create row action", () => {
|
||||||
)
|
)
|
||||||
let s3Key = result.steps[1].outputs.row.single_file_attachment.key
|
let s3Key = result.steps[1].outputs.row.single_file_attachment.key
|
||||||
|
|
||||||
const client = objectStore.ObjectStore(objectStore.ObjectStoreBuckets.APPS)
|
const client = objectStore.ObjectStore()
|
||||||
|
|
||||||
const objectData = await client
|
const objectData = await client.headObject({
|
||||||
.headObject({ Bucket: objectStore.ObjectStoreBuckets.APPS, Key: s3Key })
|
Bucket: objectStore.ObjectStoreBuckets.APPS,
|
||||||
.promise()
|
Key: s3Key,
|
||||||
|
})
|
||||||
|
|
||||||
expect(objectData).toBeDefined()
|
expect(objectData).toBeDefined()
|
||||||
expect(objectData.ContentLength).toBeGreaterThan(0)
|
expect(objectData.ContentLength).toBeGreaterThan(0)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
import { captureAutomationRuns } from "../utilities"
|
import { captureAutomationResults } from "../utilities"
|
||||||
|
|
||||||
describe("cron trigger", () => {
|
describe("cron trigger", () => {
|
||||||
const config = new TestConfiguration()
|
const config = new TestConfiguration()
|
||||||
|
@ -14,14 +14,14 @@ describe("cron trigger", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should queue a Bull cron job", async () => {
|
it("should queue a Bull cron job", async () => {
|
||||||
await createAutomationBuilder(config)
|
const { automation } = await createAutomationBuilder(config)
|
||||||
.onCron({ cron: "* * * * *" })
|
.onCron({ cron: "* * * * *" })
|
||||||
.serverLog({
|
.serverLog({
|
||||||
text: "Hello, world!",
|
text: "Hello, world!",
|
||||||
})
|
})
|
||||||
.save()
|
.save()
|
||||||
|
|
||||||
const jobs = await captureAutomationRuns(() =>
|
const jobs = await captureAutomationResults(automation, () =>
|
||||||
config.api.application.publish()
|
config.api.application.publish()
|
||||||
)
|
)
|
||||||
expect(jobs).toHaveLength(1)
|
expect(jobs).toHaveLength(1)
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
|
import { captureAutomationResults } from "../utilities"
|
||||||
|
import { Automation, Table } from "@budibase/types"
|
||||||
|
import { basicTable } from "../../../tests/utilities/structures"
|
||||||
|
|
||||||
|
describe("row deleted trigger", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
let table: Table
|
||||||
|
let automation: Automation
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
table = await config.api.table.save(basicTable())
|
||||||
|
automation = await createAutomationBuilder(config)
|
||||||
|
.onRowDeleted({ tableId: table._id! })
|
||||||
|
.serverLog({
|
||||||
|
text: "Row was deleted",
|
||||||
|
})
|
||||||
|
.save()
|
||||||
|
.then(({ automation }) => automation)
|
||||||
|
|
||||||
|
await config.api.application.publish()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
config.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should trigger when a row is deleted", async () => {
|
||||||
|
const jobs = await captureAutomationResults(automation, async () => {
|
||||||
|
await config.withProdApp(async () => {
|
||||||
|
const row = await config.api.row.save(table._id!, { name: "foo" })
|
||||||
|
await config.api.row.delete(table._id!, { _id: row._id! })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(jobs).toHaveLength(1)
|
||||||
|
expect(jobs[0].data.event).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
tableId: table._id!,
|
||||||
|
row: expect.objectContaining({ name: "foo" }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not trigger when a row is deleted in a different table", async () => {
|
||||||
|
const otherTable = await config.api.table.save(basicTable())
|
||||||
|
await config.api.application.publish()
|
||||||
|
|
||||||
|
const jobs = await captureAutomationResults(automation, async () => {
|
||||||
|
await config.withProdApp(async () => {
|
||||||
|
const row = await config.api.row.save(otherTable._id!, { name: "bar" })
|
||||||
|
await config.api.row.delete(otherTable._id!, { _id: row._id! })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(jobs).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,20 +1,22 @@
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
import { Table } from "@budibase/types"
|
import { Automation, Table } from "@budibase/types"
|
||||||
import { basicTable } from "../../../tests/utilities/structures"
|
import { basicTable } from "../../../tests/utilities/structures"
|
||||||
import { captureAutomationRuns } from "../utilities"
|
import { captureAutomationResults } from "../utilities"
|
||||||
|
|
||||||
describe("row saved trigger", () => {
|
describe("row saved trigger", () => {
|
||||||
const config = new TestConfiguration()
|
const config = new TestConfiguration()
|
||||||
let table: Table
|
let table: Table
|
||||||
|
let automation: Automation
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
table = await config.api.table.save(basicTable())
|
table = await config.api.table.save(basicTable())
|
||||||
await createAutomationBuilder(config)
|
automation = await createAutomationBuilder(config)
|
||||||
.onRowSaved({ tableId: table._id! })
|
.onRowSaved({ tableId: table._id! })
|
||||||
.serverLog({ text: "Row created!" })
|
.serverLog({ text: "Row created!" })
|
||||||
.save()
|
.save()
|
||||||
|
.then(({ automation }) => automation)
|
||||||
|
|
||||||
await config.api.application.publish()
|
await config.api.application.publish()
|
||||||
})
|
})
|
||||||
|
@ -24,12 +26,12 @@ describe("row saved trigger", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should queue a Bull job when a row is created", async () => {
|
it("should queue a Bull job when a row is created", async () => {
|
||||||
const jobs = await captureAutomationRuns(() =>
|
const results = await captureAutomationResults(automation, () =>
|
||||||
config.withProdApp(() => config.api.row.save(table._id!, { name: "foo" }))
|
config.withProdApp(() => config.api.row.save(table._id!, { name: "foo" }))
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(jobs).toHaveLength(1)
|
expect(results).toHaveLength(1)
|
||||||
expect(jobs[0].data.event).toEqual(
|
expect(results[0].data.event).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
row: expect.objectContaining({ name: "foo" }),
|
row: expect.objectContaining({ name: "foo" }),
|
||||||
|
@ -41,12 +43,12 @@ describe("row saved trigger", () => {
|
||||||
const otherTable = await config.api.table.save(basicTable())
|
const otherTable = await config.api.table.save(basicTable())
|
||||||
await config.api.application.publish()
|
await config.api.application.publish()
|
||||||
|
|
||||||
const jobs = await captureAutomationRuns(() =>
|
const results = await captureAutomationResults(automation, () =>
|
||||||
config.withProdApp(() =>
|
config.withProdApp(() =>
|
||||||
config.api.row.save(otherTable._id!, { name: "foo" })
|
config.api.row.save(otherTable._id!, { name: "foo" })
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(jobs).toBeEmpty()
|
expect(results).toBeEmpty()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
|
import { Automation, Table } from "@budibase/types"
|
||||||
|
import { basicTable } from "../../../tests/utilities/structures"
|
||||||
|
import { captureAutomationResults } from "../utilities"
|
||||||
|
|
||||||
|
describe("row updated trigger", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
let table: Table
|
||||||
|
let automation: Automation
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
table = await config.api.table.save(basicTable())
|
||||||
|
automation = await createAutomationBuilder(config)
|
||||||
|
.onRowUpdated({ tableId: table._id! })
|
||||||
|
.serverLog({ text: "Row updated!" })
|
||||||
|
.save()
|
||||||
|
.then(({ automation }) => automation)
|
||||||
|
|
||||||
|
await config.api.application.publish()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
config.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should queue a Bull job when a row is updated", async () => {
|
||||||
|
const results = await captureAutomationResults(automation, async () => {
|
||||||
|
await config.withProdApp(async () => {
|
||||||
|
const row = await config.api.row.save(table._id!, { name: "foo" })
|
||||||
|
await config.api.row.save(table._id!, { _id: row._id!, name: "bar" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(results).toHaveLength(1)
|
||||||
|
expect(results[0].data.event).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
tableId: table._id!,
|
||||||
|
row: expect.objectContaining({ name: "bar" }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not fire for rows updated in other tables", async () => {
|
||||||
|
const otherTable = await config.api.table.save(basicTable())
|
||||||
|
await config.api.application.publish()
|
||||||
|
|
||||||
|
const results = await captureAutomationResults(automation, async () => {
|
||||||
|
await config.withProdApp(async () => {
|
||||||
|
const row = await config.api.row.save(otherTable._id!, { name: "foo" })
|
||||||
|
await config.api.row.save(otherTable._id!, {
|
||||||
|
_id: row._id!,
|
||||||
|
name: "bar",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(results).toBeEmpty()
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,7 +1,7 @@
|
||||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
|
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { AutomationData, Datasource } from "@budibase/types"
|
import { Automation, AutomationData, Datasource } from "@budibase/types"
|
||||||
import { Knex } from "knex"
|
import { Knex } from "knex"
|
||||||
import { getQueue } from "../.."
|
import { getQueue } from "../.."
|
||||||
import { Job } from "bull"
|
import { Job } from "bull"
|
||||||
|
@ -38,7 +38,7 @@ export async function runInProd(fn: any) {
|
||||||
* Capture all automation runs that occur during the execution of a function.
|
* Capture all automation runs that occur during the execution of a function.
|
||||||
* This function will wait for all messages to be processed before returning.
|
* This function will wait for all messages to be processed before returning.
|
||||||
*/
|
*/
|
||||||
export async function captureAutomationRuns(
|
export async function captureAllAutomationResults(
|
||||||
f: () => Promise<unknown>
|
f: () => Promise<unknown>
|
||||||
): Promise<Job<AutomationData>[]> {
|
): Promise<Job<AutomationData>[]> {
|
||||||
const runs: Job<AutomationData>[] = []
|
const runs: Job<AutomationData>[] = []
|
||||||
|
@ -72,6 +72,18 @@ export async function captureAutomationRuns(
|
||||||
return runs
|
return runs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function captureAutomationResults(
|
||||||
|
automation: Automation | string,
|
||||||
|
f: () => Promise<unknown>
|
||||||
|
) {
|
||||||
|
const results = await captureAllAutomationResults(f)
|
||||||
|
return results.filter(
|
||||||
|
r =>
|
||||||
|
r.data.automation._id ===
|
||||||
|
(typeof automation === "string" ? automation : automation._id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function saveTestQuery(
|
export async function saveTestQuery(
|
||||||
config: TestConfiguration,
|
config: TestConfiguration,
|
||||||
client: Knex,
|
client: Knex,
|
||||||
|
|
|
@ -29,6 +29,7 @@ const DEFAULTS = {
|
||||||
PLUGINS_DIR: "/plugins",
|
PLUGINS_DIR: "/plugins",
|
||||||
FORKED_PROCESS_NAME: "main",
|
FORKED_PROCESS_NAME: "main",
|
||||||
JS_RUNNER_MEMORY_LIMIT: 64,
|
JS_RUNNER_MEMORY_LIMIT: 64,
|
||||||
|
USE_LOCAL_COMPONENT_LIBS: coreEnv.isDev() || coreEnv.isTest(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUERY_THREAD_TIMEOUT =
|
const QUERY_THREAD_TIMEOUT =
|
||||||
|
@ -113,6 +114,8 @@ const environment = {
|
||||||
DEFAULTS.JS_RUNNER_MEMORY_LIMIT,
|
DEFAULTS.JS_RUNNER_MEMORY_LIMIT,
|
||||||
LOG_JS_ERRORS: process.env.LOG_JS_ERRORS,
|
LOG_JS_ERRORS: process.env.LOG_JS_ERRORS,
|
||||||
DISABLE_USER_SYNC: process.env.DISABLE_USER_SYNC,
|
DISABLE_USER_SYNC: process.env.DISABLE_USER_SYNC,
|
||||||
|
USE_LOCAL_COMPONENT_LIBS:
|
||||||
|
process.env.USE_LOCAL_COMPONENT_LIBS || DEFAULTS.USE_LOCAL_COMPONENT_LIBS,
|
||||||
// old
|
// old
|
||||||
CLIENT_ID: process.env.CLIENT_ID,
|
CLIENT_ID: process.env.CLIENT_ID,
|
||||||
_set(key: string, value: any) {
|
_set(key: string, value: any) {
|
||||||
|
|
|
@ -7,9 +7,15 @@ import {
|
||||||
ConnectionInfo,
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
import AWS from "aws-sdk"
|
import {
|
||||||
|
DynamoDBDocument,
|
||||||
|
PutCommandInput,
|
||||||
|
GetCommandInput,
|
||||||
|
UpdateCommandInput,
|
||||||
|
DeleteCommandInput,
|
||||||
|
} from "@aws-sdk/lib-dynamodb"
|
||||||
|
import { DynamoDB } from "@aws-sdk/client-dynamodb"
|
||||||
import { AWS_REGION } from "../constants"
|
import { AWS_REGION } from "../constants"
|
||||||
import { DocumentClient } from "aws-sdk/clients/dynamodb"
|
|
||||||
|
|
||||||
interface DynamoDBConfig {
|
interface DynamoDBConfig {
|
||||||
region: string
|
region: string
|
||||||
|
@ -151,7 +157,7 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
region: config.region || AWS_REGION,
|
region: config.region || AWS_REGION,
|
||||||
endpoint: config.endpoint || undefined,
|
endpoint: config.endpoint || undefined,
|
||||||
}
|
}
|
||||||
this.client = new AWS.DynamoDB.DocumentClient(this.config)
|
this.client = DynamoDBDocument.from(new DynamoDB(this.config))
|
||||||
}
|
}
|
||||||
|
|
||||||
async testConnection() {
|
async testConnection() {
|
||||||
|
@ -159,8 +165,8 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
connected: false,
|
connected: false,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const scanRes = await new AWS.DynamoDB(this.config).listTables().promise()
|
const scanRes = await new DynamoDB(this.config).listTables()
|
||||||
response.connected = !!scanRes.$response
|
response.connected = !!scanRes.$metadata
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
response.error = e.message as string
|
response.error = e.message as string
|
||||||
}
|
}
|
||||||
|
@ -169,13 +175,13 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
|
|
||||||
async create(query: {
|
async create(query: {
|
||||||
table: string
|
table: string
|
||||||
json: Omit<DocumentClient.PutItemInput, "TableName">
|
json: Omit<PutCommandInput, "TableName">
|
||||||
}) {
|
}) {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
...query.json,
|
...query.json,
|
||||||
}
|
}
|
||||||
return this.client.put(params).promise()
|
return this.client.put(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async read(query: { table: string; json: object; index: null | string }) {
|
async read(query: { table: string; json: object; index: null | string }) {
|
||||||
|
@ -184,7 +190,7 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
IndexName: query.index ? query.index : undefined,
|
IndexName: query.index ? query.index : undefined,
|
||||||
...query.json,
|
...query.json,
|
||||||
}
|
}
|
||||||
const response = await this.client.query(params).promise()
|
const response = await this.client.query(params)
|
||||||
if (response.Items) {
|
if (response.Items) {
|
||||||
return response.Items
|
return response.Items
|
||||||
}
|
}
|
||||||
|
@ -197,7 +203,7 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
IndexName: query.index ? query.index : undefined,
|
IndexName: query.index ? query.index : undefined,
|
||||||
...query.json,
|
...query.json,
|
||||||
}
|
}
|
||||||
const response = await this.client.scan(params).promise()
|
const response = await this.client.scan(params)
|
||||||
if (response.Items) {
|
if (response.Items) {
|
||||||
return response.Items
|
return response.Items
|
||||||
}
|
}
|
||||||
|
@ -208,40 +214,40 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
}
|
}
|
||||||
return new AWS.DynamoDB(this.config).describeTable(params).promise()
|
return new DynamoDB(this.config).describeTable(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(query: {
|
async get(query: {
|
||||||
table: string
|
table: string
|
||||||
json: Omit<DocumentClient.GetItemInput, "TableName">
|
json: Omit<GetCommandInput, "TableName">
|
||||||
}) {
|
}) {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
...query.json,
|
...query.json,
|
||||||
}
|
}
|
||||||
return this.client.get(params).promise()
|
return this.client.get(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(query: {
|
async update(query: {
|
||||||
table: string
|
table: string
|
||||||
json: Omit<DocumentClient.UpdateItemInput, "TableName">
|
json: Omit<UpdateCommandInput, "TableName">
|
||||||
}) {
|
}) {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
...query.json,
|
...query.json,
|
||||||
}
|
}
|
||||||
return this.client.update(params).promise()
|
return this.client.update(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(query: {
|
async delete(query: {
|
||||||
table: string
|
table: string
|
||||||
json: Omit<DocumentClient.DeleteItemInput, "TableName">
|
json: Omit<DeleteCommandInput, "TableName">
|
||||||
}) {
|
}) {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
...query.json,
|
...query.json,
|
||||||
}
|
}
|
||||||
return this.client.delete(params).promise()
|
return this.client.delete(params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,9 @@ import {
|
||||||
ConnectionInfo,
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
import AWS from "aws-sdk"
|
import { S3 } from "@aws-sdk/client-s3"
|
||||||
import csv from "csvtojson"
|
import csv from "csvtojson"
|
||||||
|
import stream from "stream"
|
||||||
|
|
||||||
interface S3Config {
|
interface S3Config {
|
||||||
region: string
|
region: string
|
||||||
|
@ -167,7 +168,7 @@ class S3Integration implements IntegrationBase {
|
||||||
delete this.config.endpoint
|
delete this.config.endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client = new AWS.S3(this.config)
|
this.client = new S3(this.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
async testConnection() {
|
async testConnection() {
|
||||||
|
@ -175,7 +176,7 @@ class S3Integration implements IntegrationBase {
|
||||||
connected: false,
|
connected: false,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.client.listBuckets().promise()
|
await this.client.listBuckets()
|
||||||
response.connected = true
|
response.connected = true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
response.error = e.message as string
|
response.error = e.message as string
|
||||||
|
@ -209,7 +210,7 @@ class S3Integration implements IntegrationBase {
|
||||||
LocationConstraint: query.location,
|
LocationConstraint: query.location,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await this.client.createBucket(params).promise()
|
return await this.client.createBucket(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async read(query: {
|
async read(query: {
|
||||||
|
@ -220,37 +221,39 @@ class S3Integration implements IntegrationBase {
|
||||||
maxKeys: number
|
maxKeys: number
|
||||||
prefix: string
|
prefix: string
|
||||||
}) {
|
}) {
|
||||||
const response = await this.client
|
const response = await this.client.listObjects({
|
||||||
.listObjects({
|
|
||||||
Bucket: query.bucket,
|
Bucket: query.bucket,
|
||||||
Delimiter: query.delimiter,
|
Delimiter: query.delimiter,
|
||||||
Marker: query.marker,
|
Marker: query.marker,
|
||||||
MaxKeys: query.maxKeys,
|
MaxKeys: query.maxKeys,
|
||||||
Prefix: query.prefix,
|
Prefix: query.prefix,
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
return response.Contents
|
return response.Contents
|
||||||
}
|
}
|
||||||
|
|
||||||
async readCsv(query: { bucket: string; key: string }) {
|
async readCsv(query: { bucket: string; key: string }) {
|
||||||
const stream = this.client
|
const response = await this.client.getObject({
|
||||||
.getObject({
|
|
||||||
Bucket: query.bucket,
|
Bucket: query.bucket,
|
||||||
Key: query.key,
|
Key: query.key,
|
||||||
})
|
})
|
||||||
.createReadStream()
|
|
||||||
|
const fileStream = response.Body?.transformToWebStream()
|
||||||
|
|
||||||
|
if (!fileStream || !(fileStream instanceof stream.Readable)) {
|
||||||
|
throw new Error("Unable to retrieve CSV - invalid stream")
|
||||||
|
}
|
||||||
|
|
||||||
let csvError = false
|
let csvError = false
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
stream.on("error", (err: Error) => {
|
fileStream.on("error", (err: Error) => {
|
||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
const response = csv()
|
const response = csv()
|
||||||
.fromStream(stream)
|
.fromStream(fileStream)
|
||||||
.on("error", () => {
|
.on("error", () => {
|
||||||
csvError = true
|
csvError = true
|
||||||
})
|
})
|
||||||
stream.on("finish", () => {
|
fileStream.on("finish", () => {
|
||||||
resolve(response)
|
resolve(response)
|
||||||
})
|
})
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
@ -263,12 +266,10 @@ class S3Integration implements IntegrationBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(query: { bucket: string; delete: string }) {
|
async delete(query: { bucket: string; delete: string }) {
|
||||||
return await this.client
|
return await this.client.deleteObjects({
|
||||||
.deleteObjects({
|
|
||||||
Bucket: query.bucket,
|
Bucket: query.bucket,
|
||||||
Delete: JSON.parse(query.delete),
|
Delete: JSON.parse(query.delete),
|
||||||
})
|
})
|
||||||
.promise()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
const response = (body: any, extra?: any) => () => ({
|
|
||||||
promise: () => body,
|
|
||||||
...extra,
|
|
||||||
})
|
|
||||||
|
|
||||||
class DocumentClient {
|
|
||||||
put = jest.fn(response({}))
|
|
||||||
query = jest.fn(
|
|
||||||
response({
|
|
||||||
Items: [],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
scan = jest.fn(
|
|
||||||
response({
|
|
||||||
Items: [
|
|
||||||
{
|
|
||||||
Name: "test",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
get = jest.fn(response({}))
|
|
||||||
update = jest.fn(response({}))
|
|
||||||
delete = jest.fn(response({}))
|
|
||||||
}
|
|
||||||
|
|
||||||
class S3 {
|
|
||||||
listObjects = jest.fn(
|
|
||||||
response({
|
|
||||||
Contents: [],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
createBucket = jest.fn(
|
|
||||||
response({
|
|
||||||
Contents: {},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
deleteObjects = jest.fn(
|
|
||||||
response({
|
|
||||||
Contents: {},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
getSignedUrl = jest.fn((operation, params) => {
|
|
||||||
return `http://example.com/${params.Bucket}/${params.Key}`
|
|
||||||
})
|
|
||||||
headBucket = jest.fn(
|
|
||||||
response({
|
|
||||||
Contents: {},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
upload = jest.fn(
|
|
||||||
response({
|
|
||||||
Contents: {},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
getObject = jest.fn(
|
|
||||||
response(
|
|
||||||
{
|
|
||||||
Body: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
createReadStream: jest.fn().mockReturnValue("stream"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
DynamoDB: {
|
|
||||||
DocumentClient,
|
|
||||||
},
|
|
||||||
S3,
|
|
||||||
config: {
|
|
||||||
update: jest.fn(),
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,4 +1,20 @@
|
||||||
jest.mock("aws-sdk", () => require("./aws-sdk.mock"))
|
jest.mock("@aws-sdk/lib-dynamodb", () => ({
|
||||||
|
DynamoDBDocument: {
|
||||||
|
from: jest.fn(() => ({
|
||||||
|
update: jest.fn(),
|
||||||
|
put: jest.fn(),
|
||||||
|
query: jest.fn(() => ({
|
||||||
|
Items: [],
|
||||||
|
})),
|
||||||
|
scan: jest.fn(() => ({
|
||||||
|
Items: [],
|
||||||
|
})),
|
||||||
|
delete: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
jest.mock("@aws-sdk/client-dynamodb")
|
||||||
import { default as DynamoDBIntegration } from "../dynamodb"
|
import { default as DynamoDBIntegration } from "../dynamodb"
|
||||||
|
|
||||||
class TestConfiguration {
|
class TestConfiguration {
|
||||||
|
@ -57,11 +73,7 @@ describe("DynamoDB Integration", () => {
|
||||||
TableName: tableName,
|
TableName: tableName,
|
||||||
IndexName: indexName,
|
IndexName: indexName,
|
||||||
})
|
})
|
||||||
expect(response).toEqual([
|
expect(response).toEqual([])
|
||||||
{
|
|
||||||
Name: "test",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calls the get method with the correct params", async () => {
|
it("calls the get method with the correct params", async () => {
|
||||||
|
|
|
@ -1,5 +1,52 @@
|
||||||
jest.mock("aws-sdk", () => require("./aws-sdk.mock"))
|
|
||||||
import { default as S3Integration } from "../s3"
|
import { default as S3Integration } from "../s3"
|
||||||
|
jest.mock("@aws-sdk/client-s3", () => {
|
||||||
|
class S3Mock {
|
||||||
|
response(body: any, extra?: any) {
|
||||||
|
return () => ({
|
||||||
|
promise: () => body,
|
||||||
|
...extra,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
listObjects = jest.fn(
|
||||||
|
this.response({
|
||||||
|
Contents: [],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
createBucket = jest.fn(
|
||||||
|
this.response({
|
||||||
|
Contents: {},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
deleteObjects = jest.fn(
|
||||||
|
this.response({
|
||||||
|
Contents: {},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
headBucket = jest.fn(
|
||||||
|
this.response({
|
||||||
|
Contents: {},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
upload = jest.fn(
|
||||||
|
this.response({
|
||||||
|
Contents: {},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
getObject = jest.fn(
|
||||||
|
this.response(
|
||||||
|
{
|
||||||
|
Body: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createReadStream: jest.fn().mockReturnValue("stream"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { S3: S3Mock }
|
||||||
|
})
|
||||||
|
|
||||||
class TestConfiguration {
|
class TestConfiguration {
|
||||||
integration: any
|
integration: any
|
||||||
|
|
|
@ -430,7 +430,7 @@ export async function handleFileResponse(
|
||||||
size = details.ContentLength
|
size = details.ContentLength
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
presignedUrl = objectStore.getPresignedUrl(bucket, key)
|
presignedUrl = await objectStore.getPresignedUrl(bucket, key)
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
size,
|
size,
|
||||||
|
|
|
@ -18,7 +18,7 @@ export async function fetch(type?: PluginType): Promise<Plugin[]> {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
let plugins = response.rows.map((row: any) => row.doc) as Plugin[]
|
let plugins = response.rows.map((row: any) => row.doc) as Plugin[]
|
||||||
plugins = objectStore.enrichPluginURLs(plugins)
|
plugins = await objectStore.enrichPluginURLs(plugins)
|
||||||
if (type) {
|
if (type) {
|
||||||
return plugins.filter((plugin: Plugin) => plugin.schema?.type === type)
|
return plugins.filter((plugin: Plugin) => plugin.schema?.type === type)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -492,6 +492,15 @@ export function basicDatasource(): { datasource: Datasource } {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function basicDatasourcePlus(): { datasource: Datasource } {
|
||||||
|
return {
|
||||||
|
datasource: {
|
||||||
|
...basicDatasource().datasource,
|
||||||
|
plus: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function basicQuery(datasourceId: string): Query {
|
export function basicQuery(datasourceId: string): Query {
|
||||||
return {
|
return {
|
||||||
datasourceId,
|
datasourceId,
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules")
|
||||||
* @param appId The ID of the app which is being created.
|
* @param appId The ID of the app which is being created.
|
||||||
* @return once promise completes app resources should be ready in object store.
|
* @return once promise completes app resources should be ready in object store.
|
||||||
*/
|
*/
|
||||||
export const createApp = async (appId: string) => {
|
export const uploadAppFiles = async (appId: string) => {
|
||||||
await updateClientLibrary(appId)
|
await updateClientLibrary(appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export const createApp = async (appId: string) => {
|
||||||
* @param appId The ID of the app which is being deleted.
|
* @param appId The ID of the app which is being deleted.
|
||||||
* @return once promise completes the app resources will be removed from object store.
|
* @return once promise completes the app resources will be removed from object store.
|
||||||
*/
|
*/
|
||||||
export const deleteApp = async (appId: string) => {
|
export const deleteAppFiles = async (appId: string) => {
|
||||||
await objectStore.deleteFolder(ObjectStoreBuckets.APPS, `${appId}/`)
|
await objectStore.deleteFolder(ObjectStoreBuckets.APPS, `${appId}/`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,11 +36,11 @@ export const getComponentLibraryManifest = async (library: string) => {
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
const filename = "manifest.json"
|
const filename = "manifest.json"
|
||||||
|
|
||||||
if (env.isDev() || env.isTest()) {
|
if (env.USE_LOCAL_COMPONENT_LIBS) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const app = await db.get<App>(DocumentType.APP_METADATA)
|
const app = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
|
|
||||||
if (shouldServeLocally(app.version) || env.isTest()) {
|
if (shouldServeLocally(app.version) || env.USE_LOCAL_COMPONENT_LIBS) {
|
||||||
const paths = [
|
const paths = [
|
||||||
join(TOP_LEVEL_PATH, "packages/client", filename),
|
join(TOP_LEVEL_PATH, "packages/client", filename),
|
||||||
join(process.cwd(), "client", filename),
|
join(process.cwd(), "client", filename),
|
||||||
|
@ -78,7 +78,7 @@ export const getComponentLibraryManifest = async (library: string) => {
|
||||||
resp = await objectStore.retrieve(ObjectStoreBuckets.APPS, path)
|
resp = await objectStore.retrieve(ObjectStoreBuckets.APPS, path)
|
||||||
}
|
}
|
||||||
if (typeof resp !== "string") {
|
if (typeof resp !== "string") {
|
||||||
resp = resp.toString("utf8")
|
resp = resp.toString()
|
||||||
}
|
}
|
||||||
return JSON.parse(resp)
|
return JSON.parse(resp)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { budibaseTempDir } from "../budibaseDir"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { objectStore } from "@budibase/backend-core"
|
import { objectStore } from "@budibase/backend-core"
|
||||||
|
import stream from "stream"
|
||||||
|
|
||||||
const DATASOURCE_PATH = join(budibaseTempDir(), "datasource")
|
const DATASOURCE_PATH = join(budibaseTempDir(), "datasource")
|
||||||
const AUTOMATION_PATH = join(budibaseTempDir(), "automation")
|
const AUTOMATION_PATH = join(budibaseTempDir(), "automation")
|
||||||
|
@ -58,7 +59,11 @@ async function getPluginImpl(path: string, plugin: Plugin) {
|
||||||
pluginKey
|
pluginKey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (pluginJs instanceof stream.Readable) {
|
||||||
|
pluginJs.pipe(fs.createWriteStream(filename))
|
||||||
|
} else {
|
||||||
fs.writeFileSync(filename, pluginJs)
|
fs.writeFileSync(filename, pluginJs)
|
||||||
|
}
|
||||||
fs.writeFileSync(metadataName, hash)
|
fs.writeFileSync(metadataName, hash)
|
||||||
|
|
||||||
return require(filename)
|
return require(filename)
|
||||||
|
|
|
@ -359,9 +359,9 @@ export async function coreOutputProcessing(
|
||||||
if (row[property] == null) {
|
if (row[property] == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const process = (attachment: RowAttachment) => {
|
const process = async (attachment: RowAttachment) => {
|
||||||
if (!attachment.url && attachment.key) {
|
if (!attachment.url && attachment.key) {
|
||||||
attachment.url = objectStore.getAppFileUrl(attachment.key)
|
attachment.url = await objectStore.getAppFileUrl(attachment.key)
|
||||||
}
|
}
|
||||||
return attachment
|
return attachment
|
||||||
}
|
}
|
||||||
|
@ -369,11 +369,13 @@ export async function coreOutputProcessing(
|
||||||
row[property] = JSON.parse(row[property])
|
row[property] = JSON.parse(row[property])
|
||||||
}
|
}
|
||||||
if (Array.isArray(row[property])) {
|
if (Array.isArray(row[property])) {
|
||||||
row[property].forEach((attachment: RowAttachment) => {
|
await Promise.all(
|
||||||
|
row[property].map((attachment: RowAttachment) =>
|
||||||
process(attachment)
|
process(attachment)
|
||||||
})
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
process(row[property])
|
await process(row[property])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
|
|
@ -322,27 +322,27 @@ export async function save(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function enrichOIDCLogos(oidcLogos: OIDCLogosConfig) {
|
async function enrichOIDCLogos(oidcLogos: OIDCLogosConfig) {
|
||||||
if (!oidcLogos) {
|
if (!oidcLogos) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oidcLogos.config = Object.keys(oidcLogos.config || {}).reduce(
|
const newConfig: Record<string, string> = {}
|
||||||
(acc: any, key: string) => {
|
const keys = Object.keys(oidcLogos.config || {})
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
if (!key.endsWith("Etag")) {
|
if (!key.endsWith("Etag")) {
|
||||||
const etag = oidcLogos.config[`${key}Etag`]
|
const etag = oidcLogos.config[`${key}Etag`]
|
||||||
const objectStoreUrl = objectStore.getGlobalFileUrl(
|
const objectStoreUrl = await objectStore.getGlobalFileUrl(
|
||||||
oidcLogos.type,
|
oidcLogos.type,
|
||||||
key,
|
key,
|
||||||
etag
|
etag
|
||||||
)
|
)
|
||||||
acc[key] = objectStoreUrl
|
newConfig[key] = objectStoreUrl
|
||||||
} else {
|
} else {
|
||||||
acc[key] = oidcLogos.config[key]
|
newConfig[key] = oidcLogos.config[key]
|
||||||
}
|
}
|
||||||
return acc
|
}
|
||||||
},
|
oidcLogos.config = newConfig
|
||||||
{}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: UserCtx<void, FindConfigResponse>) {
|
export async function find(ctx: UserCtx<void, FindConfigResponse>) {
|
||||||
|
@ -370,7 +370,7 @@ export async function find(ctx: UserCtx<void, FindConfigResponse>) {
|
||||||
|
|
||||||
async function handleConfigType(type: ConfigType, config: Config) {
|
async function handleConfigType(type: ConfigType, config: Config) {
|
||||||
if (type === ConfigType.OIDC_LOGOS) {
|
if (type === ConfigType.OIDC_LOGOS) {
|
||||||
enrichOIDCLogos(config)
|
await enrichOIDCLogos(config)
|
||||||
} else if (type === ConfigType.AI) {
|
} else if (type === ConfigType.AI) {
|
||||||
await handleAIConfig(config)
|
await handleAIConfig(config)
|
||||||
}
|
}
|
||||||
|
@ -396,7 +396,7 @@ export async function publicOidc(ctx: Ctx<void, GetPublicOIDCConfigResponse>) {
|
||||||
const oidcCustomLogos = await configs.getOIDCLogosDoc()
|
const oidcCustomLogos = await configs.getOIDCLogosDoc()
|
||||||
|
|
||||||
if (oidcCustomLogos) {
|
if (oidcCustomLogos) {
|
||||||
enrichOIDCLogos(oidcCustomLogos)
|
await enrichOIDCLogos(oidcCustomLogos)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!oidcConfig) {
|
if (!oidcConfig) {
|
||||||
|
@ -427,7 +427,7 @@ export async function publicSettings(
|
||||||
|
|
||||||
// enrich the logo url - empty url means deleted
|
// enrich the logo url - empty url means deleted
|
||||||
if (config.logoUrl && config.logoUrl !== "") {
|
if (config.logoUrl && config.logoUrl !== "") {
|
||||||
config.logoUrl = objectStore.getGlobalFileUrl(
|
config.logoUrl = await objectStore.getGlobalFileUrl(
|
||||||
"settings",
|
"settings",
|
||||||
"logoUrl",
|
"logoUrl",
|
||||||
config.logoUrlEtag
|
config.logoUrlEtag
|
||||||
|
@ -437,7 +437,7 @@ export async function publicSettings(
|
||||||
// enrich the favicon url - empty url means deleted
|
// enrich the favicon url - empty url means deleted
|
||||||
const faviconUrl =
|
const faviconUrl =
|
||||||
branding.faviconUrl && branding.faviconUrl !== ""
|
branding.faviconUrl && branding.faviconUrl !== ""
|
||||||
? objectStore.getGlobalFileUrl(
|
? await objectStore.getGlobalFileUrl(
|
||||||
"settings",
|
"settings",
|
||||||
"faviconUrl",
|
"faviconUrl",
|
||||||
branding.faviconUrlEtag
|
branding.faviconUrlEtag
|
||||||
|
@ -522,7 +522,7 @@ export async function upload(ctx: UserCtx<void, UploadConfigFileResponse>) {
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "File has been uploaded and url stored to config.",
|
message: "File has been uploaded and url stored to config.",
|
||||||
url: objectStore.getGlobalFileUrl(type, name, etag),
|
url: await objectStore.getGlobalFileUrl(type, name, etag),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { TestConfiguration } from "../../../../tests"
|
import { TestConfiguration } from "../../../../tests"
|
||||||
|
import { withEnv } from "../../../../environment"
|
||||||
|
|
||||||
jest.unmock("node-fetch")
|
jest.unmock("node-fetch")
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ describe("/api/system/environment", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns the expected environment for self hosters", async () => {
|
it("returns the expected environment for self hosters", async () => {
|
||||||
await config.withEnv({ SELF_HOSTED: true }, async () => {
|
await withEnv({ SELF_HOSTED: true }, async () => {
|
||||||
const env = await config.api.environment.getEnvironment()
|
const env = await config.api.environment.getEnvironment()
|
||||||
expect(env.body).toEqual({
|
expect(env.body).toEqual({
|
||||||
cloud: false,
|
cloud: false,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { env as coreEnv } from "@budibase/backend-core"
|
import { env as coreEnv } from "@budibase/backend-core"
|
||||||
import { ServiceType } from "@budibase/types"
|
import { ServiceType } from "@budibase/types"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
|
|
||||||
coreEnv._set("SERVICE_TYPE", ServiceType.WORKER)
|
coreEnv._set("SERVICE_TYPE", ServiceType.WORKER)
|
||||||
|
|
||||||
|
@ -92,6 +93,32 @@ if (!environment.APPS_URL) {
|
||||||
: "http://app-service:4002"
|
: "http://app-service:4002"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setEnv(newEnvVars: Partial<typeof environment>): () => void {
|
||||||
|
const oldEnv = cloneDeep(environment)
|
||||||
|
|
||||||
|
let key: keyof typeof newEnvVars
|
||||||
|
for (key in newEnvVars) {
|
||||||
|
environment._set(key, newEnvVars[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const [key, value] of Object.entries(oldEnv)) {
|
||||||
|
environment._set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withEnv<T>(envVars: Partial<typeof environment>, f: () => T) {
|
||||||
|
const cleanup = setEnv(envVars)
|
||||||
|
const result = f()
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
return result.finally(cleanup)
|
||||||
|
} else {
|
||||||
|
cleanup()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// clean up any environment variable edge cases
|
// clean up any environment variable edge cases
|
||||||
for (let [key, value] of Object.entries(environment)) {
|
for (let [key, value] of Object.entries(environment)) {
|
||||||
// handle the edge case of "0" to disable an environment variable
|
// handle the edge case of "0" to disable an environment variable
|
||||||
|
|
|
@ -35,7 +35,6 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import API from "./api"
|
import API from "./api"
|
||||||
import jwt, { Secret } from "jsonwebtoken"
|
import jwt, { Secret } from "jsonwebtoken"
|
||||||
import cloneDeep from "lodash/fp/cloneDeep"
|
|
||||||
|
|
||||||
class TestConfiguration {
|
class TestConfiguration {
|
||||||
server: any
|
server: any
|
||||||
|
@ -247,34 +246,6 @@ class TestConfiguration {
|
||||||
return { message: "Admin user only endpoint.", status: 403 }
|
return { message: "Admin user only endpoint.", status: 403 }
|
||||||
}
|
}
|
||||||
|
|
||||||
async withEnv(newEnvVars: Partial<typeof env>, f: () => Promise<void>) {
|
|
||||||
let cleanup = this.setEnv(newEnvVars)
|
|
||||||
try {
|
|
||||||
await f()
|
|
||||||
} finally {
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Sets the environment variables to the given values and returns a function
|
|
||||||
* that can be called to reset the environment variables to their original values.
|
|
||||||
*/
|
|
||||||
setEnv(newEnvVars: Partial<typeof env>): () => void {
|
|
||||||
const oldEnv = cloneDeep(env)
|
|
||||||
|
|
||||||
let key: keyof typeof newEnvVars
|
|
||||||
for (key in newEnvVars) {
|
|
||||||
env._set(key, newEnvVars[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const [key, value] of Object.entries(oldEnv)) {
|
|
||||||
env._set(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// USERS
|
// USERS
|
||||||
|
|
||||||
async createDefaultUser() {
|
async createDefaultUser() {
|
||||||
|
|
Loading…
Reference in New Issue