Skip to content

Commit

Permalink
feat: add local Unix domain socket support (#336)
Browse files Browse the repository at this point in the history
Adds a new Connector.startLocalProxy method that starts a local proxy tunnel listening to the location defined at listenOptions enabling usage of the Connector by drivers and different libraries / frameworks that are not currently supported by the Connector directly but that can connect to databases via Unix sockets.
  • Loading branch information
jackwotherspoon authored Apr 26, 2024
1 parent 404a85f commit 72575ba
Show file tree
Hide file tree
Showing 20 changed files with 682 additions and 11 deletions.
14 changes: 9 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ jobs:

- name: Run Tests
env:
TAP_DISABLE_COVERAGE: "${{ matrix.os == 'windows-latest' && '1' || '0' }}"
TAP_ALLOW_MISSING_COVERAGE: "${{ matrix.os == 'windows-latest' && '1' || '0' }}"
TAP_ALLOW_INCOMPLETE_COVERAGE: "${{ matrix.os == 'windows-latest' && '1' || '0' }}"
TAP_ALLOW_EMPTY_COVERAGE: "${{ matrix.os == 'windows-latest' && '1' || '0' }}"
TAP_DISABLE_COVERAGE: "1"
TAP_ALLOW_MISSING_COVERAGE: "1"
TAP_ALLOW_INCOMPLETE_COVERAGE: "1"
TAP_ALLOW_EMPTY_COVERAGE: "1"
if: "${{ matrix.node-version != 'v14.x' }}"
run: npx tap -c -t0 -o test_results.tap test
timeout-minutes: 5
Expand Down Expand Up @@ -335,6 +335,10 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Install Prisma on node v16.x and up
if: "${{ matrix.node-version != 'v14.x' }}"
run: npm install prisma

- name: Setup self-direct dependency
run: npm link

Expand Down Expand Up @@ -392,5 +396,5 @@ jobs:
SQLSERVER_USER: '${{ steps.secrets.outputs.SQLSERVER_USER }}'
SQLSERVER_PASS: '${{ steps.secrets.outputs.SQLSERVER_PASS }}'
SQLSERVER_DB: '${{ steps.secrets.outputs.SQLSERVER_DB }}'
run: npx tap -c -t0 --disable-coverage --allow-empty-coverage examples/*/{mysql2,pg,tedious}/test/*{.cjs,.mjs,.ts} -o test_results.tap
run: npx tap -c -t0 --disable-coverage --allow-empty-coverage examples/*/*/test/*{.cjs,.mjs,.ts} -o test_results.tap
timeout-minutes: 5
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,39 @@ connection.close();
connector.close();
```

### Using a Local Proxy Tunnel (Unix domain socket)

Another possible way to use the Cloud SQL Node.js Connector is by creating a
local proxy server that tunnels to the secured connection established
using the `Connector.startLocalProxy()` method instead of
`Connector.getOptions()`.

This alternative approach enables usage of the Connector library with
unsupported drivers such as [Prisma](https://www.prisma.io/). Here is an
example on how to use it with its PostgreSQL driver:

```js
import {Connector} from '@google-cloud/cloud-sql-connector';
import {PrismaClient} from '@prisma/client';

const connector = new Connector();
await connector.startLocalProxy({
instanceConnectionName: 'my-project:us-east1:my-instance',
listenOptions: { path: '.s.PGSQL.5432' },
});
const hostPath = process.cwd();

const datasourceUrl =
`postgresql://my-user:password@localhost/dbName?host=${hostPath}`;
const prisma = new PrismaClient({ datasourceUrl });

connector.close();
await prisma.$disconnect();
```

For examples on each of the supported Cloud SQL databases consult our
[Prisma samples](./examples/README.md#prisma).

### Specifying IP Address Type

The Cloud SQL Connector for Node.js can be used to connect to Cloud SQL
Expand Down Expand Up @@ -457,4 +490,4 @@ Apache Version 2.0
See [LICENSE](./LICENSE)

[credentials-json-file]: https://github.com/googleapis/google-cloud-node#download-your-service-account-credentials-json-file
[google-auth-creds]: https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest#loading-credentials-from-environment-variables
[google-auth-creds]: https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest#loading-credentials-from-environment-variables
9 changes: 9 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ some of the most popular database libraries and frameworks.

## Available Examples

### Prisma

- [MySQL (CommonJS)](./prisma/mysql/connect.cjs)
- [MySQL (ESM)](./prisma/mysql/connect.mjs)
- [MySQL (TypeScript)](./prisma/mysql/connect.ts)
- [PostgreSQL (CommonJS)](./prisma/postgresql/connect.cjs)
- [PostgreSQL (ESM)](./prisma/postgresql/connect.mjs)
- [PostgreSQL (TypeScript)](./prisma/postgresql/connect.ts)

### Knex

- [MySQL (CommonJS)](./knex/mysql2/connect.cjs)
Expand Down
43 changes: 43 additions & 0 deletions examples/prisma/mysql/connect.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

const {resolve} = require('node:path');
const {Connector} = require('@google-cloud/cloud-sql-connector');
const {PrismaClient} = require('@prisma/client');

async function connect({instanceConnectionName, user, database}) {
const path = resolve('mysql.socket');
const connector = new Connector();
await connector.startLocalProxy({
instanceConnectionName,
ipType: 'PUBLIC',
authType: 'IAM',
listenOptions: {path},
});

const datasourceUrl = `mysql://${user}@localhost/${database}?socket=${path}`;
const prisma = new PrismaClient({datasourceUrl});

return {
prisma,
async close() {
await prisma.$disconnect();
connector.close();
},
};
}

module.exports = {
connect,
};
39 changes: 39 additions & 0 deletions examples/prisma/mysql/connect.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {resolve} from 'node:path';
import {Connector} from '@google-cloud/cloud-sql-connector';
import {PrismaClient} from '@prisma/client';

export async function connect({instanceConnectionName, user, database}) {
const path = resolve('mysql.socket');
const connector = new Connector();
await connector.startLocalProxy({
instanceConnectionName,
ipType: 'PUBLIC',
authType: 'IAM',
listenOptions: {path},
});

const datasourceUrl = `mysql://${user}@localhost/${database}?socket=${path}`;
const prisma = new PrismaClient({datasourceUrl});

return {
prisma,
async close() {
await prisma.$disconnect();
connector.close();
},
};
}
43 changes: 43 additions & 0 deletions examples/prisma/mysql/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {resolve} from 'node:path';
import {
AuthTypes,
Connector,
IpAddressTypes,
} from '@google-cloud/cloud-sql-connector';
import {PrismaClient} from '@prisma/client';

export async function connect({instanceConnectionName, user, database}) {
const path = resolve('mysql.socket');
const connector = new Connector();
await connector.startLocalProxy({
instanceConnectionName,
ipType: IpAddressTypes.PUBLIC,
authType: AuthTypes.IAM,
listenOptions: {path},
});

const datasourceUrl = `mysql://${user}@localhost/${database}?socket=${path}`;
const prisma = new PrismaClient({datasourceUrl});

return {
prisma,
async close() {
await prisma.$disconnect();
connector.close();
},
};
}
12 changes: 12 additions & 0 deletions examples/prisma/mysql/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
datasource db {
provider = "mysql"
url = env("DB_URL")
}

model User {
id Int @id @default(autoincrement())
}

generator client {
provider = "prisma-client-js"
}
40 changes: 40 additions & 0 deletions examples/prisma/mysql/test/connect.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

const {execSync} = require('node:child_process');
const {resolve} = require('node:path');
const t = require('tap');

function generatePrismaClient() {
const schemaPath = resolve(__dirname, '../schema.prisma');

execSync(`npx prisma generate --schema=${schemaPath}`);
}

t.test('mysql prisma cjs', async t => {
// prisma client generation should normally be part of a regular Prisma
// setup on user end but in order to tests in many different databases
// we run the generation step at runtime for each variation
generatePrismaClient();

const {connect} = require('../connect.cjs');
const {prisma, close} = await connect({
instanceConnectionName: process.env.MYSQL_IAM_CONNECTION_NAME,
user: process.env.MYSQL_IAM_USER,
database: process.env.MYSQL_DB,
});
const [{now}] = await prisma.$queryRaw`SELECT NOW() as now`;
t.ok(now.getTime(), 'should have valid returned date object');
await close();
});
43 changes: 43 additions & 0 deletions examples/prisma/mysql/test/connect.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { execSync } from 'node:child_process';
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import t from 'tap';

function generatePrismaClient() {
const p = fileURLToPath(import.meta.url)
const __dirname = dirname(p)
const schemaPath = resolve(__dirname, '../schema.prisma');

execSync(`npx prisma generate --schema=${schemaPath}`);
}

t.test('mysql prisma mjs', async t => {
// prisma client generation should normally be part of a regular Prisma
// setup on user end but in order to tests in many different databases
// we run the generation step at runtime for each variation
generatePrismaClient();

const { connect } = await import('../connect.mjs');
const { prisma, close } = await connect({
instanceConnectionName: process.env.MYSQL_IAM_CONNECTION_NAME,
user: process.env.MYSQL_IAM_USER,
database: process.env.MYSQL_DB,
});
const [{ now }] = await prisma.$queryRaw`SELECT NOW() as now`
t.ok(now.getTime(), 'should have valid returned date object');
await close();
});
42 changes: 42 additions & 0 deletions examples/prisma/mysql/test/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {execSync} from 'node:child_process';
import {resolve} from 'node:path';
import t from 'tap';

function generatePrismaClient() {
const schemaPath = resolve(__dirname, '../schema.prisma');

execSync(`npx prisma generate --schema=${schemaPath}`);
}

t.test('mysql prisma ts', async t => {
// prisma client generation should normally be part of a regular Prisma
// setup on user end but in order to tests in many different databases
// we run the generation step at runtime for each variation
generatePrismaClient();

const {
default: {connect},
} = await import('../connect.ts');
const {prisma, close} = await connect({
instanceConnectionName: process.env.MYSQL_IAM_CONNECTION_NAME,
user: process.env.MYSQL_IAM_USER,
database: process.env.MYSQL_DB,
});
const [{now}] = await prisma.$queryRaw`SELECT NOW() as now`;
t.ok(now.getTime(), 'should have valid returned date object');
await close();
});
Loading

0 comments on commit 72575ba

Please sign in to comment.