Merge branch 'master' of github.com:Budibase/budibase into chore/api-typing-4

This commit is contained in:
mike12345567 2024-12-05 14:37:08 +00:00
commit 73c5b2f729
24 changed files with 349 additions and 71 deletions

View File

@ -200,6 +200,20 @@ jobs:
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Set up PostgreSQL 16
if: matrix.datasource == 'postgres'
run: |
sudo systemctl stop postgresql
sudo apt-get remove --purge -y postgresql* libpq-dev
sudo rm -rf /etc/postgresql /var/lib/postgresql
sudo apt-get autoremove -y
sudo apt-get autoclean
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt-get update
sudo apt-get install -y postgresql-16
- name: Test server - name: Test server
env: env:
DATASOURCE: ${{ matrix.datasource }} DATASOURCE: ${{ matrix.datasource }}

View File

@ -83,6 +83,7 @@
"@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",

View File

@ -1,6 +1,10 @@
import { Ctx } from "@budibase/types" import { Ctx } from "@budibase/types"
import type { Middleware, Next } from "koa"
export default async (ctx: Ctx, next: any) => { // this middleware exists purely to be overridden by middlewares supplied by the @budibase/pro library
const middleware = (async (ctx: Ctx, next: Next) => {
// Placeholder for audit log middleware // Placeholder for audit log middleware
return next() return next()
} }) as Middleware
export default middleware

View File

