mirror of
https://github.com/beekeeper-studio/beekeeper-studio.git
synced 2026-03-13 10:12:54 +08:00
Merge branch 'master' into arm-hosted
# Conflicts: # .github/workflows/studio-build-non-production.yml # .github/workflows/studio-publish.yml
This commit is contained in:
@@ -87,20 +87,24 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
id: cache-nm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
apps/studio/node_modules
|
||||
apps/ui-kit/node_modules
|
||||
key: node-modules-${{ matrix.os }}-${{ hashFiles('yarn.lock') }}
|
||||
apps/sqltools/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
- name: Clean cache
|
||||
if: steps.cache-nm.outputs.cache-hit != 'true'
|
||||
run: yarn cache clean
|
||||
|
||||
- name: Check dependencies
|
||||
if: steps.cache-nm.outputs.cache-hit != 'true'
|
||||
run: "yarn install --frozen-lockfile --network-timeout 100000"
|
||||
env:
|
||||
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}
|
||||
|
||||
26
.github/workflows/studio-publish.yml
vendored
26
.github/workflows/studio-publish.yml
vendored
@@ -214,28 +214,32 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v3
|
||||
if: matrix.os.type == 'linux'
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
id: cache-nm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
apps/studio/node_modules
|
||||
apps/ui-kit/node_modules
|
||||
key: node-modules-${{ matrix.os.name }}-${{ hashFiles('yarn.lock') }}
|
||||
apps/sqltools/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v3
|
||||
if: matrix.os.type == 'linux'
|
||||
|
||||
- name: Clean cache
|
||||
if: steps.cache-nm.outputs.cache-hit != 'true'
|
||||
run: yarn cache clean --all
|
||||
|
||||
# FIXME (matthew) Windows needs retries. It sometimes fails to build
|
||||
# the native oracledb package.
|
||||
# But only sometimes. I cannot figure out why.
|
||||
# Someone should at some point.
|
||||
- name: yarn install (with retry)
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
if: steps.cache-nm.outputs.cache-hit != 'true'
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 20
|
||||
@@ -351,8 +355,8 @@ jobs:
|
||||
|
||||
- name: Cleanup artifacts Win
|
||||
if: startsWith(matrix.os.name, 'windows')
|
||||
run: npx rimraf "apps/studio/dist_electron/!(*.exe|*.yml)"
|
||||
continue-on-error: true
|
||||
shell: powershell
|
||||
run: Get-ChildItem apps/studio/dist_electron -File | Where-Object { $_.Extension -notin '.exe','.yml' } | Remove-Item -Force
|
||||
|
||||
|
||||
finalize_mac_yml:
|
||||
@@ -435,7 +439,7 @@ jobs:
|
||||
- name: Move release snap from edge to stable
|
||||
continue-on-error: true
|
||||
run: |
|
||||
snapcraft promote beekeeper-studio --from-channel "edge" --to-channel "stable" --yes
|
||||
snapcraft promote beekeeper-studio --from-channel "latest/edge" --to-channel "latest/stable" --yes
|
||||
|
||||
publish_repositories:
|
||||
needs: [release, create_draft_release, identify_channel]
|
||||
|
||||
108
.github/workflows/studio-test.yml
vendored
108
.github/workflows/studio-test.yml
vendored
@@ -18,10 +18,10 @@ on:
|
||||
- apps/sqltools/**
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
prep:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
test-chunks: ${{ steps['set-test-chunks'].outputs['test-chunks'] }}
|
||||
test-chunks: ${{ steps.set-test-chunks.outputs.test-chunks }}
|
||||
steps:
|
||||
- name: 'Setup jq'
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
@@ -29,27 +29,62 @@ jobs:
|
||||
version: "1.7"
|
||||
force: true
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- id: set-test-chunks
|
||||
name: Set Chunks
|
||||
run: echo "::set-output name=test-chunks::$(./bin/get-db-files-as-json.sh)"
|
||||
unit:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v2
|
||||
run: echo "test-chunks=$(./bin/get-db-files-as-json.sh)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache-nm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
apps/studio/node_modules
|
||||
apps/ui-kit/node_modules
|
||||
apps/sqltools/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
if: steps.cache-nm.outputs.cache-hit != 'true'
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 20
|
||||
max_attempts: 3
|
||||
command: yarn install --frozen-lockfile --network-timeout 100000
|
||||
env:
|
||||
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}
|
||||
HUSKY: "0"
|
||||
npm_config_node_gyp: ${{ github.workspace }}/node_modules/node-gyp/bin/node-gyp.js
|
||||
|
||||
unit:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [prep]
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
apps/studio/node_modules
|
||||
apps/ui-kit/node_modules
|
||||
apps/sqltools/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Lint
|
||||
run: yarn workspace beekeeper-studio run lint
|
||||
@@ -80,20 +115,30 @@ jobs:
|
||||
name: 🥞 ${{ matrix.chunk[0] }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- setup
|
||||
- prep
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
chunk: ${{ fromJson(needs.setup.outputs['test-chunks']) }}
|
||||
chunk: ${{ fromJson(needs.prep.outputs['test-chunks']) }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
- name: Restore node_modules cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
apps/studio/node_modules
|
||||
apps/ui-kit/node_modules
|
||||
apps/sqltools/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Install libaio (for oracle)
|
||||
run: sudo apt install libaio-dev
|
||||
@@ -101,15 +146,6 @@ jobs:
|
||||
- name: Symlink libaio v1 (for oracle)
|
||||
run: sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1
|
||||
|
||||
- name: yarn install (with retry)
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 20
|
||||
max_attempts: 3
|
||||
command: "yarn install --frozen-lockfile --network-timeout 100000"
|
||||
env:
|
||||
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}
|
||||
|
||||
- name: Test
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
@@ -147,21 +183,27 @@ jobs:
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [prep]
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
env:
|
||||
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}
|
||||
- name: Restore node_modules cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
apps/studio/node_modules
|
||||
apps/ui-kit/node_modules
|
||||
apps/sqltools/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Start postgres container
|
||||
run: docker compose up psql15 -d
|
||||
|
||||
@@ -36,7 +36,9 @@ test.describe("Using the context menu", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await electronApp.close();
|
||||
if (electronApp) {
|
||||
await electronApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("paste a query using context menu", async () => {
|
||||
|
||||
@@ -39,7 +39,9 @@ test.describe("Copy Results Verifications", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await electronApp.close();
|
||||
if (electronApp) {
|
||||
await electronApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("copy as TSV / Excel", async () => {
|
||||
|
||||
@@ -40,7 +40,9 @@ test.describe("Export Results Verifications", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await electronApp.close();
|
||||
if (electronApp) {
|
||||
await electronApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("downloads as CSV", async () => {
|
||||
|
||||
@@ -39,7 +39,9 @@ test.describe("JSON Sidebar Verifications", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await electronApp.close();
|
||||
if (electronApp) {
|
||||
await electronApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.skip("accessing the JSON sidebar", async () => {
|
||||
|
||||
@@ -22,7 +22,9 @@ test.describe('New Connection Tests', () => {
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await electronApp.close();
|
||||
if (electronApp) {
|
||||
await electronApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Test a Postgres connection', async () => {
|
||||
|
||||
@@ -23,7 +23,9 @@ test.describe("Postgres query execution", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await electronApp.close();
|
||||
if (electronApp) {
|
||||
await electronApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("perform a Postgres query", async () => {
|
||||
|
||||
@@ -25,7 +25,9 @@ test.describe("Result Pane Verifications", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await electronApp.close();
|
||||
if (electronApp) {
|
||||
await electronApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("clicks on results columns", async () => {
|
||||
|
||||
@@ -30,6 +30,7 @@ test.describe("Table creation", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!electronApp) return;
|
||||
const dropTableQuery = `DROP TABLE ${newTableName};`
|
||||
const newQueryIndex = '1';
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ test.describe("Table creation", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!electronApp) return;
|
||||
const dropTableQuery = `DROP TABLE ${newTableName};`
|
||||
const newQueryIndex = '1';
|
||||
|
||||
|
||||
@@ -103,6 +103,11 @@ module.exports = {
|
||||
name: "SQL Server URL scheme",
|
||||
schemes: ["sqlserver", "microsoftsqlserver", "mssql"],
|
||||
role: "Editor"
|
||||
},
|
||||
{
|
||||
"name": "Redis URL scheme",
|
||||
"schemes": ["redis", "rediss"],
|
||||
"role": "Editor"
|
||||
}
|
||||
],
|
||||
mac: {
|
||||
|
||||
@@ -139,6 +139,11 @@ module.exports = {
|
||||
name: "SQL Server URL scheme",
|
||||
schemes: ["sqlserver", "microsoftsqlserver", "mssql"],
|
||||
role: "Editor"
|
||||
},
|
||||
{
|
||||
"name": "Redis URL scheme",
|
||||
"schemes": ["redis", "rediss"],
|
||||
"role": "Editor"
|
||||
}
|
||||
],
|
||||
mac: {
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"core-js": "^3",
|
||||
"dateformat": "^3.0.3",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"dompurify": "^3.2.4",
|
||||
"dompurify": "^3.3.2",
|
||||
"driver.js": "^1.3.6",
|
||||
"electron-devtools-installer": "^3.2.1",
|
||||
"electron-log": "^5.1.5",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import rawLog from "@bksLogger"
|
||||
import { readFileSync } from "fs"
|
||||
import { IDbConnectionDatabase } from "@/lib/db/types"
|
||||
import {
|
||||
Trino as TrinoNodeClient,
|
||||
BasicAuth,
|
||||
QueryResult,
|
||||
ConnectionOptions as TrinoConnectionOptions
|
||||
ConnectionOptions as TrinoConnectionOptions,
|
||||
SecureContextOptions
|
||||
} from 'trino-client'
|
||||
import {
|
||||
BaseQueryResult,
|
||||
@@ -41,6 +43,7 @@ import {
|
||||
createCancelablePromise,
|
||||
joinFilters
|
||||
} from "@/common/utils"
|
||||
import { buildSchemaFilter, escapeString } from "@/lib/db/clients/utils"
|
||||
import {
|
||||
AlterTableSpec,
|
||||
TableKey
|
||||
@@ -109,8 +112,30 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
|
||||
catalog: this.database.database
|
||||
}
|
||||
|
||||
// TODO: Add ssl using SecureContextOptions (https://trinodb.github.io/trino-js-client/types/ConnectionOptions.html)
|
||||
|
||||
if (this.server.config.ssl) {
|
||||
const sslOptions: SecureContextOptions = {}
|
||||
|
||||
if (this.server.config.sslCaFile) {
|
||||
sslOptions.ca = readFileSync(this.server.config.sslCaFile)
|
||||
}
|
||||
|
||||
if (this.server.config.sslCertFile) {
|
||||
sslOptions.cert = readFileSync(this.server.config.sslCertFile)
|
||||
}
|
||||
|
||||
if (this.server.config.sslKeyFile) {
|
||||
sslOptions.key = readFileSync(this.server.config.sslKeyFile)
|
||||
}
|
||||
|
||||
if (!sslOptions.key && !sslOptions.ca && !sslOptions.cert) {
|
||||
sslOptions.rejectUnauthorized = false
|
||||
} else {
|
||||
sslOptions.rejectUnauthorized = this.server.config.sslRejectUnauthorized
|
||||
}
|
||||
|
||||
connectionObj.ssl = sslOptions
|
||||
}
|
||||
|
||||
if ((this.server.config.user != null && this.server.config.user !== '') || (this.server.config.password != null && this.server.config.password !== '')) {
|
||||
connectionObj.auth = new BasicAuth(this.server.config.user, this.server.config.password)
|
||||
}
|
||||
@@ -271,7 +296,7 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
|
||||
|
||||
async listSchemas(filter: SchemaFilterOptions): Promise<string[]> {
|
||||
log.info('filters in listSchemas', filter)
|
||||
const sql = `show schemas from ${this.db}`
|
||||
const sql = `show schemas from ${this.wrapIdentifier(this.db)}`
|
||||
const result = await this.driverExecuteSingle(sql)
|
||||
|
||||
return result?.rows ? result.rows.map((row) => row.Schema) : []
|
||||
@@ -279,8 +304,9 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
|
||||
|
||||
async listTables(filter?: FilterOptions): Promise<TableOrView[]> {
|
||||
log.info('filters in listTables', filter)
|
||||
if (!filter) return []
|
||||
const sql = `select * from ${this.db}.information_schema.tables`
|
||||
const schemaFilter = buildSchemaFilter(filter, 'table_schema')
|
||||
const whereClause = schemaFilter ? `WHERE ${schemaFilter}` : ''
|
||||
const sql = `select * from ${this.wrapIdentifier(this.db)}.information_schema.tables ${whereClause}`
|
||||
const result = await this.driverExecuteSingle(sql)
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
@@ -294,9 +320,9 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
|
||||
const sql = `
|
||||
SELECT
|
||||
*
|
||||
FROM ${this.db}.information_schema.columns
|
||||
WHERE table_schema = '${schema}'
|
||||
AND table_name = '${table}'
|
||||
FROM ${this.wrapIdentifier(this.db)}.information_schema.columns
|
||||
WHERE table_schema = '${escapeString(schema)}'
|
||||
AND table_name = '${escapeString(table)}'
|
||||
ORDER BY ordinal_position
|
||||
`
|
||||
const result = await this.driverExecuteSingle(sql)
|
||||
@@ -688,7 +714,7 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
|
||||
SELECT
|
||||
${wrappedSelects},
|
||||
ROW_NUMBER() OVER (${rowNumberOrderClause}) AS rownum
|
||||
FROM ${this.db}.${tableRef}
|
||||
FROM ${this.wrapIdentifier(this.db)}.${tableRef}
|
||||
${filter}
|
||||
)
|
||||
SELECT *
|
||||
|
||||
@@ -398,6 +398,10 @@ export class SavedConnection extends DbConnectionBase implements IConnection {
|
||||
this.ssl = true
|
||||
}
|
||||
|
||||
if (cleanedUrl.startsWith('https://')) {
|
||||
this.ssl = true
|
||||
}
|
||||
|
||||
if (parsed.params?.TrustServerCertificate && parsed.params.TrustServerCertificate === 'true') {
|
||||
this.trustServerCertificate = true
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<template>
|
||||
<div class="trino-form">
|
||||
<div class="alert alert-warning">
|
||||
<i class="material-icons">warning</i>
|
||||
<span>
|
||||
Trino support is in alpha.
|
||||
<a href="https://docs.beekeeperstudio.io/user_guide/connecting/trino">Supported features</a>,
|
||||
<a href="https://github.com/beekeeper-studio/beekeeper-studio/issues/new/choose">report an issue</a>.
|
||||
</span>
|
||||
</div>
|
||||
<div class="with-connection-type">
|
||||
<common-server-inputs :config="config" />
|
||||
</div>
|
||||
|
||||
@@ -215,7 +215,6 @@ export const CLIENTS: ClientConfig[] = [
|
||||
topLevelEntity: 'Catalog',
|
||||
defaultPort: 8080,
|
||||
disabledFeatures: [
|
||||
'server:ssl',
|
||||
'server:socketPath',
|
||||
'cancelQuery', // TODO how to do this?
|
||||
],
|
||||
|
||||
@@ -48,17 +48,17 @@ export function buildSchemaFilter(filter, schemaField = 'schema_name') {
|
||||
const { schema, only, ignore } = filter
|
||||
|
||||
if (schema) {
|
||||
return `${schemaField} = '${schema}'`;
|
||||
return `${schemaField} = '${escapeString(schema)}'`;
|
||||
}
|
||||
|
||||
const where = [];
|
||||
|
||||
if (only && only.length) {
|
||||
where.push(`${schemaField} IN (${only.map((name) => `'${name}'`).join(',')})`);
|
||||
where.push(`${schemaField} IN (${only.map((name) => `'${escapeString(name)}'`).join(',')})`);
|
||||
}
|
||||
|
||||
if (ignore && ignore.length) {
|
||||
where.push(`${schemaField} NOT IN (${ignore.map((name) => `'${name}'`).join(',')})`);
|
||||
where.push(`${schemaField} NOT IN (${ignore.map((name) => `'${escapeString(name)}'`).join(',')})`);
|
||||
}
|
||||
|
||||
return where.join(' AND ');
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
components: {}
|
||||
install: function (Vue, opts) {
|
||||
this.components = opts
|
||||
}
|
||||
}
|
||||
@@ -1,289 +1,262 @@
|
||||
import { Network} from "testcontainers"
|
||||
import { DBTestUtil, dbtimeout } from "../../../../lib/db"
|
||||
import { PostgresTestDriver, TrinoTestDriver } from './trino/container'
|
||||
import { TrinoBackingPostgresDriver, TrinoHttpDriver, TrinoNginxProxy } from './trino/container'
|
||||
import { TableOrView } from "@/lib/db/models"
|
||||
// import { runCommonTests, runReadOnlyTests } from "./all"
|
||||
|
||||
const TEST_VERSIONS = [
|
||||
{ version: 'latest', socket: false, readonly: false }
|
||||
] as const
|
||||
describe('Trino integration tests', () => {
|
||||
jest.setTimeout(dbtimeout)
|
||||
|
||||
type TestVersion = typeof TEST_VERSIONS[number]['version']
|
||||
let dbfeeder: DBTestUtil
|
||||
let httpUtil: DBTestUtil
|
||||
let httpsUtil: DBTestUtil
|
||||
|
||||
function testWith(dockerTag: TestVersion, socket = false, readonly = false) {
|
||||
describe(`Trino [${dockerTag} - socket? ${socket} - database read-only mode? ${readonly}]`, () => {
|
||||
jest.setTimeout(dbtimeout)
|
||||
beforeAll(async () => {
|
||||
const network = await new Network().start()
|
||||
|
||||
let dbfeeder: DBTestUtil
|
||||
let util: DBTestUtil
|
||||
// Start backing Postgres and seed test data
|
||||
await TrinoBackingPostgresDriver.start('latest', false, false, network)
|
||||
dbfeeder = new DBTestUtil(TrinoBackingPostgresDriver.config, "banana", TrinoBackingPostgresDriver.utilOptions)
|
||||
await dbfeeder.setupdb()
|
||||
|
||||
beforeAll(async () => {
|
||||
const network = await new Network().start()
|
||||
// Start Trino (HTTP only, on the docker network)
|
||||
await TrinoHttpDriver.start('latest', false, network)
|
||||
|
||||
await PostgresTestDriver.start(dockerTag, socket, readonly, network)
|
||||
// Direct HTTP connection to Trino
|
||||
httpUtil = new DBTestUtil(TrinoHttpDriver.config, 'postgresql', { dialect: 'trino' })
|
||||
await httpUtil.connection.connect()
|
||||
|
||||
dbfeeder = new DBTestUtil(PostgresTestDriver.config, "banana", PostgresTestDriver.utilOptions)
|
||||
await dbfeeder.setupdb()
|
||||
// Start nginx proxy for SSL termination
|
||||
await TrinoNginxProxy.start(network)
|
||||
|
||||
// now set up the trino container
|
||||
await TrinoTestDriver.start(dockerTag, readonly, network)
|
||||
// HTTPS connection (through nginx TLS termination)
|
||||
httpsUtil = new DBTestUtil(TrinoNginxProxy.httpsConfig, 'postgresql', { dialect: 'trino' })
|
||||
await httpsUtil.connection.connect()
|
||||
})
|
||||
|
||||
util = new DBTestUtil(TrinoTestDriver.config, 'postgresql', { dialect: 'trino' })
|
||||
await util.connection.connect()
|
||||
afterAll(async () => {
|
||||
if (httpsUtil?.connection) await httpsUtil.connection.disconnect()
|
||||
if (httpUtil?.connection) await httpUtil.connection.disconnect()
|
||||
await TrinoNginxProxy.stop()
|
||||
await TrinoHttpDriver.stop()
|
||||
await dbfeeder.disconnect()
|
||||
await TrinoBackingPostgresDriver.stop()
|
||||
})
|
||||
|
||||
// Trino uses catalogs instead of databases, access PostgreSQL through 'postgresql' catalog
|
||||
// util = new DBTestUtil(TrinoTestDriver.config, "postgresql", TrinoTestDriver.utilOptions)
|
||||
describe("Read Operations (HTTP)", () => {
|
||||
it("List tables should work", async () => {
|
||||
const tables: TableOrView[] = await httpUtil.connection.listTables({ schema: 'public' })
|
||||
const tableNames: string[] = tables.map((t) => t.name)
|
||||
|
||||
// await util.setupdb()
|
||||
expect(tables.length).toBeGreaterThanOrEqual(3)
|
||||
expect(tableNames).toContain('people')
|
||||
expect(tableNames).toContain('addresses')
|
||||
expect(tableNames).toContain('jobs')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// await util.disconnect()
|
||||
await TrinoTestDriver.stop()
|
||||
await dbfeeder.disconnect()
|
||||
if (util.connection) {
|
||||
await util.connection.disconnect()
|
||||
it("List tables should return tables when filter is null (bug #3947)", async () => {
|
||||
const tables: TableOrView[] = await httpUtil.connection.listTables(null)
|
||||
expect(tables.length).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it("List tables should only return tables matching the schema filter", async () => {
|
||||
const tables: TableOrView[] = await httpUtil.connection.listTables({ schema: 'public' })
|
||||
tables.forEach(t => expect(t.schema).toBe('public'))
|
||||
})
|
||||
|
||||
it("List table columns should work", async () => {
|
||||
const columns = await httpUtil.connection.listTableColumns("addresses", "public")
|
||||
const columnNames = columns.map((c) => c.columnName)
|
||||
|
||||
expect(columns.length).toBeGreaterThan(0)
|
||||
expect(columnNames).toContain('street')
|
||||
expect(columnNames).toContain('city')
|
||||
expect(columnNames).toContain('country')
|
||||
|
||||
const streetColumn = columns.find(c => c.columnName === 'street')
|
||||
expect(streetColumn).toBeDefined()
|
||||
expect(streetColumn.schemaName).toBe('public')
|
||||
expect(streetColumn.tableName).toBe('addresses')
|
||||
})
|
||||
|
||||
it("Get database version should work", async () => {
|
||||
const version = await httpUtil.connection.versionString()
|
||||
expect(version).toBeDefined()
|
||||
expect(typeof version).toBe('string')
|
||||
expect(version.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("List schemas should work", async () => {
|
||||
const schemas = await httpUtil.connection.listSchemas()
|
||||
expect(schemas).toBeDefined()
|
||||
expect(Array.isArray(schemas)).toBe(true)
|
||||
if (schemas.length > 0) {
|
||||
expect(schemas).toContain('public')
|
||||
}
|
||||
})
|
||||
|
||||
describe("Read Operations", () => {
|
||||
it("List tables should work", async () => {
|
||||
const tables: TableOrView[] = await util.connection.listTables({ schema: 'public' })
|
||||
const tableNames: string[] = tables.map((t) => t.name)
|
||||
it("Should be able to retrieve data from a table", async () => {
|
||||
const data = await httpUtil.connection.selectTop('people', 0, 100, [], [], 'public', [])
|
||||
expect(data.result).toBeDefined()
|
||||
expect(Array.isArray(data.result)).toBe(true)
|
||||
expect(data.fields).toBeDefined()
|
||||
expect(data.fields.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
expect(tables.length).toBeGreaterThanOrEqual(3)
|
||||
expect(tableNames).toContain('people')
|
||||
expect(tableNames).toContain('addresses')
|
||||
expect(tableNames).toContain('jobs')
|
||||
})
|
||||
it("Should be able to filter columns", async () => {
|
||||
let r = await httpUtil.connection.selectTop("jobs", 0, 10, [], [], 'public', [])
|
||||
expect(r.result).toBeDefined()
|
||||
expect(r.fields).toBeDefined()
|
||||
|
||||
it("List tables should return empty array when filter is undefined", async () => {
|
||||
const tables: TableOrView[] = await util.connection.listTables()
|
||||
expect(tables).toEqual([])
|
||||
})
|
||||
if (r.result.length > 0) {
|
||||
const firstRow = r.result[0]
|
||||
expect(firstRow).toBeDefined()
|
||||
expect(typeof firstRow).toBe('object')
|
||||
}
|
||||
|
||||
it("List table columns should work", async () => {
|
||||
const columns = await util.connection.listTableColumns("addresses", "public")
|
||||
const columnNames = columns.map((c) => c.columnName)
|
||||
r = await httpUtil.connection.selectTop("jobs", 0, 10, [], [], 'public', ['title'])
|
||||
expect(r.result).toBeDefined()
|
||||
expect(r.fields).toBeDefined()
|
||||
})
|
||||
|
||||
expect(columns.length).toBeGreaterThan(0)
|
||||
expect(columnNames).toContain('street')
|
||||
expect(columnNames).toContain('city')
|
||||
expect(columnNames).toContain('country')
|
||||
it("Should handle sorting correctly", async () => {
|
||||
const result = await httpUtil.connection.selectTop('people', 0, 10, [], [], 'public', [])
|
||||
expect(result.result).toBeDefined()
|
||||
expect(result.fields).toBeDefined()
|
||||
expect(result.fields.length).toBeGreaterThan(0)
|
||||
expect(Array.isArray(result.result)).toBe(true)
|
||||
})
|
||||
|
||||
// Verify column properties
|
||||
const streetColumn = columns.find(c => c.columnName === 'street')
|
||||
expect(streetColumn).toBeDefined()
|
||||
expect(streetColumn.schemaName).toBe('public')
|
||||
expect(streetColumn.tableName).toBe('addresses')
|
||||
})
|
||||
it("Should handle pagination correctly", async () => {
|
||||
let result = await httpUtil.connection.selectTop('people', 0, 2, [], [], 'public', [])
|
||||
expect(result.result).toBeDefined()
|
||||
expect(Array.isArray(result.result)).toBe(true)
|
||||
expect(result.result.length).toBeLessThanOrEqual(2)
|
||||
|
||||
it("Get database version should work", async () => {
|
||||
const version = await util.connection.versionString()
|
||||
expect(version).toBeDefined()
|
||||
expect(typeof version).toBe('string')
|
||||
expect(version.length).toBeGreaterThan(0)
|
||||
})
|
||||
result = await httpUtil.connection.selectTop('people', 2, 2, [], [], 'public', [])
|
||||
expect(result.result).toBeDefined()
|
||||
expect(Array.isArray(result.result)).toBe(true)
|
||||
})
|
||||
|
||||
it("List schemas should work", async () => {
|
||||
const schemas = await util.connection.listSchemas()
|
||||
expect(schemas).toBeDefined()
|
||||
expect(Array.isArray(schemas)).toBe(true)
|
||||
// We know from our debugging that schemas work, so if we get an empty array
|
||||
// it's likely just a timing/implementation issue, but the method works
|
||||
if (schemas.length > 0) {
|
||||
expect(schemas).toContain('public')
|
||||
}
|
||||
})
|
||||
it("Should execute custom queries", async () => {
|
||||
const queryResult = await httpUtil.connection.query('SELECT COUNT(*) as table_count FROM postgresql.information_schema.tables WHERE table_schema = \'public\'')
|
||||
const results = await queryResult.execute()
|
||||
|
||||
it("Should be able to retrieve data from a table", async () => {
|
||||
const data = await util.connection.selectTop('people', 0, 100, [], [], 'public', [])
|
||||
expect(data.result).toBeDefined()
|
||||
expect(Array.isArray(data.result)).toBe(true)
|
||||
expect(data.fields).toBeDefined()
|
||||
expect(data.fields.length).toBeGreaterThan(0)
|
||||
})
|
||||
expect(results).toBeDefined()
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0]).toBeDefined()
|
||||
expect(results[0].rows).toBeDefined()
|
||||
})
|
||||
|
||||
it("Should be able to filter columns", async () => {
|
||||
// Get all columns
|
||||
let r = await util.connection.selectTop("jobs", 0, 10, [], [], 'public', [])
|
||||
expect(r.result).toBeDefined()
|
||||
expect(r.fields).toBeDefined()
|
||||
it("Should support Trino-specific features", async () => {
|
||||
const catalogsQuery = await httpUtil.connection.query('SHOW CATALOGS')
|
||||
const catalogsResult = await catalogsQuery.execute()
|
||||
|
||||
if (r.result.length > 0) {
|
||||
const firstRow = r.result[0]
|
||||
expect(firstRow).toBeDefined()
|
||||
expect(typeof firstRow).toBe('object')
|
||||
}
|
||||
expect(catalogsResult).toBeDefined()
|
||||
expect(catalogsResult.length).toBeGreaterThan(0)
|
||||
expect(catalogsResult[0].rows).toBeDefined()
|
||||
|
||||
// Get only specific column if we specify it
|
||||
r = await util.connection.selectTop("jobs", 0, 10, [], [], 'public', ['title'])
|
||||
expect(r.result).toBeDefined()
|
||||
expect(r.fields).toBeDefined()
|
||||
})
|
||||
const catalogNames = catalogsResult[0].rows.map(row => row.Catalog)
|
||||
expect(Array.isArray(catalogNames)).toBe(true)
|
||||
if (catalogNames.length > 0) {
|
||||
expect(catalogNames.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it("Should handle sorting correctly", async () => {
|
||||
// Test sorting functionality
|
||||
const result = await util.connection.selectTop('people', 0, 10, [], [], 'public', [])
|
||||
expect(result.result).toBeDefined()
|
||||
expect(result.fields).toBeDefined()
|
||||
expect(result.fields.length).toBeGreaterThan(0)
|
||||
it("Should handle complex queries with joins", async () => {
|
||||
const joinQuery = `
|
||||
SELECT COUNT(*) as count_result
|
||||
FROM postgresql.public.people p
|
||||
WHERE p.id IS NOT NULL
|
||||
LIMIT 5
|
||||
`
|
||||
|
||||
// The sorting functionality should work even if no data is returned
|
||||
expect(Array.isArray(result.result)).toBe(true)
|
||||
})
|
||||
const queryResult = await httpUtil.connection.query(joinQuery)
|
||||
const results = await queryResult.execute()
|
||||
|
||||
it("Should handle pagination correctly", async () => {
|
||||
// Page 1 (first 2 items)
|
||||
let result = await util.connection.selectTop('people', 0, 2, [], [], 'public', [])
|
||||
expect(result.result).toBeDefined()
|
||||
expect(Array.isArray(result.result)).toBe(true)
|
||||
expect(result.result.length).toBeLessThanOrEqual(2)
|
||||
|
||||
// Page 2 (next 2 items)
|
||||
result = await util.connection.selectTop('people', 2, 2, [], [], 'public', [])
|
||||
expect(result.result).toBeDefined()
|
||||
expect(Array.isArray(result.result)).toBe(true)
|
||||
})
|
||||
|
||||
it("Should execute custom queries", async () => {
|
||||
const queryResult = await util.connection.query('SELECT COUNT(*) as table_count FROM postgresql.information_schema.tables WHERE table_schema = \'public\'')
|
||||
const results = await queryResult.execute()
|
||||
|
||||
expect(results).toBeDefined()
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0]).toBeDefined()
|
||||
expect(results[0].rows).toBeDefined()
|
||||
})
|
||||
|
||||
it("Should support Trino-specific features", async () => {
|
||||
// Test SHOW CATALOGS - a Trino-specific command
|
||||
const catalogsQuery = await util.connection.query('SHOW CATALOGS')
|
||||
const catalogsResult = await catalogsQuery.execute()
|
||||
|
||||
expect(catalogsResult).toBeDefined()
|
||||
expect(catalogsResult.length).toBeGreaterThan(0)
|
||||
expect(catalogsResult[0].rows).toBeDefined()
|
||||
|
||||
// The catalogs should include at least system catalogs
|
||||
const catalogNames = catalogsResult[0].rows.map(row => row.Catalog)
|
||||
expect(Array.isArray(catalogNames)).toBe(true)
|
||||
// We know from debug that postgresql catalog should be there, but make test more flexible
|
||||
if (catalogNames.length > 0) {
|
||||
// Should have some catalogs (system, postgresql, etc.)
|
||||
expect(catalogNames.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it("Should handle complex queries with joins", async () => {
|
||||
// Use a simpler query that's more likely to work
|
||||
const joinQuery = `
|
||||
SELECT COUNT(*) as count_result
|
||||
FROM postgresql.public.people p
|
||||
WHERE p.id IS NOT NULL
|
||||
LIMIT 5
|
||||
`
|
||||
|
||||
const queryResult = await util.connection.query(joinQuery)
|
||||
const results = await queryResult.execute()
|
||||
|
||||
expect(results).toBeDefined()
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0].rows).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Trino-specific constraints and features", () => {
|
||||
it("Should indicate Trino doesn't support transactions", async () => {
|
||||
expect((util.connection as any).supportsTransaction).toBe(false)
|
||||
})
|
||||
|
||||
it("Should return null for primary keys (not supported)", async () => {
|
||||
const primaryKeys = await util.connection.getPrimaryKeys()
|
||||
expect(primaryKeys).toEqual([])
|
||||
|
||||
const singlePrimaryKey = await util.connection.getPrimaryKey('people', 'public')
|
||||
expect(singlePrimaryKey).toBeNull()
|
||||
})
|
||||
|
||||
it("Should return null for data modification operations", async () => {
|
||||
const alterResult = await util.connection.alterTable({} as any)
|
||||
expect(alterResult).toBeNull()
|
||||
|
||||
const createDbResult = await util.connection.createDatabase()
|
||||
expect(createDbResult).toBeNull()
|
||||
|
||||
const truncateResult = await util.connection.truncateElementSql('test', 'public')
|
||||
expect(truncateResult).toBeNull()
|
||||
|
||||
const duplicateResult = await util.connection.duplicateTable('test', 'public', 'test_copy')
|
||||
expect(duplicateResult).toBeNull()
|
||||
|
||||
const duplicateSqlResult = await util.connection.duplicateTableSql('test', 'public', 'test_copy')
|
||||
expect(duplicateSqlResult).toBeNull()
|
||||
|
||||
const setNameResult = await util.connection.setElementNameSql('test', 'test_new', 'public')
|
||||
expect(setNameResult).toBeNull()
|
||||
|
||||
const builderResult = await util.connection.getBuilder('test_table', 'public')
|
||||
expect(builderResult).toBeNull()
|
||||
})
|
||||
|
||||
it("Should support selectTopSql for query generation", async () => {
|
||||
const sql = await util.connection.selectTopSql('people', 0, 10,
|
||||
[], [], 'public', [])
|
||||
|
||||
expect(sql).toBeDefined()
|
||||
expect(typeof sql).toBe('string')
|
||||
expect(sql.length).toBeGreaterThan(0)
|
||||
expect(sql).toContain('SELECT')
|
||||
expect(sql).toContain('FROM')
|
||||
expect(sql).toContain('people')
|
||||
})
|
||||
|
||||
it("Should work with catalog.schema.table format", async () => {
|
||||
// Test that we can query using full Trino naming convention
|
||||
const queryResult = await util.connection.query('SELECT 1 as test_column FROM postgresql.public.people LIMIT 1')
|
||||
const results = await queryResult.execute()
|
||||
|
||||
expect(results).toBeDefined()
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0].rows).toBeDefined()
|
||||
expect(results[0].rows.length).toBeGreaterThan(0)
|
||||
})
|
||||
expect(results).toBeDefined()
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0].rows).toBeDefined()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
TEST_VERSIONS.forEach(({ version, socket, readonly }) => testWith(version, socket, readonly))
|
||||
describe("Trino-specific constraints and features", () => {
|
||||
it("Should indicate Trino doesn't support transactions", async () => {
|
||||
expect((httpUtil.connection as any).supportsTransaction).toBe(false)
|
||||
})
|
||||
|
||||
// Additional util.connection tests
|
||||
describe('Trino util.Connection Edge Cases', () => {
|
||||
jest.setTimeout(dbtimeout)
|
||||
it("Should return null for primary keys (not supported)", async () => {
|
||||
const primaryKeys = await httpUtil.connection.getPrimaryKeys()
|
||||
expect(primaryKeys).toEqual([])
|
||||
|
||||
it('should validate util.connection configuration', async () => {
|
||||
// Basic configuration validation test
|
||||
const config: IDbutil.ConnectionServerConfig = {
|
||||
client: 'trino',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
user: 'testuser',
|
||||
readOnlyMode: false,
|
||||
osUser: 'testuser',
|
||||
ssh: null,
|
||||
sslCaFile: null,
|
||||
sslCertFile: null,
|
||||
sslKeyFile: null,
|
||||
sslRejectUnauthorized: false,
|
||||
ssl: false,
|
||||
domain: null,
|
||||
socketPath: null,
|
||||
socketPathEnabled: false,
|
||||
password: null
|
||||
}
|
||||
const singlePrimaryKey = await httpUtil.connection.getPrimaryKey('people', 'public')
|
||||
expect(singlePrimaryKey).toBeNull()
|
||||
})
|
||||
|
||||
expect(config.client).toBe('trino')
|
||||
expect(config.port).toBe(8080)
|
||||
expect(config.readOnlyMode).toBe(false)
|
||||
it("Should return null for data modification operations", async () => {
|
||||
const alterResult = await httpUtil.connection.alterTable({} as any)
|
||||
expect(alterResult).toBeNull()
|
||||
|
||||
const createDbResult = await httpUtil.connection.createDatabase()
|
||||
expect(createDbResult).toBeNull()
|
||||
|
||||
const truncateResult = await httpUtil.connection.truncateElementSql('test', 'public')
|
||||
expect(truncateResult).toBeNull()
|
||||
|
||||
const duplicateResult = await httpUtil.connection.duplicateTable('test', 'public', 'test_copy')
|
||||
expect(duplicateResult).toBeNull()
|
||||
|
||||
const duplicateSqlResult = await httpUtil.connection.duplicateTableSql('test', 'public', 'test_copy')
|
||||
expect(duplicateSqlResult).toBeNull()
|
||||
|
||||
const setNameResult = await httpUtil.connection.setElementNameSql('test', 'test_new', 'public')
|
||||
expect(setNameResult).toBeNull()
|
||||
|
||||
const builderResult = await httpUtil.connection.getBuilder('test_table', 'public')
|
||||
expect(builderResult).toBeNull()
|
||||
})
|
||||
|
||||
it("Should support selectTopSql for query generation", async () => {
|
||||
const sql = await httpUtil.connection.selectTopSql('people', 0, 10,
|
||||
[], [], 'public', [])
|
||||
|
||||
expect(sql).toBeDefined()
|
||||
expect(typeof sql).toBe('string')
|
||||
expect(sql.length).toBeGreaterThan(0)
|
||||
expect(sql).toContain('SELECT')
|
||||
expect(sql).toContain('FROM')
|
||||
expect(sql).toContain('people')
|
||||
})
|
||||
|
||||
it("Should work with catalog.schema.table format", async () => {
|
||||
const queryResult = await httpUtil.connection.query('SELECT 1 as test_column FROM postgresql.public.people LIMIT 1')
|
||||
const results = await queryResult.execute()
|
||||
|
||||
expect(results).toBeDefined()
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0].rows).toBeDefined()
|
||||
expect(results[0].rows.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SSL connection via load balancer (bug #3695)', () => {
|
||||
it("Should connect to Trino over HTTPS with ssl=true and CA cert", async () => {
|
||||
const version = await httpsUtil.connection.versionString()
|
||||
expect(version).toBeDefined()
|
||||
expect(typeof version).toBe('string')
|
||||
expect(version.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("Should be able to list tables over SSL", async () => {
|
||||
const tables: TableOrView[] = await httpsUtil.connection.listTables({ schema: 'public' })
|
||||
const tableNames = tables.map(t => t.name)
|
||||
expect(tables.length).toBeGreaterThanOrEqual(3)
|
||||
expect(tableNames).toContain('people')
|
||||
})
|
||||
|
||||
it("Should be able to query data over SSL", async () => {
|
||||
const result = await httpsUtil.connection.selectTop('people', 0, 10, [], [], 'public', [])
|
||||
expect(result.result).toBeDefined()
|
||||
expect(Array.isArray(result.result)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { dbtimeout, Options } from "@tests/lib/db";
|
||||
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers";
|
||||
import { GenericContainer, Wait, StartedTestContainer, Network } from "testcontainers";
|
||||
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import fs from 'fs'
|
||||
import { execSync } from 'child_process'
|
||||
import { IDbConnectionServerConfig } from "@/lib/db/types";
|
||||
|
||||
export const PostgresTestDriver = {
|
||||
export const TrinoBackingPostgresDriver = {
|
||||
container: null,
|
||||
utilOptions: null,
|
||||
config: null,
|
||||
@@ -87,19 +88,19 @@ export const PostgresTestDriver = {
|
||||
}
|
||||
}
|
||||
|
||||
export const TrinoTestDriver = {
|
||||
export const TrinoHttpDriver = {
|
||||
container: null as StartedTestContainer | null,
|
||||
pgContainer: null as StartedTestContainer | null,
|
||||
utilOptions: null as Options | null,
|
||||
config: null as IDbConnectionServerConfig | null,
|
||||
async start(dockerTag: string, readonly: boolean, network) {
|
||||
const startupTimeout = dbtimeout * 2;
|
||||
|
||||
// Create a temporary directory for catalog configuration
|
||||
|
||||
// Create a temporary directory for catalog and config
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trino-'));
|
||||
const catalogDir = path.join(tempDir, 'catalog');
|
||||
fs.mkdirSync(catalogDir);
|
||||
|
||||
|
||||
// Create postgresql.properties file in the catalog directory
|
||||
const postgresConfig = `connector.name=postgresql
|
||||
connection-url=jdbc:postgresql://postgres:5432/banana
|
||||
@@ -109,14 +110,36 @@ connection-password=example`;
|
||||
const catalogFile = path.join(catalogDir, "postgresql.properties")
|
||||
fs.writeFileSync(catalogFile, postgresConfig)
|
||||
|
||||
// Create config.properties with process-forwarded enabled for load balancer support
|
||||
const configDir = path.join(tempDir, 'config');
|
||||
fs.mkdirSync(configDir);
|
||||
const configFile = path.join(configDir, 'config.properties');
|
||||
fs.writeFileSync(configFile, [
|
||||
'#single node install config',
|
||||
'coordinator=true',
|
||||
'node-scheduler.include-coordinator=true',
|
||||
'http-server.http.port=8080',
|
||||
'http-server.process-forwarded=true',
|
||||
'discovery.uri=http://localhost:8080',
|
||||
'catalog.management=${ENV:CATALOG_MANAGEMENT}',
|
||||
].join('\n'));
|
||||
|
||||
this.container = await new GenericContainer(`trinodb/trino:${dockerTag}`)
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases("trino")
|
||||
.withExposedPorts(8080)
|
||||
.withBindMounts([{
|
||||
source: catalogDir,
|
||||
target: '/etc/trino/catalog',
|
||||
mode: 'ro'
|
||||
}])
|
||||
.withBindMounts([
|
||||
{
|
||||
source: catalogDir,
|
||||
target: '/etc/trino/catalog',
|
||||
mode: 'ro'
|
||||
},
|
||||
{
|
||||
source: configFile,
|
||||
target: '/etc/trino/config.properties',
|
||||
mode: 'ro'
|
||||
}
|
||||
])
|
||||
.withWaitStrategy(Wait.forLogMessage("SERVER STARTED"))
|
||||
.withStartupTimeout(startupTimeout)
|
||||
.start()
|
||||
@@ -146,3 +169,117 @@ connection-password=example`;
|
||||
await this.container?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nginx reverse proxy in front of Trino, following Trino docs best practice
|
||||
* of using a load balancer to terminate TLS.
|
||||
*
|
||||
* Exposes two ports:
|
||||
* - 8080: HTTP passthrough to Trino
|
||||
* - 8443: HTTPS termination, proxied to Trino over HTTP
|
||||
*/
|
||||
export const TrinoNginxProxy = {
|
||||
container: null as StartedTestContainer | null,
|
||||
certFile: null as string | null,
|
||||
httpConfig: null as IDbConnectionServerConfig | null,
|
||||
httpsConfig: null as IDbConnectionServerConfig | null,
|
||||
|
||||
async start(network) {
|
||||
const startupTimeout = dbtimeout
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trino-nginx-'))
|
||||
const certDir = path.join(tempDir, 'certs')
|
||||
fs.mkdirSync(certDir)
|
||||
|
||||
// Generate self-signed cert for nginx
|
||||
const keyFile = path.join(certDir, 'server.key')
|
||||
const certFile = path.join(certDir, 'server.crt')
|
||||
execSync(
|
||||
`openssl req -x509 -newkey rsa:2048 -keyout ${keyFile} -out ${certFile} ` +
|
||||
`-days 1 -nodes -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"`,
|
||||
{ stdio: 'pipe' }
|
||||
)
|
||||
this.certFile = certFile
|
||||
|
||||
// Nginx config: HTTP on 8080, HTTPS on 8443, both proxy to trino:8080
|
||||
const nginxConf = path.join(tempDir, 'nginx.conf')
|
||||
fs.writeFileSync(nginxConf, String.raw`
|
||||
events { worker_connections 64; }
|
||||
http {
|
||||
server {
|
||||
listen 8080;
|
||||
location / {
|
||||
proxy_pass http://trino:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 8443 ssl;
|
||||
ssl_certificate /etc/nginx/certs/server.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/server.key;
|
||||
location / {
|
||||
proxy_pass http://trino:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
this.container = await new GenericContainer('nginx:alpine')
|
||||
.withNetwork(network)
|
||||
.withExposedPorts(8080, 8443)
|
||||
.withBindMounts([
|
||||
{ source: nginxConf, target: '/etc/nginx/nginx.conf', mode: 'ro' },
|
||||
{ source: certDir, target: '/etc/nginx/certs', mode: 'ro' },
|
||||
])
|
||||
.withWaitStrategy(Wait.forListeningPorts())
|
||||
.withStartupTimeout(startupTimeout)
|
||||
.start()
|
||||
|
||||
const baseConfig = {
|
||||
client: 'trino' as const,
|
||||
user: 'test',
|
||||
password: null,
|
||||
osUser: 'foo',
|
||||
ssh: null,
|
||||
sslCertFile: null,
|
||||
sslKeyFile: null,
|
||||
domain: null,
|
||||
socketPath: null,
|
||||
socketPathEnabled: false,
|
||||
readOnlyMode: false,
|
||||
}
|
||||
|
||||
this.httpConfig = {
|
||||
...baseConfig,
|
||||
host: this.container.getHost(),
|
||||
port: this.container.getMappedPort(8080),
|
||||
ssl: false,
|
||||
sslCaFile: null,
|
||||
sslRejectUnauthorized: false,
|
||||
}
|
||||
|
||||
this.httpsConfig = {
|
||||
...baseConfig,
|
||||
host: this.container.getHost(),
|
||||
port: this.container.getMappedPort(8443),
|
||||
ssl: true,
|
||||
sslCaFile: certFile,
|
||||
sslRejectUnauthorized: false,
|
||||
}
|
||||
},
|
||||
|
||||
async stop() {
|
||||
await this.container?.stop()
|
||||
},
|
||||
}
|
||||
|
||||
187
apps/studio/tests/unit/lib/db/clients/trino.spec.ts
Normal file
187
apps/studio/tests/unit/lib/db/clients/trino.spec.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
const capturedQueries: string[] = []
|
||||
|
||||
jest.mock('trino-client', () => {
|
||||
const mockQuery = jest.fn().mockImplementation((sql: string) => {
|
||||
capturedQueries.push(sql)
|
||||
return Promise.resolve({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
data: [['1.0.0']],
|
||||
columns: [{ name: '_col0', type: 'varchar' }]
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
Trino: {
|
||||
create: jest.fn().mockReturnValue({ query: mockQuery })
|
||||
},
|
||||
BasicAuth: jest.fn().mockImplementation((user, pass) => ({ type: 'basic', username: user, password: pass })),
|
||||
}
|
||||
})
|
||||
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { Trino as TrinoNodeClient } from 'trino-client'
|
||||
import { TrinoClient } from '@commercial/backend/lib/db/clients/trino'
|
||||
import { IDbConnectionServer } from '@/lib/db/backendTypes'
|
||||
import { IDbConnectionDatabase } from '@/lib/db/types'
|
||||
|
||||
function makeServer(overrides: Partial<IDbConnectionServer['config']> = {}): IDbConnectionServer {
|
||||
return {
|
||||
db: {},
|
||||
config: {
|
||||
client: 'trino',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
user: 'testuser',
|
||||
password: null,
|
||||
readOnlyMode: false,
|
||||
osUser: 'testuser',
|
||||
ssh: null,
|
||||
sslCaFile: null,
|
||||
sslCertFile: null,
|
||||
sslKeyFile: null,
|
||||
sslRejectUnauthorized: false,
|
||||
ssl: false,
|
||||
domain: null,
|
||||
socketPath: null,
|
||||
socketPathEnabled: false,
|
||||
...overrides,
|
||||
},
|
||||
} as IDbConnectionServer
|
||||
}
|
||||
|
||||
function makeDatabase(): IDbConnectionDatabase {
|
||||
return {
|
||||
database: 'postgresql',
|
||||
connected: false,
|
||||
connecting: false,
|
||||
namespace: null,
|
||||
}
|
||||
}
|
||||
|
||||
describe('TrinoClient SSL configuration (bug #3695)', () => {
|
||||
let tmpDir: string
|
||||
let caFile: string
|
||||
let certFile: string
|
||||
let keyFile: string
|
||||
|
||||
beforeAll(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trino-test-'))
|
||||
caFile = path.join(tmpDir, 'ca.pem')
|
||||
certFile = path.join(tmpDir, 'cert.pem')
|
||||
keyFile = path.join(tmpDir, 'key.pem')
|
||||
fs.writeFileSync(caFile, 'fake-ca')
|
||||
fs.writeFileSync(certFile, 'fake-cert')
|
||||
fs.writeFileSync(keyFile, 'fake-key')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true })
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should pass ssl options to Trino.create() when ssl is true', async () => {
|
||||
const server = makeServer({ ssl: true })
|
||||
const client = new TrinoClient(server, makeDatabase())
|
||||
await client.connect()
|
||||
|
||||
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
|
||||
expect(createCall.ssl).toBeDefined()
|
||||
})
|
||||
|
||||
it('should use https protocol when ssl is true', async () => {
|
||||
const server = makeServer({ ssl: true })
|
||||
const client = new TrinoClient(server, makeDatabase())
|
||||
await client.connect()
|
||||
|
||||
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
|
||||
expect(createCall.server).toContain('https')
|
||||
})
|
||||
|
||||
it('should pass ssl cert/key/ca files when configured', async () => {
|
||||
const server = makeServer({
|
||||
ssl: true,
|
||||
sslCaFile: caFile,
|
||||
sslCertFile: certFile,
|
||||
sslKeyFile: keyFile,
|
||||
})
|
||||
const client = new TrinoClient(server, makeDatabase())
|
||||
await client.connect()
|
||||
|
||||
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
|
||||
expect(createCall.ssl).toBeDefined()
|
||||
expect(createCall.ssl.ca).toBeDefined()
|
||||
expect(createCall.ssl.cert).toBeDefined()
|
||||
expect(createCall.ssl.key).toBeDefined()
|
||||
})
|
||||
|
||||
it('should set rejectUnauthorized based on config', async () => {
|
||||
const server = makeServer({
|
||||
ssl: true,
|
||||
sslRejectUnauthorized: false,
|
||||
})
|
||||
const client = new TrinoClient(server, makeDatabase())
|
||||
await client.connect()
|
||||
|
||||
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
|
||||
expect(createCall.ssl).toBeDefined()
|
||||
expect(createCall.ssl.rejectUnauthorized).toBe(false)
|
||||
})
|
||||
|
||||
it('should not pass ssl options when ssl is false', async () => {
|
||||
const server = makeServer({ ssl: false })
|
||||
const client = new TrinoClient(server, makeDatabase())
|
||||
await client.connect()
|
||||
|
||||
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
|
||||
expect(createCall.ssl).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('TrinoClient SQL escaping', () => {
|
||||
let client: TrinoClient
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
capturedQueries.length = 0
|
||||
client = new TrinoClient(makeServer(), makeDatabase())
|
||||
await client.connect()
|
||||
capturedQueries.length = 0
|
||||
})
|
||||
|
||||
it('should wrap catalog name with identifier quoting in listSchemas', async () => {
|
||||
const maliciousDb = "cat; DROP TABLE users --"
|
||||
;(client as any).db = maliciousDb
|
||||
await client.listSchemas(null)
|
||||
|
||||
const sql = capturedQueries[0]
|
||||
// Catalog name must be inside double-quote identifiers
|
||||
expect(sql).toContain('"cat; DROP TABLE users --"')
|
||||
})
|
||||
|
||||
it('should wrap catalog name with identifier quoting in listTables', async () => {
|
||||
const maliciousDb = "cat; DROP TABLE users --"
|
||||
;(client as any).db = maliciousDb
|
||||
await client.listTables(null)
|
||||
|
||||
const sql = capturedQueries[0]
|
||||
// Catalog name must be inside double-quote identifiers
|
||||
expect(sql).toContain('"cat; DROP TABLE users --".information_schema')
|
||||
})
|
||||
|
||||
it('should escape schema and table names in listTableColumns', async () => {
|
||||
await client.listTableColumns("test'; DROP TABLE users --", "public'; DROP TABLE users --")
|
||||
|
||||
const sql = capturedQueries[0]
|
||||
// Single quotes in values must be doubled to stay inside SQL string literals
|
||||
expect(sql).toContain("public''; DROP TABLE users --")
|
||||
expect(sql).toContain("test''; DROP TABLE users --")
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { buildSelectTopQuery, escapeString, isAllowedReadOnlyQuery } from "../../../../../src/lib/db/clients/utils";
|
||||
import { buildSchemaFilter, buildSelectTopQuery, escapeString, isAllowedReadOnlyQuery } from "../../../../../src/lib/db/clients/utils";
|
||||
|
||||
describe('Escape String', () => {
|
||||
it("should escape single quotes", () => {
|
||||
@@ -82,6 +82,31 @@ describe("buildSelectTopQuery", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSchemaFilter SQL injection', () => {
|
||||
it("should escape single quotes in schema name to prevent SQL injection", () => {
|
||||
const malicious = "'; DROP TABLE users; --"
|
||||
const result = buildSchemaFilter({ schema: malicious })
|
||||
// The single quote must be doubled so the value stays inside the SQL string literal
|
||||
// Safe: schema_name = '''; DROP TABLE users; --' (the '' is an escaped quote inside the literal)
|
||||
// Unsafe: schema_name = ''; DROP TABLE users; --' (the ' closes the literal, rest is executable)
|
||||
// Result: schema_name = '''; DROP TABLE users; --'
|
||||
// In SQL: opening ', then '' (escaped quote), then rest of string, closing '
|
||||
// The whole value is treated as one string literal — safe.
|
||||
expect(result).toBe("schema_name = '''; DROP TABLE users; --'")
|
||||
})
|
||||
|
||||
it("should escape single quotes in 'only' filter values", () => {
|
||||
const result = buildSchemaFilter({ only: ["public", "'; DROP TABLE users; --"] })
|
||||
expect(result).toContain("'public'")
|
||||
expect(result).toContain("'''; DROP TABLE users; --'")
|
||||
})
|
||||
|
||||
it("should escape single quotes in 'ignore' filter values", () => {
|
||||
const result = buildSchemaFilter({ ignore: ["'; DROP TABLE users; --"] })
|
||||
expect(result).toContain("'''; DROP TABLE users; --'")
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAllowedReadOnly', () => {
|
||||
it('Should return as a read only query', () => {
|
||||
const queries = [
|
||||
|
||||
@@ -21,11 +21,22 @@ Conectarse a una base de datos Trino desde Beekeeper Studio es sencillo. Selecci
|
||||
Para conectarte a una base de datos Trino, necesitaras la siguiente informacion:
|
||||
|
||||
- **Host**: La direccion IP o nombre de host de tu servidor Trino.
|
||||
- **Puerto**: El puerto predeterminado es 8080, pero esto se puede personalizar si tu servidor usa un puerto diferente.
|
||||
- **Puerto**: El puerto predeterminado es 8080 para HTTP, o 8443 para HTTPS. Esto se puede personalizar si tu servidor usa un puerto diferente.
|
||||
- **Nombre de usuario**: Tu nombre de usuario de Trino, siendo default el valor predeterminado tipico.
|
||||
- **Contrasena**: Tu contrasena de Trino, si aplica.
|
||||
- **Catalogo predeterminado (opcional)**: El catalogo al que quieres conectarte inicialmente al inicio
|
||||
|
||||
### Conexiones SSL / HTTPS
|
||||
|
||||
Si tu coordinador Trino esta configurado con TLS/HTTPS, habilita **SSL** en el formulario de conexion. Beekeeper Studio soporta tres modos de SSL:
|
||||
|
||||
1. **Confiar en el certificado del servidor** — Habilita SSL sin proporcionar archivos de certificado. Beekeeper Studio se conectara por HTTPS pero no verificara el certificado del servidor. Esta es la opcion mas sencilla y funciona con certificados autofirmados.
|
||||
2. **Proporcionar un certificado CA** — Si tu servidor Trino usa un certificado firmado por una CA privada, proporciona el archivo de certificado CA. Deja "Rechazar no autorizados" desmarcado para permitir la conexion.
|
||||
3. **Verificacion completa de certificados** — Proporciona el certificado CA y opcionalmente un certificado de cliente y archivo de clave, luego marca "Rechazar no autorizados" para aplicar la verificacion TLS completa.
|
||||
|
||||
!!! tip
|
||||
Si importas una URL de conexion que comienza con `https://`, SSL se habilitara automaticamente.
|
||||
|
||||
### Probar tu conexion de Trino
|
||||
|
||||
Antes de guardar los detalles de tu conexion, Beekeeper Studio te permite probar la conexion:
|
||||
@@ -40,6 +51,7 @@ Una vez que los detalles de tu conexion han sido verificados, puedes elegir guar
|
||||
|
||||
## Funciones soportadas
|
||||
|
||||
- Conexiones SSL / HTTPS (con archivos opcionales de CA, certificado de cliente y clave)
|
||||
- Vista de datos de tabla
|
||||
- Ordenamiento y filtrado de datos de tabla
|
||||
- Vista de estructura de tabla
|
||||
|
||||
@@ -21,11 +21,22 @@ Connecting to a Trino database from Beekeeper Studio is straightforward. SElect
|
||||
To connect to a Trino database, you'll need the following information:
|
||||
|
||||
- **Host**: The IP address or hostname of your Trino server.
|
||||
- **Port**: The default port is 8080, but this can be customized if your server uses a different port.
|
||||
- **Port**: The default port is 8080 for HTTP, or 8443 for HTTPS. This can be customized if your server uses a different port.
|
||||
- **Username**: Your Trino username, with default being the typical default.
|
||||
- **Password**: Your Trino password, if applicable.
|
||||
- **Default Catalog (optional)**: The catalog you want initially connected to at startup
|
||||
|
||||
### SSL / HTTPS Connections
|
||||
|
||||
If your Trino coordinator is configured with TLS/HTTPS, enable **SSL** in the connection form. Beekeeper Studio supports three SSL modes:
|
||||
|
||||
1. **Trust the server certificate** — Enable SSL without providing any certificate files. Beekeeper Studio will connect over HTTPS but will not verify the server's certificate. This is the simplest option and works with self-signed certificates.
|
||||
2. **Provide a CA certificate** — If your Trino server uses a certificate signed by a private CA, provide the CA certificate file. Leave "Reject Unauthorized" unchecked to allow the connection.
|
||||
3. **Full certificate verification** — Provide the CA certificate and optionally a client certificate and key file, then check "Reject Unauthorized" to enforce full TLS verification.
|
||||
|
||||
!!! tip
|
||||
If you import a connection URL that starts with `https://`, SSL will be enabled automatically.
|
||||
|
||||
### Testing Your Trino Connection
|
||||
|
||||
Before saving your connection details, Beekeeper Studio allows you to test the connection:
|
||||
@@ -40,6 +51,7 @@ Once your connection details have been verified, you can choose to save them by
|
||||
|
||||
## Supported Features
|
||||
|
||||
- SSL / HTTPS connections (with optional CA, client cert, and key files)
|
||||
- Table data view
|
||||
- Table data sorting, filtering
|
||||
- Table structure view
|
||||
|
||||
20
yarn.lock
20
yarn.lock
@@ -8105,10 +8105,10 @@ domexception@^4.0.0:
|
||||
dependencies:
|
||||
webidl-conversions "^7.0.0"
|
||||
|
||||
dompurify@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
|
||||
integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==
|
||||
dompurify@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.2.tgz#58c515d0f8508b8749452a028aa589ad80b36325"
|
||||
integrity sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
@@ -9858,14 +9858,14 @@ immediate@~3.0.5:
|
||||
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
|
||||
|
||||
immutable@^4.0.0:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
||||
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
||||
version "4.3.8"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.8.tgz#02d183c7727fb2bb1d5d0380da0d779dce9296a7"
|
||||
integrity sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==
|
||||
|
||||
immutable@^5.0.2:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1"
|
||||
integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==
|
||||
version "5.1.5"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165"
|
||||
integrity sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==
|
||||
|
||||
import-fresh@^3.0.0:
|
||||
version "3.3.1"
|
||||
|
||||
Reference in New Issue
Block a user