Merge branch 'master' of github.com:Budibase/budibase into chore/api-typing-4
This commit is contained in:
commit
73c5b2f729
|
@ -200,6 +200,20 @@ jobs:
|
|||
|
||||
- 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
|
||||
env:
|
||||
DATASOURCE: ${{ matrix.datasource }}
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
"@types/semver": "7.3.7",
|
||||
"@types/tar-fs": "2.0.1",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/koa": "2.13.4",
|
||||
"chance": "1.1.8",
|
||||
"ioredis-mock": "8.9.0",
|
||||
"jest": "29.7.0",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
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
|
||||
return next()
|
||||
}
|
||||
}) as Middleware
|
||||
|
||||
export default middleware
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { ErrorCode, InvalidAPIKeyError } from "../errors"
|
||||
import tracer from "dd-trace"
|
||||
import type { Middleware, Next } from "koa"
|
||||
|
||||
const ONE_MINUTE = 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.
|
||||
* 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) : []
|
||||
return async (ctx: Ctx | any, next: any) => {
|
||||
return (async (ctx: Ctx, next: Next) => {
|
||||
let publicEndpoint = false
|
||||
const version = ctx.request.headers[Header.API_VER]
|
||||
const version = getHeader(ctx, Header.API_VER)
|
||||
// the path is not authenticated
|
||||
const found = matches(ctx, noAuthOptions)
|
||||
if (found) {
|
||||
|
@ -116,18 +125,18 @@ export default function (
|
|||
}
|
||||
try {
|
||||
// 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 =
|
||||
getCookie<SessionCookie>(ctx, Cookie.Auth) ||
|
||||
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]) {
|
||||
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,
|
||||
user: User | { tenantId: string } | undefined = undefined,
|
||||
internal: boolean = false,
|
||||
|
@ -243,5 +252,5 @@ export default function (
|
|||
ctx.throw(err.status || 403, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as Middleware
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Header } from "../constants"
|
||||
import { buildMatcherRegex, matches } from "./matchers"
|
||||
import { Ctx, EndpointMatcher } from "@budibase/types"
|
||||
import type { Middleware, Next } from "koa"
|
||||
|
||||
/**
|
||||
* GET, HEAD and OPTIONS methods are considered safe operations
|
||||
|
@ -36,7 +37,7 @@ export default function (
|
|||
opts: { noCsrfPatterns: EndpointMatcher[] } = { noCsrfPatterns: [] }
|
||||
) {
|
||||
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
|
||||
return async (ctx: Ctx, next: any) => {
|
||||
return (async (ctx: Ctx, next: Next) => {
|
||||
// don't apply for excluded paths
|
||||
const found = matches(ctx, noCsrfOptions)
|
||||
if (found) {
|
||||
|
@ -77,5 +78,5 @@ export default function (
|
|||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}) as Middleware
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
GetTenantIdOptions,
|
||||
TenantResolutionStrategy,
|
||||
} from "@budibase/types"
|
||||
import type { Next, Middleware } from "koa"
|
||||
|
||||
export default function (
|
||||
allowQueryStringPatterns: EndpointMatcher[],
|
||||
|
@ -17,7 +18,7 @@ export default function (
|
|||
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
|
||||
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
|
||||
|
||||
return async function (ctx: Ctx, next: any) {
|
||||
return async function (ctx: Ctx, next: Next) {
|
||||
const allowNoTenant =
|
||||
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
|
||||
const tenantOpts: GetTenantIdOptions = {
|
||||
|
@ -32,5 +33,5 @@ export default function (
|
|||
const tenantId = getTenantIDFromCtx(ctx, tenantOpts)
|
||||
ctx.set(Header.TENANT_ID, tenantId as string)
|
||||
return doInTenant(tenantId, next)
|
||||
}
|
||||
} as Middleware
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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 CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
|
@ -12,11 +12,13 @@
|
|||
automationStore,
|
||||
selectedAutomation,
|
||||
} from "stores/builder"
|
||||
import { createLocalStorageStore } from "@budibase/frontend-core"
|
||||
import { fly } from "svelte/transition"
|
||||
|
||||
$: automationId = $selectedAutomation?.data?._id
|
||||
$: builderStore.selectResource(automationId)
|
||||
|
||||
// Keep URL and state in sync for selected screen ID
|
||||
const surveyDismissed = createLocalStorageStore("automation-survey", false)
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "automationId",
|
||||
stateKey: "selectedAutomationId",
|
||||
|
@ -29,9 +31,11 @@
|
|||
|
||||
let modal
|
||||
let webhookModal
|
||||
let mounted = false
|
||||
|
||||
onMount(() => {
|
||||
$automationStore.showTestPanel = false
|
||||
mounted = true
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
|
@ -79,6 +83,43 @@
|
|||
</Modal>
|
||||
</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>
|
||||
.root {
|
||||
flex: 1 1 auto;
|
||||
|
@ -108,11 +149,9 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.setup {
|
||||
padding-top: 9px;
|
||||
border-left: var(--border-light);
|
||||
|
@ -125,4 +164,39 @@
|
|||
grid-column: 3;
|
||||
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>
|
||||
|
|
|
@ -312,9 +312,10 @@ export async function getExternalSchema(
|
|||
if (!connector.getExternalSchema) {
|
||||
ctx.throw(400, "Datasource does not support exporting external schema")
|
||||
}
|
||||
const response = await connector.getExternalSchema()
|
||||
|
||||
ctx.body = {
|
||||
schema: response,
|
||||
try {
|
||||
ctx.body = { schema: await connector.getExternalSchema() }
|
||||
} catch (e: any) {
|
||||
ctx.throw(400, e.message)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
CountCalculationFieldMetadata,
|
||||
CreateViewResponse,
|
||||
UpdateViewResponse,
|
||||
DeleteViewResponse,
|
||||
} from "@budibase/types"
|
||||
import { builderSocket, gridSocket } from "../../../websockets"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
|
|
@ -58,12 +58,9 @@ if (apiEnabled()) {
|
|||
})
|
||||
)
|
||||
.use(pro.licensing())
|
||||
// @ts-ignore
|
||||
.use(currentApp)
|
||||
.use(auth.auditLog)
|
||||
// @ts-ignore
|
||||
.use(migrations)
|
||||
// @ts-ignore
|
||||
.use(cleanup)
|
||||
|
||||
// authenticated routes
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
private readonly config: MSSQLConfig
|
||||
private index: number = 0
|
||||
|
@ -527,20 +555,24 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|||
return this.queryWithReturning(json, queryFn, processFn)
|
||||
}
|
||||
|
||||
async getExternalSchema() {
|
||||
private async getColumnDefinitions(): Promise<MSSQLColumnDefinition[]> {
|
||||
// Query to retrieve table schema
|
||||
const query = `
|
||||
SELECT
|
||||
t.name AS TableName,
|
||||
c.name AS ColumnName,
|
||||
ty.name AS DataType,
|
||||
ty.precision AS Precision,
|
||||
ty.scale AS Scale,
|
||||
c.max_length AS MaxLength,
|
||||
c.is_nullable AS IsNullable,
|
||||
c.is_identity AS IsIdentity
|
||||
FROM
|
||||
sys.tables t
|
||||
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
|
||||
t.is_ms_shipped = 0
|
||||
ORDER BY
|
||||
|
@ -553,17 +585,36 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|||
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 tables: any = {}
|
||||
for (const row of result.recordset) {
|
||||
const {
|
||||
TableName,
|
||||
ColumnName,
|
||||
DataType,
|
||||
MaxLength,
|
||||
IsNullable,
|
||||
IsIdentity,
|
||||
} = row
|
||||
const columns = await this.getColumnDefinitions()
|
||||
for (const row of columns) {
|
||||
const { TableName, ColumnName, IsNullable, IsIdentity } = row
|
||||
|
||||
if (!tables[TableName]) {
|
||||
tables[TableName] = {
|
||||
|
@ -571,9 +622,11 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|||
}
|
||||
}
|
||||
|
||||
const columnDefinition = `${ColumnName} ${DataType}${
|
||||
MaxLength ? `(${MaxLength})` : ""
|
||||
}${IsNullable ? " NULL" : " NOT NULL"}`
|
||||
const nullable = IsNullable ? "NULL" : "NOT NULL"
|
||||
const identity = IsIdentity ? "IDENTITY" : ""
|
||||
const columnDefinition = `[${ColumnName}] ${this.getDataType(
|
||||
row
|
||||
)} ${nullable} ${identity}`
|
||||
|
||||
tables[TableName].columns.push(columnDefinition)
|
||||
|
||||
|
|
|
@ -412,7 +412,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
|||
async getExternalSchema() {
|
||||
try {
|
||||
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"]]
|
||||
|
||||
|
@ -432,7 +432,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
|||
dumpContent.push(createTableStatement)
|
||||
}
|
||||
|
||||
return dumpContent.join("\n")
|
||||
return dumpContent.join(";\n") + ";"
|
||||
} finally {
|
||||
this.disconnect()
|
||||
}
|
||||
|
|
|
@ -476,21 +476,15 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|||
this.config.password
|
||||
}" pg_dump --schema-only "${dumpCommandParts.join(" ")}"`
|
||||
|
||||
return new Promise<string>((res, rej) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
exec(dumpCommand, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error generating dump: ${error.message}`)
|
||||
rej(error.message)
|
||||
if (error || stderr) {
|
||||
console.error(stderr)
|
||||
reject(new Error(stderr))
|
||||
return
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`pg_dump error: ${stderr}`)
|
||||
rej(stderr)
|
||||
return
|
||||
}
|
||||
|
||||
res(stdout)
|
||||
resolve(stdout)
|
||||
console.log("SQL dump generated successfully!")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -149,6 +149,7 @@ export function datasourceDescribe(opts: DatasourceDescribeOpts) {
|
|||
isMongodb: dbName === DatabaseName.MONGODB,
|
||||
isMSSQL: dbName === DatabaseName.SQL_SERVER,
|
||||
isOracle: dbName === DatabaseName.ORACLE,
|
||||
isMariaDB: dbName === DatabaseName.MARIADB,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -158,19 +159,19 @@ function getDatasource(
|
|||
return providers[sourceName]()
|
||||
}
|
||||
|
||||
export async function knexClient(ds: Datasource) {
|
||||
export async function knexClient(ds: Datasource, opts?: Knex.Config) {
|
||||
switch (ds.source) {
|
||||
case SourceName.POSTGRES: {
|
||||
return postgres.knexClient(ds)
|
||||
return postgres.knexClient(ds, opts)
|
||||
}
|
||||
case SourceName.MYSQL: {
|
||||
return mysql.knexClient(ds)
|
||||
return mysql.knexClient(ds, opts)
|
||||
}
|
||||
case SourceName.SQL_SERVER: {
|
||||
return mssql.knexClient(ds)
|
||||
return mssql.knexClient(ds, opts)
|
||||
}
|
||||
case SourceName.ORACLE: {
|
||||
return oracle.knexClient(ds)
|
||||
return oracle.knexClient(ds, opts)
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported source: ${ds.source}`)
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Datasource, SourceName } from "@budibase/types"
|
|||
import { GenericContainer, Wait } from "testcontainers"
|
||||
import { generator, testContainerUtils } from "@budibase/backend-core/tests"
|
||||
import { startContainer } from "."
|
||||
import knex from "knex"
|
||||
import knex, { Knex } from "knex"
|
||||
import { MSSQL_IMAGE } from "./images"
|
||||
|
||||
let ports: Promise<testContainerUtils.Port[]>
|
||||
|
@ -57,7 +57,7 @@ export async function getDatasource(): Promise<Datasource> {
|
|||
return datasource
|
||||
}
|
||||
|
||||
export async function knexClient(ds: Datasource) {
|
||||
export async function knexClient(ds: Datasource, opts?: Knex.Config) {
|
||||
if (!ds.config) {
|
||||
throw new Error("Datasource config is missing")
|
||||
}
|
||||
|
@ -68,5 +68,6 @@ export async function knexClient(ds: Datasource) {
|
|||
return knex({
|
||||
client: "mssql",
|
||||
connection: ds.config,
|
||||
...opts,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { GenericContainer, Wait } from "testcontainers"
|
|||
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
||||
import { generator, testContainerUtils } from "@budibase/backend-core/tests"
|
||||
import { startContainer } from "."
|
||||
import knex from "knex"
|
||||
import knex, { Knex } from "knex"
|
||||
import { MYSQL_IMAGE } from "./images"
|
||||
|
||||
let ports: Promise<testContainerUtils.Port[]>
|
||||
|
@ -63,7 +63,7 @@ export async function getDatasource(): Promise<Datasource> {
|
|||
return datasource
|
||||
}
|
||||
|
||||
export async function knexClient(ds: Datasource) {
|
||||
export async function knexClient(ds: Datasource, opts?: Knex.Config) {
|
||||
if (!ds.config) {
|
||||
throw new Error("Datasource config is missing")
|
||||
}
|
||||
|
@ -74,5 +74,6 @@ export async function knexClient(ds: Datasource) {
|
|||
return knex({
|
||||
client: "mysql2",
|
||||
connection: ds.config,
|
||||
...opts,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Datasource, SourceName } from "@budibase/types"
|
|||
import { GenericContainer, Wait } from "testcontainers"
|
||||
import { generator, testContainerUtils } from "@budibase/backend-core/tests"
|
||||
import { startContainer } from "."
|
||||
import knex from "knex"
|
||||
import knex, { Knex } from "knex"
|
||||
|
||||
let ports: Promise<testContainerUtils.Port[]>
|
||||
|
||||
|
@ -58,7 +58,7 @@ export async function getDatasource(): Promise<Datasource> {
|
|||
return datasource
|
||||
}
|
||||
|
||||
export async function knexClient(ds: Datasource) {
|
||||
export async function knexClient(ds: Datasource, opts?: Knex.Config) {
|
||||
if (!ds.config) {
|
||||
throw new Error("Datasource config is missing")
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ export async function knexClient(ds: Datasource) {
|
|||
user: ds.config.user,
|
||||
password: ds.config.password,
|
||||
},
|
||||
...opts,
|
||||
})
|
||||
|
||||
return c
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Datasource, SourceName } from "@budibase/types"
|
|||
import { GenericContainer, Wait } from "testcontainers"
|
||||
import { generator, testContainerUtils } from "@budibase/backend-core/tests"
|
||||
import { startContainer } from "."
|
||||
import knex from "knex"
|
||||
import knex, { Knex } from "knex"
|
||||
import { POSTGRES_IMAGE } from "./images"
|
||||
|
||||
let ports: Promise<testContainerUtils.Port[]>
|
||||
|
@ -51,7 +51,10 @@ export async function getDatasource(): Promise<Datasource> {
|
|||
return datasource
|
||||
}
|
||||
|
||||
export async function knexClient(ds: Datasource) {
|
||||
export async function knexClient(
|
||||
ds: Datasource,
|
||||
opts?: Knex.Config
|
||||
): Promise<Knex> {
|
||||
if (!ds.config) {
|
||||
throw new Error("Datasource config is missing")
|
||||
}
|
||||
|
@ -62,5 +65,6 @@ export async function knexClient(ds: Datasource) {
|
|||
return knex({
|
||||
client: "pg",
|
||||
connection: ds.config,
|
||||
...opts,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { UserCtx } from "@budibase/types"
|
||||
import { checkMissingMigrations } from "../appMigrations"
|
||||
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
|
||||
|
||||
// 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)
|
||||
}
|
||||
}) as Middleware
|
||||
|
||||
export default middleware
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Ctx } from "@budibase/types"
|
||||
import { context } from "@budibase/backend-core"
|
||||
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 current = context.getCurrentContext()
|
||||
|
@ -30,4 +31,6 @@ export default async (ctx: Ctx, next: any) => {
|
|||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
}) as Middleware
|
||||
|
||||
export default middleware
|
||||
|
|
|
@ -13,8 +13,9 @@ import env from "../environment"
|
|||
import { isWebhookEndpoint, isBrowser, isApiKey } from "./utils"
|
||||
import { UserCtx, ContextUser } from "@budibase/types"
|
||||
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
|
||||
let requestAppId = await utils.getAppIdFromCtx(ctx)
|
||||
if (!requestAppId) {
|
||||
|
@ -116,4 +117,6 @@ export default async (ctx: UserCtx, next: any) => {
|
|||
|
||||
return next()
|
||||
})
|
||||
}
|
||||
}) as Middleware
|
||||
|
||||
export default middleware
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
CreateDatasourceResponse,
|
||||
Datasource,
|
||||
FetchDatasourceInfoResponse,
|
||||
FetchExternalSchemaResponse,
|
||||
FieldType,
|
||||
RelationshipType,
|
||||
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 (
|
||||
{
|
||||
one,
|
||||
|
|
|
@ -2,7 +2,7 @@ export enum TemplateType {
|
|||
APP = "app",
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
export interface TemplateMetadata {
|
||||
background: string
|
||||
icon: string
|
||||
category: string
|
||||
|
@ -14,7 +14,7 @@ export interface Template {
|
|||
image: string
|
||||
}
|
||||
|
||||
export type FetchTemplateResponse = Template[]
|
||||
export type FetchTemplateResponse = TemplateMetadata[]
|
||||
|
||||
export interface DownloadTemplateResponse {
|
||||
message: string
|
||||
|
|
Loading…
Reference in New Issue