@ -22,6 +22,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { ErrorCode, InvalidAPIKeyError } from "../errors" import { ErrorCode, InvalidAPIKeyError } from "../errors"
import tracer from "dd-trace" import tracer from "dd-trace"
import type { Middleware, Next } from "koa"
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
? parseInt(env.SESSION_UPDATE_PERIOD) ? parseInt(env.SESSION_UPDATE_PERIOD)
@ -94,6 +95,14 @@ async function checkApiKey(
}) })
} }
function getHeader(ctx: Ctx, header: Header): string | undefined {
const contents = ctx.request.headers[header]
if (Array.isArray(contents)) {
throw new Error("Unexpected header format")
}
return contents
}
/** /**
* This middleware is tenancy aware, so that it does not depend on other middlewares being used. * This middleware is tenancy aware, so that it does not depend on other middlewares being used.
* The tenancy modules should not be used here and it should be assumed that the tenancy context * The tenancy modules should not be used here and it should be assumed that the tenancy context
@ -106,9 +115,9 @@ export default function (
} }
) { ) {
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
return async (ctx: Ctx | any, next: any) => { return (async (ctx: Ctx, next: Next) => {
let publicEndpoint = false let publicEndpoint = false
const version = ctx.request.headers[Header.API_VER] const version = getHeader(ctx, Header.API_VER)
// the path is not authenticated // the path is not authenticated
const found = matches(ctx, noAuthOptions) const found = matches(ctx, noAuthOptions)
if (found) { if (found) {
@ -116,18 +125,18 @@ export default function (
} }
try { try {
// check the actual user is authenticated first, try header or cookie // check the actual user is authenticated first, try header or cookie
let headerToken = ctx.request.headers[Header.TOKEN] let headerToken = getHeader(ctx, Header.TOKEN)
const authCookie = const authCookie =
getCookie<SessionCookie>(ctx, Cookie.Auth) || getCookie<SessionCookie>(ctx, Cookie.Auth) ||
openJwt<SessionCookie>(headerToken) openJwt<SessionCookie>(headerToken)
let apiKey = ctx.request.headers[Header.API_KEY] let apiKey = getHeader(ctx, Header.API_KEY)
if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) { if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) {
apiKey = ctx.request.headers[Header.AUTHORIZATION].split(" ")[1] apiKey = ctx.request.headers[Header.AUTHORIZATION].split(" ")[1]
} }
const tenantId = ctx.request.headers[Header.TENANT_ID] const tenantId = getHeader(ctx, Header.TENANT_ID)
let authenticated: boolean = false, let authenticated: boolean = false,
user: User | { tenantId: string } | undefined = undefined, user: User | { tenantId: string } | undefined = undefined,
internal: boolean = false, internal: boolean = false,
@ -243,5 +252,5 @@ export default function (
ctx.throw(err.status || 403, err) ctx.throw(err.status || 403, err)
} }
} }
} }) as Middleware
} }

View File

@ -1,6 +1,7 @@
import { Header } from "../constants" import { Header } from "../constants"
import { buildMatcherRegex, matches } from "./matchers" import { buildMatcherRegex, matches } from "./matchers"
import { Ctx, EndpointMatcher } from "@budibase/types" import { Ctx, EndpointMatcher } from "@budibase/types"
import type { Middleware, Next } from "koa"
/** /**
* GET, HEAD and OPTIONS methods are considered safe operations * GET, HEAD and OPTIONS methods are considered safe operations
@ -36,7 +37,7 @@ export default function (
opts: { noCsrfPatterns: EndpointMatcher[] } = { noCsrfPatterns: [] } opts: { noCsrfPatterns: EndpointMatcher[] } = { noCsrfPatterns: [] }
) { ) {
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns) const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
return async (ctx: Ctx, next: any) => { return (async (ctx: Ctx, next: Next) => {
// don't apply for excluded paths // don't apply for excluded paths
const found = matches(ctx, noCsrfOptions) const found = matches(ctx, noCsrfOptions)
if (found) { if (found) {
@ -77,5 +78,5 @@ export default function (
} }
return next() return next()
} }) as Middleware
} }

View File

@ -8,6 +8,7 @@ import {
GetTenantIdOptions, GetTenantIdOptions,
TenantResolutionStrategy, TenantResolutionStrategy,
} from "@budibase/types" } from "@budibase/types"
import type { Next, Middleware } from "koa"
export default function ( export default function (
allowQueryStringPatterns: EndpointMatcher[], allowQueryStringPatterns: EndpointMatcher[],
@ -17,7 +18,7 @@ export default function (
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
return async function (ctx: Ctx, next: any) { return async function (ctx: Ctx, next: Next) {
const allowNoTenant = const allowNoTenant =
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
const tenantOpts: GetTenantIdOptions = { const tenantOpts: GetTenantIdOptions = {
@ -32,5 +33,5 @@ export default function (
const tenantId = getTenantIDFromCtx(ctx, tenantOpts) const tenantId = getTenantIDFromCtx(ctx, tenantOpts)
ctx.set(Header.TENANT_ID, tenantId as string) ctx.set(Header.TENANT_ID, tenantId as string)
return doInTenant(tenantId, next) return doInTenant(tenantId, next)
} } as Middleware
} }

View File

@ -1,5 +1,5 @@
<script> <script>
import { Heading, Body, Layout, Button, Modal } from "@budibase/bbui" import { Heading, Body, Layout, Button, Modal, Icon } from "@budibase/bbui"
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte" import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte" import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
@ -12,11 +12,13 @@
automationStore, automationStore,
selectedAutomation, selectedAutomation,
} from "stores/builder" } from "stores/builder"
import { createLocalStorageStore } from "@budibase/frontend-core"
import { fly } from "svelte/transition"
$: automationId = $selectedAutomation?.data?._id $: automationId = $selectedAutomation?.data?._id
$: builderStore.selectResource(automationId) $: builderStore.selectResource(automationId)
// Keep URL and state in sync for selected screen ID const surveyDismissed = createLocalStorageStore("automation-survey", false)
const stopSyncing = syncURLToState({ const stopSyncing = syncURLToState({
urlParam: "automationId", urlParam: "automationId",
stateKey: "selectedAutomationId", stateKey: "selectedAutomationId",
@ -29,9 +31,11 @@
let modal let modal
let webhookModal let webhookModal
let mounted = false
onMount(() => { onMount(() => {
$automationStore.showTestPanel = false $automationStore.showTestPanel = false
mounted = true
}) })
onDestroy(stopSyncing) onDestroy(stopSyncing)
@ -79,6 +83,43 @@
</Modal> </Modal>
</div> </div>
{#if !$surveyDismissed && mounted}
<div
class="survey"
in:fly={{ x: 600, duration: 260, delay: 1000 }}
out:fly={{ x: 600, duration: 260 }}
>
<div class="survey__body">
<div class="survey__title">We value your feedback!</div>
<div class="survey__text">
<a
href="https://t.maze.co/310149185"
target="_blank"
rel="noopener noreferrer"
on:click={() => surveyDismissed.set(true)}
>
Complete our survey on Automations</a
>
and receive a $20 thank-you gift.
<a
href="https://drive.google.com/file/d/12-qk_2F9g5PdbM6wuKoz2KkIyLI-feMX/view?usp=sharing"
target="_blank"
rel="noopener noreferrer"
>
Terms apply.
</a>
</div>
</div>
<Icon
name="Close"
hoverable
color="var(--spectrum-global-color-static-gray-300)"
hoverColor="var(--spectrum-global-color-static-gray-100)"
on:click={() => surveyDismissed.set(true)}
/>
</div>
{/if}
<style> <style>
.root { .root {
flex: 1 1 auto; flex: 1 1 auto;
@ -108,11 +149,9 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.main { .main {
width: 300px; width: 300px;
} }
.setup { .setup {
padding-top: 9px; padding-top: 9px;
border-left: var(--border-light); border-left: var(--border-light);
@ -125,4 +164,39 @@
grid-column: 3; grid-column: 3;
overflow: auto; overflow: auto;
} }
/* Survey */
.survey {
position: absolute;
bottom: 32px;
right: 32px;
background: var(--spectrum-semantic-positive-color-background);
display: flex;
flex-direction: row;
padding: var(--spacing-l) var(--spacing-xl);
border-radius: 4px;
gap: var(--spacing-xl);
}
.survey * {
color: var(--spectrum-global-color-static-gray-300);
white-space: nowrap;
}
.survey a {
text-decoration: underline;
transition: color 130ms ease-out;
}
.survey a:hover {
color: var(--spectrum-global-color-static-gray-100);
cursor: pointer;
}
.survey__body {
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.survey__title {
font-weight: 600;
font-size: 15px;
}
</style> </style>

View File

@ -312,9 +312,10 @@ export async function getExternalSchema(
if (!connector.getExternalSchema) { if (!connector.getExternalSchema) {
ctx.throw(400, "Datasource does not support exporting external schema") ctx.throw(400, "Datasource does not support exporting external schema")
} }
const response = await connector.getExternalSchema()
ctx.body = { try {
schema: response, ctx.body = { schema: await connector.getExternalSchema() }
} catch (e: any) {
ctx.throw(400, e.message)
} }
} }

