Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=test
MSSQL_CREDENTIALS:Server=mssql,1433;Database=master;User Id=sa;Password=Pass@word;trustServerCertificate=true;
MSSQL_CREDENTIALS_READ_ONLY:Server=mssql,1433;Database=master;User Id=reader;Password=re@derP@ssw0rd;trustServerCertificate=true;
MYSQL_TEST_CREDENTIALS=mysql://root@mysql:3306/mysql?sslMode=DISABLED
MSSQL_TEST_CREDENTIALS=Server=mssql,1433;Database=master;User Id=sa;Password=Pass@word;trustServerCertificate=true;
POSTGRES_TEST_CREDENTIALS=postgres://postgres@postgres:5432/postgres?sslmode=disable
5 changes: 4 additions & 1 deletion .env.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const MSSQL_CREDENTIALS = env("MSSQL_CREDENTIALS");
export const MSSQL_TEST_CREDENTIALS = env("MSSQL_TEST_CREDENTIALS");
export const MYSQL_TEST_CREDENTIALS = env("MYSQL_TEST_CREDENTIALS");
export const POSTGRES_TEST_CREDENTIALS = env("POSTGRES_TEST_CREDENTIALS");
export const SNOWFLAKE_TEST_CREDENTIALS = env("SNOWFLAKE_TEST_CREDENTIALS");
export const NODE_ENV = env("NODE_ENV");

function env(key, defaultValue) {
Expand Down
4 changes: 2 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2018
"ecmaVersion": 2022
},
"env": {
"node": true,
Expand All @@ -18,7 +18,7 @@
{
"files": ["*.test.js"],
"env": {
"jest": true
"mocha": true
}
}
],
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Test
on: push

jobs:
test:
runs-on: ubuntu-20.04
defaults:
run:
working-directory: .
env:
DOCKER_PACKAGE: ghcr.io/${{ github.repository }}/database-proxy_test

steps:
- uses: actions/checkout@v3
- name: Docker login
run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_ACTOR} --password-stdin ghcr.io
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Republish
id: republish
continue-on-error: true
if: ${{ needs.Changes.outputs.connector == 'false' }}
run: |
../.github/retry docker pull ${DOCKER_PACKAGE}:${{ github.event.before }}
docker tag ${DOCKER_PACKAGE}:${{ github.event.before }} ${DOCKER_PACKAGE}:${GITHUB_SHA}
../.github/retry docker push ${DOCKER_PACKAGE}:${GITHUB_SHA}

- name: Build
if: ${{ steps.republish.outcome != 'success' }}
run: docker-compose build
- name: Lint
if: ${{ steps.republish.outcome != 'success' }}
run: docker-compose run lint
- name: Test
if: ${{ steps.republish.outcome != 'success' }}
run: docker-compose run test
env:
SNOWFLAKE_TEST_CREDENTIALS: ${{ secrets.SNOWFLAKE_TEST_CREDENTIALS }}
- name: Container logs
if: failure()
run: docker-compose logs --no-color --timestamps
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
ssl/localhost.csr

*.secret
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM node:18.12.1-alpine

RUN apk update && apk --no-cache add git

RUN mkdir /app
WORKDIR /app

Expand Down
4 changes: 2 additions & 2 deletions data/seed.mssql.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import mssql from "mssql";
import {MSSQL_CREDENTIALS} from "../.env.test.js";
import {MSSQL_TEST_CREDENTIALS} from "../.env.test.js";

const credentials = MSSQL_CREDENTIALS;
const credentials = MSSQL_TEST_CREDENTIALS;

