From 4fbbf45ea8af140bf790785384d0bfa9ef34e277 Mon Sep 17 00:00:00 2001 From: Brayden Wilmoth Date: Wed, 22 May 2024 18:03:45 -0400 Subject: [PATCH 1/4] Support Neon connections via their library --- package-lock.json | 137 +++++++++++++++++++++++++++++++++-- package.json | 4 +- playground/index.js | 19 ++--- src/connections/neon-http.ts | 73 +++++++++++++++++++ src/index.ts | 1 + 5 files changed, 216 insertions(+), 18 deletions(-) create mode 100644 src/connections/neon-http.ts diff --git a/package-lock.json b/package-lock.json index eaab823..672edcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { - "name": "@outerbase/query-builder", - "version": "1.0.9", + "name": "@outerbase/sdk", + "version": "1.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@outerbase/query-builder", - "version": "1.0.9", + "name": "@outerbase/sdk", + "version": "1.0.10", "license": "MIT", "dependencies": { - "handlebars": "^4.7.8" + "@neondatabase/serverless": "^0.9.3", + "handlebars": "^4.7.8", + "ws": "^8.17.0" }, "bin": { "sync-database-models": "dist/generators/generate-models.js" @@ -19,15 +21,32 @@ "typescript": "^5.4.3" } }, + "node_modules/@neondatabase/serverless": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.9.3.tgz", + "integrity": "sha512-6ZBK8asl2Z3+ADEaELvbaVVGVlmY1oAzkxxZfpmXPKFuJhbDN+5fU3zYBamsahS/Ch1zE+CVWB3R+8QEI2LMSw==", + "dependencies": { + "@types/pg": "8.11.6" + } + }, "node_modules/@types/node": { "version": "20.12.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/pg": { + "version": "8.11.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", + "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -61,6 +80,89 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -97,13 +199,32 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 8bbdae3..ca8fc37 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "author": "Outerbase", "license": "MIT", "dependencies": { - "handlebars": "^4.7.8" + "@neondatabase/serverless": "^0.9.3", + "handlebars": "^4.7.8", + "ws": "^8.17.0" }, "devDependencies": { "@types/node": "^20.12.12", diff --git a/playground/index.js b/playground/index.js index 58c460a..eff04ae 100644 --- a/playground/index.js +++ b/playground/index.js @@ -1,22 +1,23 @@ -import { CloudflareD1Connection, Outerbase, equalsNumber } from '../dist/index.js'; +import { CloudflareD1Connection, NeonHttpConnection, Outerbase, equalsNumber } from '../dist/index.js'; import express from 'express'; const app = express(); const port = 4000; app.get('/', async (req, res) => { - const d1 = new CloudflareD1Connection( - 'API_KEY', - 'ACCOUNT_ID', - 'DATABASE_ID' - ); - const db = Outerbase(d1); + // Establish connection to your provider database + const d1 = new CloudflareD1Connection('API_KEY', 'ACCOUNT_ID', 'DATABASE_ID'); + const neon = new NeonHttpConnection('postgresql://brayden:XYZ.us-east-2.aws.neon.tech/neondb?sslmode=require'); + + // Create an Outerbase instance from the data connection + await neon.connect(); + const db = Outerbase(neon); // SELECT: let { data } = await db.selectFrom([ - { table: 'table_name', columns: ['id'] } + { table: 'playing_with_neon', columns: ['id', 'name', 'value'] } ]) - .where(equalsNumber('id', 1)) + // .where(equalsNumber('id', 1)) .query() res.json(data); diff --git a/src/connections/neon-http.ts b/src/connections/neon-http.ts new file mode 100644 index 0000000..85c3c59 --- /dev/null +++ b/src/connections/neon-http.ts @@ -0,0 +1,73 @@ +import { Client } from '@neondatabase/serverless'; +import { Connection } from './index'; +import ws from 'ws'; + +export class NeonHttpConnection implements Connection { + databaseUrl: string; + client: Client; + + /** + * Creates a new NeonHttpConnection object with the provided API key, + * account ID, and database ID. + * + * @param databaseUrl - The URL to the database to be used for the connection. + */ + constructor(databaseUrl: string) { + this.databaseUrl = databaseUrl; + + this.client = new Client(this.databaseUrl); + this.client.neonConfig.webSocketConstructor = ws; + } + + /** + * Performs a connect action on the current Connection object. + * + * @param details - Unused in the Neon scenario. + * @returns Promise + */ + async connect(details: Record): Promise { + return this.client.connect(); + } + + /** + * Performs a disconnect action on the current Connection object. + * + * @returns Promise + */ + async disconnect(): Promise { + return this.client.end(); + } + + /** + * Triggers a query action on the current Connection object. The query + * is a SQL query that will be executed on a Neon database. + * + * @param query - The SQL query to be executed. + * @param parameters - An object containing the parameters to be used in the query. + * @returns Promise<{ data: any, error: Error | null }> + */ + async query(query: string, parameters: Record[] | undefined): Promise<{ data: any, error: Error | null }> { + let items = null + let error = null + + // TODO: + // - Support an array of query params as a secondary argument + + try { + // Perform the query in a transaction block + await this.client.query('BEGIN'); + const { rows } = await this.client.query(query, []); + items = rows; + await this.client.query('COMMIT'); + } catch (error) { + // Rollback the transaction if an error occurs + await this.client.query('ROLLBACK'); + throw error; + } + + return { + data: items, + error: error + }; + } +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d915917..6069579 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './connections'; export * from './connections/outerbase'; export * from './connections/cloudflare'; +export * from './connections/neon-http'; export * from './client'; export * from './models'; export * from './models/decorators'; From 385e4b7dc69f0e3c86d422f82dc8e68c8b58594a Mon Sep 17 00:00:00 2001 From: Brayden Wilmoth Date: Tue, 25 Jun 2024 12:44:41 -0400 Subject: [PATCH 2/4] Latest playground --- playground/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/index.js b/playground/index.js index eff04ae..b40584d 100644 --- a/playground/index.js +++ b/playground/index.js @@ -7,7 +7,7 @@ const port = 4000; app.get('/', async (req, res) => { // Establish connection to your provider database const d1 = new CloudflareD1Connection('API_KEY', 'ACCOUNT_ID', 'DATABASE_ID'); - const neon = new NeonHttpConnection('postgresql://brayden:XYZ.us-east-2.aws.neon.tech/neondb?sslmode=require'); + const neon = new NeonHttpConnection('postgresql://brayden:Q2kv1fBPDqwN@ep-damp-hill-a59vzq0g.us-east-2.aws.neon.tech/neondb?sslmode=require'); // Create an Outerbase instance from the data connection await neon.connect(); From aa8c911fff5300b95490260903b89adc9de9bd14 Mon Sep 17 00:00:00 2001 From: Brayden Wilmoth Date: Tue, 25 Jun 2024 15:35:02 -0400 Subject: [PATCH 3/4] Update connection string details --- playground/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/playground/index.js b/playground/index.js index 306033d..ef4c4d4 100644 --- a/playground/index.js +++ b/playground/index.js @@ -7,7 +7,7 @@ const port = 4000; app.get('/', async (req, res) => { // Establish connection to your provider database const d1 = new CloudflareD1Connection('API_KEY', 'ACCOUNT_ID', 'DATABASE_ID'); - const neon = new NeonHttpConnection('postgresql://brayden:Q2kv1fBPDqwN@ep-damp-hill-a59vzq0g.us-east-2.aws.neon.tech/neondb?sslmode=require'); + const neon = new NeonHttpConnection('postgresql://USER:PASSWORD@ep-damp-hill-a59vzq0g.us-east-2.aws.neon.tech/neondb?sslmode=require'); // Create an Outerbase instance from the data connection await neon.connect(); @@ -20,10 +20,7 @@ app.get('/', async (req, res) => { // .where(equalsNumber('id', 1)) // .query() - let { data, query } = await db.queryRaw('SELECT * FROM playing_with_neon WHERE id = $1', ['1']); - - console.log('Query: ', query) - + let { data } = await db.queryRaw('SELECT * FROM playing_with_neon WHERE id = $1', ['1']); res.json(data); }); From 7f6c7092f70407378d362f5b9b96530688ed91a2 Mon Sep 17 00:00:00 2001 From: Brayden Wilmoth Date: Tue, 25 Jun 2024 22:14:18 -0400 Subject: [PATCH 4/4] Add Neon tests --- playground/index.js | 4 +++- src/connections/neon-http.ts | 9 ++++++--- tests/connections/neon.test.ts | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tests/connections/neon.test.ts diff --git a/playground/index.js b/playground/index.js index ef4c4d4..766cd14 100644 --- a/playground/index.js +++ b/playground/index.js @@ -7,7 +7,9 @@ const port = 4000; app.get('/', async (req, res) => { // Establish connection to your provider database const d1 = new CloudflareD1Connection('API_KEY', 'ACCOUNT_ID', 'DATABASE_ID'); - const neon = new NeonHttpConnection('postgresql://USER:PASSWORD@ep-damp-hill-a59vzq0g.us-east-2.aws.neon.tech/neondb?sslmode=require'); + const neon = new NeonHttpConnection({ + databaseUrl: 'postgresql://USER:PASSWORD@ep-damp-hill-a59vzq0g.us-east-2.aws.neon.tech/neondb?sslmode=require' + }); // Create an Outerbase instance from the data connection await neon.connect(); diff --git a/src/connections/neon-http.ts b/src/connections/neon-http.ts index 97262e6..064eee9 100644 --- a/src/connections/neon-http.ts +++ b/src/connections/neon-http.ts @@ -1,10 +1,13 @@ import { Client } from '@neondatabase/serverless'; import ws from 'ws'; - import { Connection } from './index'; import { Query, constructRawQuery } from '../query'; import { QueryParamsPositional, QueryType } from '../query-params'; +export type NeonConnectionDetails = { + databaseUrl: string +}; + export class NeonHttpConnection implements Connection { databaseUrl: string; client: Client; @@ -18,8 +21,8 @@ export class NeonHttpConnection implements Connection { * * @param databaseUrl - The URL to the database to be used for the connection. */ - constructor(databaseUrl: string) { - this.databaseUrl = databaseUrl; + constructor(private _: NeonConnectionDetails) { + this.databaseUrl = _.databaseUrl; this.client = new Client(this.databaseUrl); this.client.neonConfig.webSocketConstructor = ws; diff --git a/tests/connections/neon.test.ts b/tests/connections/neon.test.ts new file mode 100644 index 0000000..ce8125d --- /dev/null +++ b/tests/connections/neon.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from '@jest/globals' + +import { NeonHttpConnection } from 'src/connections/neon-http' +import { QueryType } from 'src/query-params' + +describe('NeonHttpConnection', () => { + describe('Query Type', () => { + const connection = new NeonHttpConnection({ + databaseUrl: 'postgresql://USER:PASSWORD@some-random-string0g.us-east-2.aws.neon.tech/neondb?sslmode=require' + }) + + test('Query type is set to positional', () => { + expect(connection.queryType).toBe(QueryType.positional) + }) + + test('Query type is set not named', () => { + expect(connection.queryType).not.toBe(QueryType.named) + }) + }) +})