Skip to content

Commit b850831

Browse files
authored
Merge pull request #39 from outerbase/bwilmoth/neon-http
Neon Serverless Connection
2 parents 6e15713 + 7f6c709 commit b850831

File tree

6 files changed

+265
-7
lines changed

6 files changed

+265
-7
lines changed

package-lock.json

Lines changed: 135 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@
3131
"author": "Outerbase",
3232
"license": "MIT",
3333
"dependencies": {
34-
"handlebars": "^4.7.8"
34+
"@neondatabase/serverless": "^0.9.3",
35+
"handlebars": "^4.7.8",
36+
"ws": "^8.17.1"
3537
},
3638
"devDependencies": {
3739
"@jest/globals": "^29.7.0",
3840
"@types/node": "^20.12.12",
41+
"@types/ws": "^8.5.10",
3942
"husky": "^9.0.11",
4043
"jest": "^29.7.0",
4144
"lint-staged": "^15.2.4",

playground/index.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
1-
import { CloudflareD1Connection, Outerbase, OuterbaseConnection, equalsNumber } from '../dist/index.js';
1+
import { CloudflareD1Connection, Outerbase, NeonHttpConnection, OuterbaseConnection, equalsNumber } from '../dist/index.js';
22
import express from 'express';
33

44
const app = express();
55
const port = 4000;
66

77
app.get('/', async (req, res) => {
8-
const data = {}
8+
// Establish connection to your provider database
9+
const d1 = new CloudflareD1Connection('API_KEY', 'ACCOUNT_ID', 'DATABASE_ID');
10+
const neon = new NeonHttpConnection({
11+
databaseUrl: 'postgresql://USER:[email protected]/neondb?sslmode=require'
12+
});
13+
14+
// Create an Outerbase instance from the data connection
15+
await neon.connect();
16+
const db = Outerbase(neon);
17+
18+
// SELECT:
19+
// let { data, query } = await db.selectFrom([
20+
// { table: 'playing_with_neon', columns: ['id', 'name', 'value'] }
21+
// ])
22+
// .where(equalsNumber('id', 1))
23+
// .query()
24+
25+
let { data } = await db.queryRaw('SELECT * FROM playing_with_neon WHERE id = $1', ['1']);
926
res.json(data);
1027
});
1128

src/connections/neon-http.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Client } from '@neondatabase/serverless';
2+
import ws from 'ws';
3+
import { Connection } from './index';
4+
import { Query, constructRawQuery } from '../query';
5+
import { QueryParamsPositional, QueryType } from '../query-params';
6+
7+
export type NeonConnectionDetails = {
8+
databaseUrl: string
9+
};
10+
11+
export class NeonHttpConnection implements Connection {
12+
databaseUrl: string;
13+
client: Client;
14+
15+
// Default query type to named for Outerbase
16+
queryType = QueryType.positional
17+
18+
/**
19+
* Creates a new NeonHttpConnection object with the provided API key,
20+
* account ID, and database ID.
21+
*
22+
* @param databaseUrl - The URL to the database to be used for the connection.
23+
*/
24+
constructor(private _: NeonConnectionDetails) {
25+
this.databaseUrl = _.databaseUrl;
26+
27+
this.client = new Client(this.databaseUrl);
28+
this.client.neonConfig.webSocketConstructor = ws;
29+
}
30+
31+
/**
32+
* Performs a connect action on the current Connection object.
33+
*
34+
* @param details - Unused in the Neon scenario.
35+
* @returns Promise<any>
36+
*/
37+
async connect(): Promise<any> {
38+
return this.client.connect();
39+
}
40+
41+
/**
42+
* Performs a disconnect action on the current Connection object.
43+
*
44+
* @returns Promise<any>
45+
*/
46+
async disconnect(): Promise<any> {
47+
return this.client.end();
48+
}
49+
50+
/**
51+
* Triggers a query action on the current Connection object. The query
52+
* is a SQL query that will be executed on a Neon database. Neon's driver
53+
* requires positional parameters to be used in the specific format of `$1`,
54+
* `$2`, etc. The query is sent to the Neon database and the response is returned.
55+
*
56+
* @param query - The SQL query to be executed.
57+
* @param parameters - An object containing the parameters to be used in the query.
58+
* @returns Promise<{ data: any, error: Error | null }>
59+
*/
60+
async query(query: Query): Promise<{ data: any; error: Error | null; query: string }> {
61+
let items = null
62+
let error = null
63+
64+
// Replace all `?` with `$1`, `$2`, etc.
65+
let index = 0;
66+
const formattedQuery = query.query.replace(/\?/g, () => `$${++index}`);
67+
68+
try {
69+
await this.client.query('BEGIN');
70+
const { rows } = await this.client.query(formattedQuery, query.parameters as QueryParamsPositional);
71+
items = rows;
72+
await this.client.query('COMMIT');
73+
} catch (error) {
74+
await this.client.query('ROLLBACK');
75+
throw error;
76+
}
77+
78+
const rawSQL = constructRawQuery(query)
79+
80+
return {
81+
data: items,
82+
error: error,
83+
query: rawSQL
84+
};
85+
}
86+
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './connections';
22
export * from './connections/outerbase';
33
export * from './connections/cloudflare';
4+
export * from './connections/neon-http';
45
export * from './client';
56
export * from './models';
67
export * from './models/decorators';

tests/connections/neon.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, test } from '@jest/globals'
2+
3+
import { NeonHttpConnection } from 'src/connections/neon-http'
4+
import { QueryType } from 'src/query-params'
5+
6+
describe('NeonHttpConnection', () => {
7+
describe('Query Type', () => {
8+
const connection = new NeonHttpConnection({
9+
databaseUrl: 'postgresql://USER:[email protected]/neondb?sslmode=require'
10+
})
11+
12+
test('Query type is set to positional', () => {
13+
expect(connection.queryType).toBe(QueryType.positional)
14+
})
15+
16+
test('Query type is set not named', () => {
17+
expect(connection.queryType).not.toBe(QueryType.named)
18+
})
19+
})
20+
})

0 commit comments

Comments
 (0)