const seed = async () => {
await mssql.connect(credentials);
Expand Down
16 changes: 14 additions & 2 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
version: "3.7"

services:
test:
env_file:
- .env.secret

mssql:
image: mcr.microsoft.com/azure-sql-edge
expose:
- "1433"
ports:
- "1433:1433"

mysql:
ports:
- "3306:3306"

postgres:
ports:
- "5432:5432"
26 changes: 23 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
version: "3.7"

services:
lint:
build: .
command: eslint .

test:
build: .
depends_on:
- mssql
- mysql
- postgres
env_file:
- .env.test
environment:
- SNOWFLAKE_TEST_CREDENTIALS
networks:
- db_proxy_test
command: sh -c "set -o pipefail && wait-on -d 10000 -t 30000 tcp:mssql:1433 && node ./data/seed.mssql.js && TZ=UTC NODE_ENV=TEST node_modules/.bin/mocha"
command: sh -c "set -o pipefail && wait-on -d 15000 -t 30000 tcp:mysql:3306 tcp:mssql:1433 tcp:postgres:5432 && node ./data/seed.mssql.js && TZ=UTC NODE_ENV=TEST node_modules/.bin/mocha --exit"

mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
Expand All @@ -20,8 +28,20 @@ services:
- MSSQL_SLEEP=7
volumes:
- ./data/AdventureWorks2019.bak:/var/opt/mssql/backup/test.bak
ports:
- "1433:1433"
networks:
- db_proxy_test

mysql:
image: mariadb:10.6.4
environment:
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=yes
networks:
- db_proxy_test

postgres:
image: postgres:13.8-alpine3.16
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
networks:
- db_proxy_test

Expand Down
6 changes: 2 additions & 4 deletions lib/databricks.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,14 @@ export async function queryStream(req, res, connection) {
res.write(`${JSON.stringify(responseSchema)}`);
res.write("\n");

await new Promise(async (resolve, reject) => {
await new Promise((resolve, reject) => {
const stream = new Readable.from(rows);

stream.once("data", () => {
clearInterval(keepAlive);
});

stream.on("close", (error) => {
stream.on("close", () => {
resolve();
stream.destroy();
});
Expand Down Expand Up @@ -345,8 +345,6 @@ export async function check(req, res, connection) {
});

return {ok: true};
} catch (e) {
throw e;
} finally {
if (connection) {
try {
Expand Down
11 changes: 9 additions & 2 deletions lib/mssql.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Transform} from "stream";

import {failedCheck, badRequest, notImplemented} from "./errors.js";
import {validateQueryPayload} from "./validate.js";
import Pools from "./pools.js";

const TYPES = mssql.TYPES;
const READ_ONLY = new Set(["SELECT", "USAGE", "CONNECT"]);
Expand Down Expand Up @@ -124,8 +125,6 @@ export async function check(req, res, pool) {
return {ok: true};
}

export const ConnectionPool = mssql.ConnectionPool;

export default (credentials) => {
const pool = new mssql.ConnectionPool(credentials);

Expand All @@ -144,6 +143,14 @@ export default (credentials) => {
};
};

export const pools = new Pools((credentials) =>
Object.defineProperty(new mssql.ConnectionPool(credentials), "end", {
value() {
this.close();
},
})
);

// See https://github.com/tediousjs/node-mssql/blob/66587d97c9ce21bffba8ca360c72a540f2bc47a6/lib/datatypes.js#L6
const boolean = ["null", "boolean"],
integer = ["null", "integer"],
Expand Down
17 changes: 16 additions & 1 deletion lib/mysql.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import JSONStream from "JSONStream";
import {json} from "micro";
import mysql, {createConnection} from "mysql2";
import mysql, {createConnection, createPool} from "mysql2";
import {failedCheck} from "./errors.js";
import {notFound} from "./errors.js";
import Pools from "./pools.js";

const {Types, ConnectionConfig} = mysql;

export const pools = new Pools(({host, port, database, user, password, ssl}) =>
createPool({
host,
port,
database,
user,
password,
ssl: ssl === "required" ? {} : false,
connectTimeout: 25e3,
connectionLimit: 30,
decimalNumbers: true,
})
);

export async function query(req, res, pool) {
const {sql, params} = await json(req);
const keepAlive = setInterval(() => res.write("\n"), 25e3);
Expand Down
24 changes: 20 additions & 4 deletions lib/oracle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Transform} from "stream";

import {badRequest, failedCheck} from "./errors.js";
import {validateQueryPayload} from "./validate.js";
import Pools from "./pools.js";

const READ_ONLY = new Set(["SELECT", "USAGE", "CONNECT"]);
export class OracleSingleton {
Expand Down Expand Up @@ -204,8 +205,6 @@ export async function check(req, res, pool) {
);

return {ok: true};
} catch (e) {
throw e;
} finally {
if (connection) {
try {
Expand All @@ -217,13 +216,30 @@ export async function check(req, res, pool) {
}
}

export const pools = new Pools(async (credentials) => {
const oracledb = await OracleSingleton.getInstance();
credentials.connectionString = decodeURI(credentials.connectionString);
const pool = await oracledb.createPool(credentials);

Object.defineProperty(pool, "end", {
value() {
// We must ensure there is no query still running before we close the pool.
if (this._connectionsOut === 0) {
this.close();
}
},
});

return pool;
});

export default async ({url, username, password}) => {
OracleSingleton.initialize();
// We do not want to import the oracledb library until we are sure that the user is looking to use Oracle.
// Installing the oracledb library is a pain, so we want to avoid it if possible.
const config = {
username: username,
password: password,
username,
password,
connectionString: decodeURI(url),
};

Expand Down
55 changes: 55 additions & 0 deletions lib/pools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import LRU from "lru-cache";
import * as Sentry from "@sentry/node";

const maxAge = 1000 * 60 * 10; // 10m

export default class Pools {
constructor(createPool) {
this.createPool = createPool;
this.cache = new LRU({
max: 100,
maxAge,
updateAgeOnGet: true,
dispose(_key, pool) {
pool.end();
},
});

let loop;
(loop = () => {
this.cache.prune();
this.timeout = setTimeout(loop, maxAge / 2);
})();
}

async get(credentials) {
const key = JSON.stringify(credentials);
if (this.cache.has(key)) return this.cache.get(key);
const pool = await this.createPool(credentials);

pool.on("error", (error) => {
// We need to attach a handler otherwise the process could exit, but we
// just don't care about these errors because the client will get cleaned
// up already. For debugging purposes, we'll add a Sentry breadcrumb if
// something else errors more loudly.
Sentry.addBreadcrumb({
message: error.message,
category: "pool",
level: "error",
data: error,
});
});

this.cache.set(key, pool);
return pool;
}

del(credentials) {
this.cache.del(JSON.stringify(credentials));
}

end() {
if (this.timeout) clearTimeout(this.timeout);
for (const pool of this.cache.values()) pool.end();
}
}
Loading