View File

@ -16,7 +16,6 @@ import {
CountCalculationFieldMetadata, CountCalculationFieldMetadata,
CreateViewResponse, CreateViewResponse,
UpdateViewResponse, UpdateViewResponse,
DeleteViewResponse,
} from "@budibase/types" } from "@budibase/types"
import { builderSocket, gridSocket } from "../../../websockets" import { builderSocket, gridSocket } from "../../../websockets"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"

View File

@ -58,12 +58,9 @@ if (apiEnabled()) {
}) })
) )
.use(pro.licensing()) .use(pro.licensing())
// @ts-ignore
.use(currentApp) .use(currentApp)
.use(auth.auditLog) .use(auth.auditLog)
// @ts-ignore
.use(migrations) .use(migrations)
// @ts-ignore
.use(cleanup) .use(cleanup)
// authenticated routes // authenticated routes

View File

@ -588,3 +588,102 @@ if (descriptions.length) {
} }
) )
} }
const datasources = datasourceDescribe({
exclude: [DatabaseName.MONGODB, DatabaseName.SQS, DatabaseName.ORACLE],
})
if (datasources.length) {
describe.each(datasources)(
"$dbName",
({ config, dsProvider, isPostgres, isMySQL, isMariaDB }) => {
let datasource: Datasource
let client: Knex
beforeEach(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
client = ds.client!
})
describe("external export", () => {
let table: Table
beforeEach(async () => {
table = await config.api.table.save(
tableForDatasource(datasource, {
name: "simple",
primary: ["id"],
primaryDisplay: "name",
schema: {
id: {
name: "id",
autocolumn: true,
type: FieldType.NUMBER,
constraints: {
presence: false,
},
},
name: {
name: "name",
autocolumn: false,
type: FieldType.STRING,
constraints: {
presence: false,
},
},
},
})
)
})
it("should be able to export and reimport a schema", async () => {
let { schema } = await config.api.datasource.externalSchema(
datasource
)
if (isPostgres) {
// pg_dump 17 puts this config parameter into the dump but no DB < 17
// can load it. We're using postgres 16 in tests at the time of writing.
schema = schema.replace("SET transaction_timeout = 0;", "")
}
await config.api.table.destroy(table._id!, table._rev!)
if (isMySQL || isMariaDB) {
// MySQL/MariaDB clients don't let you run multiple queries in a
// single call. They also throw an error when given an empty query.
// The below handles both of these things.
for (let query of schema.split(";\n")) {
query = query.trim()
if (!query) {
continue
}
await client.raw(query)
}
} else {
await client.raw(schema)
}
await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
const tables = await config.api.table.fetch()
const newTable = tables.find(t => t.name === table.name)!
// This is only set on tables created through Budibase, we don't
// expect it to match after we import the table.
delete table.created
for (const field of Object.values(newTable.schema)) {
// Will differ per-database, not useful for this test.
delete field.externalType
}
expect(newTable).toEqual(table)
})
})
}
)
}

