diff --git a/.github/workflows/studio-build-non-production.yml b/.github/workflows/studio-build-non-production.yml index 8291ac2b1..885833f4c 100644 --- a/.github/workflows/studio-build-non-production.yml +++ b/.github/workflows/studio-build-non-production.yml @@ -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' }} diff --git a/.github/workflows/studio-publish.yml b/.github/workflows/studio-publish.yml index 384481777..dc8f34578 100644 --- a/.github/workflows/studio-publish.yml +++ b/.github/workflows/studio-publish.yml @@ -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] diff --git a/.github/workflows/studio-test.yml b/.github/workflows/studio-test.yml index 4b3be2e89..ba5075d52 100644 --- a/.github/workflows/studio-test.yml +++ b/.github/workflows/studio-test.yml @@ -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 diff --git a/apps/studio/e2e/tests/contextMenu.test.ts b/apps/studio/e2e/tests/contextMenu.test.ts index eeac83585..d5d07ab2c 100644 --- a/apps/studio/e2e/tests/contextMenu.test.ts +++ b/apps/studio/e2e/tests/contextMenu.test.ts @@ -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 () => { diff --git a/apps/studio/e2e/tests/copyResults.test.ts b/apps/studio/e2e/tests/copyResults.test.ts index 086304e1c..fdba1ff44 100644 --- a/apps/studio/e2e/tests/copyResults.test.ts +++ b/apps/studio/e2e/tests/copyResults.test.ts @@ -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 () => { diff --git a/apps/studio/e2e/tests/exportResults.test.ts b/apps/studio/e2e/tests/exportResults.test.ts index e299f2458..3c24a3408 100644 --- a/apps/studio/e2e/tests/exportResults.test.ts +++ b/apps/studio/e2e/tests/exportResults.test.ts @@ -40,7 +40,9 @@ test.describe("Export Results Verifications", () => { }); afterEach(async () => { - await electronApp.close(); + if (electronApp) { + await electronApp.close(); + } }); test("downloads as CSV", async () => { diff --git a/apps/studio/e2e/tests/jsonSideBar.test.ts b/apps/studio/e2e/tests/jsonSideBar.test.ts index 297561c77..1ca7ebfa8 100644 --- a/apps/studio/e2e/tests/jsonSideBar.test.ts +++ b/apps/studio/e2e/tests/jsonSideBar.test.ts @@ -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 () => { diff --git a/apps/studio/e2e/tests/newConnection.test.ts b/apps/studio/e2e/tests/newConnection.test.ts index dd46aedc1..850728bfd 100644 --- a/apps/studio/e2e/tests/newConnection.test.ts +++ b/apps/studio/e2e/tests/newConnection.test.ts @@ -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 () => { diff --git a/apps/studio/e2e/tests/queryExecution.test.ts b/apps/studio/e2e/tests/queryExecution.test.ts index 44120057d..b6d9d68bd 100644 --- a/apps/studio/e2e/tests/queryExecution.test.ts +++ b/apps/studio/e2e/tests/queryExecution.test.ts @@ -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 () => { diff --git a/apps/studio/e2e/tests/queryResultPane.test.ts b/apps/studio/e2e/tests/queryResultPane.test.ts index 4e9b36992..7ad862fb8 100644 --- a/apps/studio/e2e/tests/queryResultPane.test.ts +++ b/apps/studio/e2e/tests/queryResultPane.test.ts @@ -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 () => { diff --git a/apps/studio/e2e/tests/tableCreation.test.ts b/apps/studio/e2e/tests/tableCreation.test.ts index e8d57aa6e..53fdb0e00 100644 --- a/apps/studio/e2e/tests/tableCreation.test.ts +++ b/apps/studio/e2e/tests/tableCreation.test.ts @@ -30,6 +30,7 @@ test.describe("Table creation", () => { }); afterEach(async () => { + if (!electronApp) return; const dropTableQuery = `DROP TABLE ${newTableName};` const newQueryIndex = '1'; diff --git a/apps/studio/e2e/tests/tableSideBar.test.ts b/apps/studio/e2e/tests/tableSideBar.test.ts index b50e82440..0551ae8d3 100644 --- a/apps/studio/e2e/tests/tableSideBar.test.ts +++ b/apps/studio/e2e/tests/tableSideBar.test.ts @@ -32,6 +32,7 @@ test.describe("Table creation", () => { }); afterEach(async () => { + if (!electronApp) return; const dropTableQuery = `DROP TABLE ${newTableName};` const newQueryIndex = '1'; diff --git a/apps/studio/electron-builder-config-test.js b/apps/studio/electron-builder-config-test.js index eb5207897..8eb546632 100644 --- a/apps/studio/electron-builder-config-test.js +++ b/apps/studio/electron-builder-config-test.js @@ -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: { diff --git a/apps/studio/electron-builder-config.js b/apps/studio/electron-builder-config.js index a475cf5f3..1eb531e76 100644 --- a/apps/studio/electron-builder-config.js +++ b/apps/studio/electron-builder-config.js @@ -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: { diff --git a/apps/studio/package.json b/apps/studio/package.json index 225c1b47a..0c2c2d2ec 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -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", diff --git a/apps/studio/src-commercial/backend/lib/db/clients/trino.ts b/apps/studio/src-commercial/backend/lib/db/clients/trino.ts index e182c6f70..46e4f111b 100644 --- a/apps/studio/src-commercial/backend/lib/db/clients/trino.ts +++ b/apps/studio/src-commercial/backend/lib/db/clients/trino.ts @@ -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 { 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 { async listSchemas(filter: SchemaFilterOptions): Promise { 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 { async listTables(filter?: FilterOptions): Promise { 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 { 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 { SELECT ${wrappedSelects}, ROW_NUMBER() OVER (${rowNumberOrderClause}) AS rownum - FROM ${this.db}.${tableRef} + FROM ${this.wrapIdentifier(this.db)}.${tableRef} ${filter} ) SELECT * diff --git a/apps/studio/src/common/appdb/models/saved_connection.ts b/apps/studio/src/common/appdb/models/saved_connection.ts index 47a39a89c..537a01b38 100644 --- a/apps/studio/src/common/appdb/models/saved_connection.ts +++ b/apps/studio/src/common/appdb/models/saved_connection.ts @@ -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 } diff --git a/apps/studio/src/components/connection/TrinoForm.vue b/apps/studio/src/components/connection/TrinoForm.vue index 2fbae3d04..f813e0aa7 100644 --- a/apps/studio/src/components/connection/TrinoForm.vue +++ b/apps/studio/src/components/connection/TrinoForm.vue @@ -1,13 +1,5 @@