View File

@ -193,6 +193,34 @@ const SCHEMA: Integration = {
}, },
} }
interface MSSQLColumnDefinition {
TableName: string
ColumnName: string
DataType: string
MaxLength: number
IsNullable: boolean
IsIdentity: boolean
Precision: number
Scale: number
}
interface ColumnDefinitionMetadata {
usesMaxLength?: boolean
usesPrecision?: boolean
}
const COLUMN_DEFINITION_METADATA: Record<string, ColumnDefinitionMetadata> = {
DATETIME2: { usesMaxLength: true },
TIME: { usesMaxLength: true },
DATETIMEOFFSET: { usesMaxLength: true },
NCHAR: { usesMaxLength: true },
NVARCHAR: { usesMaxLength: true },
BINARY: { usesMaxLength: true },
VARBINARY: { usesMaxLength: true },
DECIMAL: { usesPrecision: true },
NUMERIC: { usesPrecision: true },
}
class SqlServerIntegration extends Sql implements DatasourcePlus { class SqlServerIntegration extends Sql implements DatasourcePlus {
private readonly config: MSSQLConfig private readonly config: MSSQLConfig
private index: number = 0 private index: number = 0
@ -527,20 +555,24 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
return this.queryWithReturning(json, queryFn, processFn) return this.queryWithReturning(json, queryFn, processFn)
} }
async getExternalSchema() { private async getColumnDefinitions(): Promise<MSSQLColumnDefinition[]> {
// Query to retrieve table schema // Query to retrieve table schema
const query = ` const query = `
SELECT SELECT
t.name AS TableName, t.name AS TableName,
c.name AS ColumnName, c.name AS ColumnName,
ty.name AS DataType, ty.name AS DataType,
ty.precision AS Precision,
ty.scale AS Scale,
c.max_length AS MaxLength, c.max_length AS MaxLength,
c.is_nullable AS IsNullable, c.is_nullable AS IsNullable,
c.is_identity AS IsIdentity c.is_identity AS IsIdentity
FROM FROM
sys.tables t sys.tables t
INNER JOIN sys.columns c ON t.object_id = c.object_id INNER JOIN sys.columns c ON t.object_id = c.object_id
INNER JOIN sys.types ty ON c.system_type_id = ty.system_type_id INNER JOIN sys.types ty
ON c.system_type_id = ty.system_type_id
AND c.user_type_id = ty.user_type_id
WHERE WHERE
t.is_ms_shipped = 0 t.is_ms_shipped = 0
ORDER BY ORDER BY
@ -553,17 +585,36 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
sql: query, sql: query,
}) })
return result.recordset as MSSQLColumnDefinition[]
}
private getDataType(columnDef: MSSQLColumnDefinition): string {
const { DataType, MaxLength, Precision, Scale } = columnDef
const { usesMaxLength = false, usesPrecision = false } =
COLUMN_DEFINITION_METADATA[DataType] || {}
let dataType = DataType
if (usesMaxLength) {
if (MaxLength === -1) {
dataType += `(MAX)`
} else {
dataType += `(${MaxLength})`
}
}
if (usesPrecision) {
dataType += `(${Precision}, ${Scale})`
}
return dataType
}
async getExternalSchema() {
const scriptParts = [] const scriptParts = []
const tables: any = {} const tables: any = {}
for (const row of result.recordset) { const columns = await this.getColumnDefinitions()
const { for (const row of columns) {
TableName, const { TableName, ColumnName, IsNullable, IsIdentity } = row
ColumnName,
DataType,
MaxLength,
IsNullable,
IsIdentity,
} = row
if (!tables[TableName]) { if (!tables[TableName]) {
tables[TableName] = { tables[TableName] = {
@ -571,9 +622,11 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
} }
} }
const columnDefinition = `${ColumnName} ${DataType}${ const nullable = IsNullable ? "NULL" : "NOT NULL"
MaxLength ? `(${MaxLength})` : "" const identity = IsIdentity ? "IDENTITY" : ""
}${IsNullable ? " NULL" : " NOT NULL"}` const columnDefinition = `[${ColumnName}] ${this.getDataType(
row
)} ${nullable} ${identity}`
tables[TableName].columns.push(columnDefinition) tables[TableName].columns.push(columnDefinition)

View File

@ -412,7 +412,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
async getExternalSchema() { async getExternalSchema() {
try { try {
const [databaseResult] = await this.internalQuery({ const [databaseResult] = await this.internalQuery({
sql: `SHOW CREATE DATABASE ${this.config.database}`, sql: `SHOW CREATE DATABASE IF NOT EXISTS \`${this.config.database}\``,
}) })
let dumpContent = [databaseResult["Create Database"]] let dumpContent = [databaseResult["Create Database"]]
@ -432,7 +432,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
dumpContent.push(createTableStatement) dumpContent.push(createTableStatement)
} }
return dumpContent.join("\n") return dumpContent.join(";\n") + ";"
} finally { } finally {
this.disconnect() this.disconnect()
} }

View File

@ -476,21 +476,15 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
this.config.password this.config.password
}" pg_dump --schema-only "${dumpCommandParts.join(" ")}"` }" pg_dump --schema-only "${dumpCommandParts.join(" ")}"`
return new Promise<string>((res, rej) => { return new Promise<string>((resolve, reject) => {
exec(dumpCommand, (error, stdout, stderr) => { exec(dumpCommand, (error, stdout, stderr) => {
if (error) { if (error || stderr) {
console.error(`Error generating dump: ${error.message}`) console.error(stderr)
rej(error.message) reject(new Error(stderr))
return return
} }
if (stderr) { resolve(stdout)
console.error(`pg_dump error: ${stderr}`)
rej(stderr)
return
}
res(stdout)
console.log("SQL dump generated successfully!") console.log("SQL dump generated successfully!")
}) })
}) })

View File

@ -149,6 +149,7 @@ export function datasourceDescribe(opts: DatasourceDescribeOpts) {
isMongodb: dbName === DatabaseName.MONGODB, isMongodb: dbName === DatabaseName.MONGODB,
isMSSQL: dbName === DatabaseName.SQL_SERVER, isMSSQL: dbName === DatabaseName.SQL_SERVER,
isOracle: dbName === DatabaseName.ORACLE, isOracle: dbName === DatabaseName.ORACLE,
isMariaDB: dbName === DatabaseName.MARIADB,
})) }))
} }
@ -158,19 +159,19 @@ function getDatasource(
return providers[sourceName]() return providers[sourceName]()
} }
export async function knexClient(ds: Datasource) { export async function knexClient(ds: Datasource, opts?: Knex.Config) {
switch (ds.source) { switch (ds.source) {
case SourceName.POSTGRES: { case SourceName.POSTGRES: {
return postgres.knexClient(ds) return postgres.knexClient(ds, opts)
} }
case SourceName.MYSQL: { case SourceName.MYSQL: {
return mysql.knexClient(ds) return mysql.knexClient(ds, opts)
} }
case SourceName.SQL_SERVER: { case SourceName.SQL_SERVER: {
return mssql.knexClient(ds) return mssql.knexClient(ds, opts)
} }
case SourceName.ORACLE: { case SourceName.ORACLE: {
return oracle.knexClient(ds) return oracle.knexClient(ds, opts)
} }
default: { default: {
throw new Error(`Unsupported source: ${ds.source}`) throw new Error(`Unsupported source: ${ds.source}`)

View File

@ -2,7 +2,7 @@ import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait } from "testcontainers" import { GenericContainer, Wait } from "testcontainers"
import { generator, testContainerUtils } from "@budibase/backend-core/tests" import { generator, testContainerUtils } from "@budibase/backend-core/tests"
import { startContainer } from "." import { startContainer } from "."
import knex from "knex" import knex, { Knex } from "knex"
import { MSSQL_IMAGE } from "./images" import { MSSQL_IMAGE } from "./images"
let ports: Promise<testContainerUtils.Port[]> let ports: Promise<testContainerUtils.Port[]>
@ -57,7 +57,7 @@ export async function getDatasource(): Promise<Datasource> {
return datasource return datasource
} }
export async function knexClient(ds: Datasource) { export async function knexClient(ds: Datasource, opts?: Knex.Config) {
if (!ds.config) { if (!ds.config) {
throw new Error("Datasource config is missing") throw new Error("Datasource config is missing")
} }
@ -68,5 +68,6 @@ export async function knexClient(ds: Datasource) {
return knex({ return knex({
client: "mssql", client: "mssql",
connection: ds.config, connection: ds.config,
...opts,
}) })
} }

View File

@ -3,7 +3,7 @@ import { GenericContainer, Wait } from "testcontainers"
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
import { generator, testContainerUtils } from "@budibase/backend-core/tests" import { generator, testContainerUtils } from "@budibase/backend-core/tests"
import { startContainer } from "." import { startContainer } from "."
import knex from "knex" import knex, { Knex } from "knex"
import { MYSQL_IMAGE } from "./images" import { MYSQL_IMAGE } from "./images"
let ports: Promise<testContainerUtils.Port[]> let ports: Promise<testContainerUtils.Port[]>
@ -63,7 +63,7 @@ export async function getDatasource(): Promise<Datasource> {
return datasource return datasource
} }
export async function knexClient(ds: Datasource) { export async function knexClient(ds: Datasource, opts?: Knex.Config) {
if (!ds.config) { if (!ds.config) {
throw new Error("Datasource config is missing") throw new Error("Datasource config is missing")
} }
@ -74,5 +74,6 @@ export async function knexClient(ds: Datasource) {
return knex({ return knex({
client: "mysql2", client: "mysql2",
connection: ds.config, connection: ds.config,
...opts,
}) })
} }

View File

@ -2,7 +2,7 @@ import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait } from "testcontainers" import { GenericContainer, Wait } from "testcontainers"
import { generator, testContainerUtils } from "@budibase/backend-core/tests" import { generator, testContainerUtils } from "@budibase/backend-core/tests"
import { startContainer } from "." import { startContainer } from "."
import knex from "knex" import knex, { Knex } from "knex"
let ports: Promise<testContainerUtils.Port[]> let ports: Promise<testContainerUtils.Port[]>
@ -58,7 +58,7 @@ export async function getDatasource(): Promise<Datasource> {
return datasource return datasource
} }
export async function knexClient(ds: Datasource) { export async function knexClient(ds: Datasource, opts?: Knex.Config) {
if (!ds.config) { if (!ds.config) {
throw new Error("Datasource config is missing") throw new Error("Datasource config is missing")
} }
@ -76,6 +76,7 @@ export async function knexClient(ds: Datasource) {
user: ds.config.user, user: ds.config.user,
password: ds.config.password, password: ds.config.password,
}, },
...opts,
}) })
return c return c

View File

@ -2,7 +2,7 @@ import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait } from "testcontainers" import { GenericContainer, Wait } from "testcontainers"
import { generator, testContainerUtils } from "@budibase/backend-core/tests" import { generator, testContainerUtils } from "@budibase/backend-core/tests"
import { startContainer } from "." import { startContainer } from "."
import knex from "knex" import knex, { Knex } from "knex"
import { POSTGRES_IMAGE } from "./images" import { POSTGRES_IMAGE } from "./images"
let ports: Promise<testContainerUtils.Port[]> let ports: Promise<testContainerUtils.Port[]>
@ -51,7 +51,10 @@ export async function getDatasource(): Promise<Datasource> {
return datasource return datasource
} }
export async function knexClient(ds: Datasource) { export async function knexClient(
ds: Datasource,
opts?: Knex.Config
): Promise<Knex> {
if (!ds.config) { if (!ds.config) {
throw new Error("Datasource config is missing") throw new Error("Datasource config is missing")
} }
@ -62,5 +65,6 @@ export async function knexClient(ds: Datasource) {
return knex({ return knex({
client: "pg", client: "pg",
connection: ds.config, connection: ds.config,
...opts,
}) })
} }

View File

@ -1,8 +1,9 @@
import { UserCtx } from "@budibase/types" import { UserCtx } from "@budibase/types"
import { checkMissingMigrations } from "../appMigrations" import { checkMissingMigrations } from "../appMigrations"
import env from "../environment" import env from "../environment"
import type { Middleware, Next } from "koa"
export default async (ctx: UserCtx, next: any) => { const middleware = (async (ctx: UserCtx, next: Next) => {
const { appId } = ctx const { appId } = ctx
// migrations can be disabled via environment variable if you // migrations can be disabled via environment variable if you
@ -16,4 +17,6 @@ export default async (ctx: UserCtx, next: any) => {
} }
return checkMissingMigrations(ctx, next, appId) return checkMissingMigrations(ctx, next, appId)
} }) as Middleware
export default middleware

View File

@ -1,8 +1,9 @@
import { Ctx } from "@budibase/types" import { Ctx } from "@budibase/types"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { tracer } from "dd-trace" import { tracer } from "dd-trace"
import type { Middleware, Next } from "koa"
export default async (ctx: Ctx, next: any) => { const middleware = (async (ctx: Ctx, next: Next) => {
const resp = await next() const resp = await next()
const current = context.getCurrentContext() const current = context.getCurrentContext()
@ -30,4 +31,6 @@ export default async (ctx: Ctx, next: any) => {
} }
return resp return resp
} }) as Middleware
export default middleware

View File

@ -13,8 +13,9 @@ import env from "../environment"
import { isWebhookEndpoint, isBrowser, isApiKey } from "./utils" import { isWebhookEndpoint, isBrowser, isApiKey } from "./utils"
import { UserCtx, ContextUser } from "@budibase/types" import { UserCtx, ContextUser } from "@budibase/types"
import tracer from "dd-trace" import tracer from "dd-trace"
import type { Middleware, Next } from "koa"
export default async (ctx: UserCtx, next: any) => { const middleware = (async (ctx: UserCtx, next: Next) => {
// try to get the appID from the request // try to get the appID from the request
let requestAppId = await utils.getAppIdFromCtx(ctx) let requestAppId = await utils.getAppIdFromCtx(ctx)
if (!requestAppId) { if (!requestAppId) {
@ -116,4 +117,6 @@ export default async (ctx: UserCtx, next: any) => {
return next() return next()
}) })
} }) as Middleware
export default middleware

View File

@ -3,6 +3,7 @@ import {
CreateDatasourceResponse, CreateDatasourceResponse,
Datasource, Datasource,
FetchDatasourceInfoResponse, FetchDatasourceInfoResponse,
FetchExternalSchemaResponse,
FieldType, FieldType,
RelationshipType, RelationshipType,
UpdateDatasourceRequest, UpdateDatasourceRequest,
@ -96,6 +97,19 @@ export class DatasourceAPI extends TestAPI {
) )
} }
externalSchema = async (
datasource: Datasource | string,
expectations?: Expectations
): Promise<FetchExternalSchemaResponse> => {
const id = typeof datasource === "string" ? datasource : datasource._id
return await this._get<FetchExternalSchemaResponse>(
`/api/datasources/${id}/schema/external`,
{
expectations,
}
)
}
addExistingRelationship = async ( addExistingRelationship = async (
{ {
one, one,

View File

@ -2,7 +2,7 @@ export enum TemplateType {
APP = "app", APP = "app",
} }
export interface Template { export interface TemplateMetadata {
background: string background: string
icon: string icon: string
category: string category: string
@ -14,7 +14,7 @@ export interface Template {
image: string image: string
} }
export type FetchTemplateResponse = Template[] export type FetchTemplateResponse = TemplateMetadata[]
export interface DownloadTemplateResponse { export interface DownloadTemplateResponse {
message: string message: string