From 3e4ec15e884fca2bd5bc5472bfbdfbc339b3fd64 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 27 Oct 2025 11:54:59 +0100 Subject: [PATCH 1/5] =?UTF-8?q?start=20Neon=20Client=20work=20=F0=9F=90=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 35 +++++++++--- src/connectors/neon.ts | 100 +++++++++++++++++++++++++++++++++++ test/connectors/neon.test.ts | 12 +++++ test/test-db.sql | 2 + 5 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 src/connectors/neon.ts create mode 100644 test/connectors/neon.test.ts create mode 100644 test/test-db.sql diff --git a/package.json b/package.json index 16480a7..f3bf86e 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@cloudflare/workers-types": "^4.20251014.0", "@electric-sql/pglite": "^0.3.11", "@libsql/client": "^0.15.15", + "@neondatabase/serverless": "^1.0.2", "@planetscale/database": "^1.19.0", "@types/better-sqlite3": "^7.6.13", "@types/bun": "^1.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad96d1d..0ba47de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@libsql/client': specifier: ^0.15.15 version: 0.15.15 + '@neondatabase/serverless': + specifier: ^1.0.2 + version: 1.0.2 '@planetscale/database': specifier: ^1.19.0 version: 1.19.0 @@ -53,7 +56,7 @@ importers: version: 17.2.3 drizzle-orm: specifier: ^0.44.7 - version: 0.44.7(@cloudflare/workers-types@4.20251014.0)(@electric-sql/pglite@0.3.11)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7) + version: 0.44.7(@cloudflare/workers-types@4.20251014.0)(@electric-sql/pglite@0.3.11)(@libsql/client@0.15.15)(@neondatabase/serverless@1.0.2)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7) eslint: specifier: ^9.38.0 version: 9.38.0(jiti@2.6.1) @@ -95,13 +98,13 @@ importers: devDependencies: db0: specifier: latest - version: 0.3.4(@electric-sql/pglite@0.3.11)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7))(mysql2@3.15.3)(sqlite3@5.1.7) + version: 0.3.4(@electric-sql/pglite@0.3.11)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@neondatabase/serverless@1.0.2)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7))(mysql2@3.15.3)(sqlite3@5.1.7) drizzle-kit: specifier: ^0.20.14 version: 0.20.18 drizzle-orm: specifier: ^0.29.4 - version: 0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7) + version: 0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@neondatabase/serverless@1.0.2)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7) jiti: specifier: ^1.21.0 version: 1.21.7 @@ -1052,6 +1055,10 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@neondatabase/serverless@1.0.2': + resolution: {integrity: sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw==} + engines: {node: '>=19.0.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1636,6 +1643,9 @@ packages: '@types/node@22.14.1': resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} + '@types/node@22.18.12': + resolution: {integrity: sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==} + '@types/node@24.6.1': resolution: {integrity: sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==} @@ -4407,6 +4417,11 @@ snapshots: '@neon-rs/load@0.0.4': {} + '@neondatabase/serverless@1.0.2': + dependencies: + '@types/node': 22.18.12 + '@types/pg': 8.15.5 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4789,6 +4804,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.18.12': + dependencies: + undici-types: 6.21.0 + '@types/node@24.6.1': dependencies: undici-types: 7.13.0 @@ -5285,12 +5304,12 @@ snapshots: data-uri-to-buffer@4.0.1: {} - db0@0.3.4(@electric-sql/pglite@0.3.11)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7))(mysql2@3.15.3)(sqlite3@5.1.7): + db0@0.3.4(@electric-sql/pglite@0.3.11)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@neondatabase/serverless@1.0.2)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7))(mysql2@3.15.3)(sqlite3@5.1.7): optionalDependencies: '@electric-sql/pglite': 0.3.11 '@libsql/client': 0.15.15 better-sqlite3: 12.4.1 - drizzle-orm: 0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7) + drizzle-orm: 0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@neondatabase/serverless@1.0.2)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7) mysql2: 3.15.3 sqlite3: 5.1.7 @@ -5372,10 +5391,11 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7): + drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.15)(@neondatabase/serverless@1.0.2)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7): optionalDependencies: '@cloudflare/workers-types': 4.20251014.0 '@libsql/client': 0.15.15 + '@neondatabase/serverless': 1.0.2 '@planetscale/database': 1.19.0 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.15.5 @@ -5386,11 +5406,12 @@ snapshots: pg: 8.16.3 sqlite3: 5.1.7 - drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251014.0)(@electric-sql/pglite@0.3.11)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7): + drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251014.0)(@electric-sql/pglite@0.3.11)(@libsql/client@0.15.15)(@neondatabase/serverless@1.0.2)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(bun-types@1.3.1(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7): optionalDependencies: '@cloudflare/workers-types': 4.20251014.0 '@electric-sql/pglite': 0.3.11 '@libsql/client': 0.15.15 + '@neondatabase/serverless': 1.0.2 '@planetscale/database': 1.19.0 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.15.5 diff --git a/src/connectors/neon.ts b/src/connectors/neon.ts new file mode 100644 index 0000000..aec5b26 --- /dev/null +++ b/src/connectors/neon.ts @@ -0,0 +1,100 @@ +import { + Client as NeonClient, + type QueryResult, + type WebSocketConstructor, +} from "@neondatabase/serverless"; +import type { Connector, Primitive } from "db0"; + +import { BoundableStatement } from "./_internal/statement.ts"; + +export type ConnectorOptions = { + connectionString: string; + pooler?: boolean; + webSocketConstructor?: WebSocketConstructor; +}; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +export default function postgresqlConnector( + connectionString?: ConnectorOptions, + webSocketConstructor?: WebSocketConstructor, +): Connector { + let _client: undefined | NeonClient | Promise; + function getClient() { + if (_client) { + return _client; + } + + const client = new NeonClient(connectionString); + _client = client.connect().then(() => { + /** + * @description Allow to override the WebSocket constructor or provide one when platform does not support it. + * @see https://github.com/neondatabase/serverless?tab=readme-ov-file#pool-and-client + */ + if (webSocketConstructor) { + client.neonConfig.webSocketConstructor = webSocketConstructor; + } + + _client = client; + return _client; + }); + + return _client; + } + + const query: InternalQuery = async (sql, params) => { + const client = getClient(); + + return (await client).query(normalizeParams(sql), params); + }; + + return { + name: "neon", + dialect: "postgresql", + getInstance: () => getClient(), + exec: (sql) => query(sql), + prepare: (sql) => new StatementWrapper(sql, query), + dispose: async () => { + await (await _client)?.end?.(); + _client = undefined; + }, + }; +} + +// // https://www.postgresql.org/docs/9.3/sql-prepare.html +function normalizeParams(sql: string) { + let i = 0; + return sql.replace(/\?/g, () => `$${++i}`); +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows; + } + + async run(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows[0]; + } +} diff --git a/test/connectors/neon.test.ts b/test/connectors/neon.test.ts new file mode 100644 index 0000000..dff0b81 --- /dev/null +++ b/test/connectors/neon.test.ts @@ -0,0 +1,12 @@ +import { describe } from "vitest"; +import connector from "../../src/connectors/neon"; +import { testConnector } from "./_tests"; + +describe.runIf(process.env.NEON_URL)("connectors: neon.test", () => { + testConnector({ + dialect: "postgresql", + connector: connector({ + connectionString: process.env.NEON_URL!, + }), + }); +}); diff --git a/test/test-db.sql b/test/test-db.sql new file mode 100644 index 0000000..c63c2d4 --- /dev/null +++ b/test/test-db.sql @@ -0,0 +1,2 @@ +CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT) + From 8cc74c5488a64cbfb09b74d670479995404e4483 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 27 Oct 2025 21:45:47 +0100 Subject: [PATCH 2/5] get Pooler, HTTP, and Serverless connectors for Neon --- src/connectors/{neon.ts => neon/http.ts} | 2 +- src/connectors/neon/index.ts | 82 +++++++++++++++++++ src/connectors/neon/ws.ts | 100 +++++++++++++++++++++++ test/connectors/neon.test.ts | 12 --- test/connectors/neon/http.test.ts | 12 +++ test/connectors/neon/index.test.ts | 15 ++++ test/connectors/neon/ws.test.ts | 12 +++ 7 files changed, 222 insertions(+), 13 deletions(-) rename src/connectors/{neon.ts => neon/http.ts} (97%) create mode 100644 src/connectors/neon/index.ts create mode 100644 src/connectors/neon/ws.ts delete mode 100644 test/connectors/neon.test.ts create mode 100644 test/connectors/neon/http.test.ts create mode 100644 test/connectors/neon/index.test.ts create mode 100644 test/connectors/neon/ws.test.ts diff --git a/src/connectors/neon.ts b/src/connectors/neon/http.ts similarity index 97% rename from src/connectors/neon.ts rename to src/connectors/neon/http.ts index aec5b26..9ffed32 100644 --- a/src/connectors/neon.ts +++ b/src/connectors/neon/http.ts @@ -5,7 +5,7 @@ import { } from "@neondatabase/serverless"; import type { Connector, Primitive } from "db0"; -import { BoundableStatement } from "./_internal/statement.ts"; +import { BoundableStatement } from "../_internal/statement.ts"; export type ConnectorOptions = { connectionString: string; diff --git a/src/connectors/neon/index.ts b/src/connectors/neon/index.ts new file mode 100644 index 0000000..88389a4 --- /dev/null +++ b/src/connectors/neon/index.ts @@ -0,0 +1,82 @@ +import { + neon, + type FullQueryResults, + type NeonQueryFunction, +} from "@neondatabase/serverless"; +import type { Connector, Primitive, Statement } from "db0"; + +import { BoundableStatement } from "../_internal/statement.ts"; + +export type ConnectorOptions = { + connectionString: string; +}; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise>; + +export default function neonServerlessConnector( + opts: ConnectorOptions, +): Connector> { + let _client: undefined | NeonQueryFunction; + + function getClient() { + if (_client) { + return _client; + } + + _client = neon(opts.connectionString, { fullResults: true }); + + return _client; + } + + const query: InternalQuery = async (sql, params) => { + const client = getClient(); + + return client.query(normalizeParams(sql), params); + }; + + return { + name: "neon", + dialect: "postgresql", + getInstance: (): NeonQueryFunction => getClient(), + exec: (sql: string) => query(sql), + prepare: (sql: string): Statement => new StatementWrapper(sql, query), + }; +} + +// // https://www.postgresql.org/docs/9.3/sql-prepare.html +function normalizeParams(sql: string) { + let i = 0; + return sql.replace(/\?/g, () => `$${++i}`); +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows; + } + + async run(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows[0]; + } +} diff --git a/src/connectors/neon/ws.ts b/src/connectors/neon/ws.ts new file mode 100644 index 0000000..614f0c4 --- /dev/null +++ b/src/connectors/neon/ws.ts @@ -0,0 +1,100 @@ +import { + neonConfig, + Pool as NeonPool, + type QueryResult, + type WebSocketConstructor, +} from "@neondatabase/serverless"; +import type { Connector, Primitive } from "db0"; + +import { BoundableStatement } from "../_internal/statement.ts"; + +export type ConnectorOptions = { + connectionString: string; + webSocketConstructor?: WebSocketConstructor; +}; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +/** + * @description Creates a new Neon pool connector. + * @param opts + * @returns + */ +export default function neonPoolConnector( + opts: ConnectorOptions, +): Connector { + let _client: undefined | NeonPool | Promise; + function getClient() { + if (_client) { + return _client; + } + + const client = new NeonPool({ connectionString: opts.connectionString }); + _client = client.connect().then(() => { + /** + * @description Allow to override the WebSocket constructor or provide one when platform does not support it. + * @see https://github.com/neondatabase/serverless?tab=readme-ov-file#pool-and-client + */ + if (opts.webSocketConstructor) { + neonConfig.webSocketConstructor = opts.webSocketConstructor; + } + + _client = client; + return _client; + }); + + return _client; + } + + const query: InternalQuery = async (sql, params) => { + const client = getClient(); + + return (await client).query(normalizeParams(sql), params); + }; + + return { + name: "neon", + dialect: "postgresql", + getInstance: () => getClient(), + exec: (sql) => query(sql), + prepare: (sql) => new StatementWrapper(sql, query), + }; +} + +// // https://www.postgresql.org/docs/9.3/sql-prepare.html +function normalizeParams(sql: string) { + let i = 0; + return sql.replace(/\?/g, () => `$${++i}`); +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows; + } + + async run(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows[0]; + } +} diff --git a/test/connectors/neon.test.ts b/test/connectors/neon.test.ts deleted file mode 100644 index dff0b81..0000000 --- a/test/connectors/neon.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe } from "vitest"; -import connector from "../../src/connectors/neon"; -import { testConnector } from "./_tests"; - -describe.runIf(process.env.NEON_URL)("connectors: neon.test", () => { - testConnector({ - dialect: "postgresql", - connector: connector({ - connectionString: process.env.NEON_URL!, - }), - }); -}); diff --git a/test/connectors/neon/http.test.ts b/test/connectors/neon/http.test.ts new file mode 100644 index 0000000..e6e6148 --- /dev/null +++ b/test/connectors/neon/http.test.ts @@ -0,0 +1,12 @@ +import { describe } from "vitest"; +import { testConnector } from "../_tests"; +import neonClientConnector from "../../../src/connectors/neon/http"; + +describe.runIf(process.env.NEON_URL_HTTP)("connectors: neon.test", () => { + testConnector({ + dialect: "postgresql", + connector: neonClientConnector({ + connectionString: process.env.NEON_URL_HTTP!, + }), + }); +}); diff --git a/test/connectors/neon/index.test.ts b/test/connectors/neon/index.test.ts new file mode 100644 index 0000000..92fe2df --- /dev/null +++ b/test/connectors/neon/index.test.ts @@ -0,0 +1,15 @@ +import { describe } from "vitest"; +import connector from "../../../src/connectors/neon"; +import { testConnector } from "../_tests"; + +describe.runIf(process.env.NEON_URL_SERVERLESS)( + "connectors: neon serverless (index)", + () => { + testConnector({ + dialect: "postgresql", + connector: connector({ + connectionString: process.env.NEON_URL_SERVERLESS!, + }), + }); + }, +); diff --git a/test/connectors/neon/ws.test.ts b/test/connectors/neon/ws.test.ts new file mode 100644 index 0000000..fee4a4e --- /dev/null +++ b/test/connectors/neon/ws.test.ts @@ -0,0 +1,12 @@ +import { describe } from "vitest"; +import connector from "../../../src/connectors/neon/ws"; +import { testConnector } from "../_tests"; + +describe.runIf(process.env.NEON_URL_WS)("connectors: neon pool (ws)", () => { + testConnector({ + dialect: "postgresql", + connector: connector({ + connectionString: process.env.NEON_URL_WS!, + }), + }); +}); From b61438fb1985f66edb35ccc8795733e67ac9c4ed Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 10 Nov 2025 16:07:56 +0100 Subject: [PATCH 3/5] simplify Neon setup --- package.json | 3 + pnpm-lock.yaml | 81 +++++++++++++++++++++ src/connectors/neon.ts | 110 +++++++++++++++++++++++++++++ src/connectors/neon/http.ts | 100 -------------------------- src/connectors/neon/index.ts | 82 --------------------- src/connectors/neon/ws.ts | 100 -------------------------- test/connectors/neon.connstring.ts | 45 ++++++++++++ test/connectors/neon.test.ts | 13 ++++ test/connectors/neon/http.test.ts | 12 ---- test/connectors/neon/index.test.ts | 15 ---- test/connectors/neon/ws.test.ts | 12 ---- 11 files changed, 252 insertions(+), 321 deletions(-) create mode 100644 src/connectors/neon.ts delete mode 100644 src/connectors/neon/http.ts delete mode 100644 src/connectors/neon/index.ts delete mode 100644 src/connectors/neon/ws.ts create mode 100644 test/connectors/neon.connstring.ts create mode 100644 test/connectors/neon.test.ts delete mode 100644 test/connectors/neon/http.test.ts delete mode 100644 test/connectors/neon/index.test.ts delete mode 100644 test/connectors/neon/ws.test.ts diff --git a/package.json b/package.json index f3bf86e..ebd2bb0 100644 --- a/package.json +++ b/package.json @@ -109,5 +109,8 @@ "@parcel/watcher", "es5-ext" ] + }, + "dependencies": { + "get-db": "^0.9.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ba47de..7aeef10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + get-db: + specifier: ^0.9.2 + version: 0.9.2 sqlite3: specifier: '*' version: 5.1.7 @@ -140,6 +143,12 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@clack/core@0.4.2': + resolution: {integrity: sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==} + + '@clack/prompts@0.10.1': + resolution: {integrity: sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==} + '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} @@ -1655,6 +1664,9 @@ packages: '@types/react@19.1.13': resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==} + '@types/tinycolor2@1.4.6': + resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2139,6 +2151,10 @@ packages: difflib@0.2.4: resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dotenv@17.2.3: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} @@ -2578,6 +2594,11 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-db@0.9.2: + resolution: {integrity: sha512-MFKeZlFOeKSodQqpq+istvhzluWES2fR5wbd2THJHRd/Bi73aeafg/5BU+gy5GqB4wnXTrh6x8OZsVBKt547fw==} + engines: {node: '>=20.19.0'} + hasBin: true + get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} @@ -2622,6 +2643,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gradient-string@3.0.0: + resolution: {integrity: sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==} + engines: {node: '>=14'} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -3135,6 +3160,14 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + + p-wait-for@5.0.2: + resolution: {integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==} + engines: {node: '>=12'} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3534,6 +3567,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -3541,6 +3577,9 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinygradient@1.1.5: + resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} @@ -3825,6 +3864,17 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@clack/core@0.4.2': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.10.1': + dependencies: + '@clack/core': 0.4.2 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 @@ -4822,6 +4872,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/tinycolor2@1.4.6': {} + '@types/unist@2.0.11': {} '@types/ws@8.18.1': @@ -5363,6 +5415,8 @@ snapshots: dependencies: heap: 0.2.7 + dotenv@16.6.1: {} + dotenv@17.2.3: {} dreamopt@0.8.0: @@ -5815,6 +5869,15 @@ snapshots: dependencies: is-property: 1.0.2 + get-db@0.9.2: + dependencies: + '@clack/prompts': 0.10.1 + '@neondatabase/serverless': 1.0.2 + dotenv: 16.6.1 + gradient-string: 3.0.0 + open: 10.2.0 + p-wait-for: 5.0.2 + get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -5869,6 +5932,11 @@ snapshots: graceful-fs@4.2.11: optional: true + gradient-string@3.0.0: + dependencies: + chalk: 5.4.0 + tinygradient: 1.1.5 + graphemer@1.4.0: {} hanji@0.0.5: @@ -6499,6 +6567,12 @@ snapshots: aggregate-error: 3.1.0 optional: true + p-timeout@6.1.4: {} + + p-wait-for@5.0.2: + dependencies: + p-timeout: 6.1.4 + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6945,6 +7019,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.15: @@ -6952,6 +7028,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinygradient@1.1.5: + dependencies: + '@types/tinycolor2': 1.4.6 + tinycolor2: 1.6.0 + tinyrainbow@3.0.3: {} to-regex-range@5.0.1: diff --git a/src/connectors/neon.ts b/src/connectors/neon.ts new file mode 100644 index 0000000..4965057 --- /dev/null +++ b/src/connectors/neon.ts @@ -0,0 +1,110 @@ +import * as pg from "@neondatabase/serverless"; +import type { Connector, Primitive } from "db0"; + +import { BoundableStatement } from "./_internal/statement.ts"; +import { instantNeon, type InstantNeonParams } from "get-db/sdk"; + +export type ConnectorOptions = ({ url?: string } | pg.ClientConfig) & + InstantNeonParams & { neverGenerateConnectionString?: boolean }; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +export default function neonConnector( + opts?: ConnectorOptions, +): Connector { + let _client: undefined | pg.Client | Promise; + + async function getConnectionString() { + if (opts && "url" in opts) { + return opts.url; + } else if (opts && "connectionString" in opts) { + return opts.connectionString; + } else if ( + process.env.NODE_ENV !== "production" && + !(opts || {}).neverGenerateConnectionString + ) { + const { poolerUrl } = await instantNeon({ referrer: "db0/neon-driver" }); + + return poolerUrl; + } + + throw new Error( + "[db0]:: Missing connection string for connector. Check your environment variables.", + ); + } + + async function getClient() { + if (_client) { + return _client; + } + + const connectionString = await getConnectionString(); + + const client = + typeof opts === "object" + ? new pg.Client({ ...opts, connectionString }) + : new pg.Client(connectionString); + + _client = client.connect().then(() => { + _client = client; + return _client; + }); + + return _client; + } + + const query: InternalQuery = async (sql, params) => { + const client = await getClient(); + return client.query(normalizeParams(sql), params); + }; + + return { + name: "neon", + dialect: "postgresql", + getInstance: () => getClient(), + exec: (sql) => query(sql), + prepare: (sql) => new StatementWrapper(sql, query), + dispose: async () => { + await (await _client)?.end?.(); + _client = undefined; + }, + }; +} + +// https://www.postgresql.org/docs/9.3/sql-prepare.html +function normalizeParams(sql: string) { + let i = 0; + return sql.replace(/\?/g, () => `$${++i}`); +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows; + } + + async run(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows[0]; + } +} diff --git a/src/connectors/neon/http.ts b/src/connectors/neon/http.ts deleted file mode 100644 index 9ffed32..0000000 --- a/src/connectors/neon/http.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - Client as NeonClient, - type QueryResult, - type WebSocketConstructor, -} from "@neondatabase/serverless"; -import type { Connector, Primitive } from "db0"; - -import { BoundableStatement } from "../_internal/statement.ts"; - -export type ConnectorOptions = { - connectionString: string; - pooler?: boolean; - webSocketConstructor?: WebSocketConstructor; -}; - -type InternalQuery = ( - sql: string, - params?: Primitive[], -) => Promise; - -export default function postgresqlConnector( - connectionString?: ConnectorOptions, - webSocketConstructor?: WebSocketConstructor, -): Connector { - let _client: undefined | NeonClient | Promise; - function getClient() { - if (_client) { - return _client; - } - - const client = new NeonClient(connectionString); - _client = client.connect().then(() => { - /** - * @description Allow to override the WebSocket constructor or provide one when platform does not support it. - * @see https://github.com/neondatabase/serverless?tab=readme-ov-file#pool-and-client - */ - if (webSocketConstructor) { - client.neonConfig.webSocketConstructor = webSocketConstructor; - } - - _client = client; - return _client; - }); - - return _client; - } - - const query: InternalQuery = async (sql, params) => { - const client = getClient(); - - return (await client).query(normalizeParams(sql), params); - }; - - return { - name: "neon", - dialect: "postgresql", - getInstance: () => getClient(), - exec: (sql) => query(sql), - prepare: (sql) => new StatementWrapper(sql, query), - dispose: async () => { - await (await _client)?.end?.(); - _client = undefined; - }, - }; -} - -// // https://www.postgresql.org/docs/9.3/sql-prepare.html -function normalizeParams(sql: string) { - let i = 0; - return sql.replace(/\?/g, () => `$${++i}`); -} - -class StatementWrapper extends BoundableStatement { - #query: InternalQuery; - #sql: string; - - constructor(sql: string, query: InternalQuery) { - super(); - this.#sql = sql; - this.#query = query; - } - - async all(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return res.rows; - } - - async run(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return { - success: true, - ...res, - }; - } - - async get(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return res.rows[0]; - } -} diff --git a/src/connectors/neon/index.ts b/src/connectors/neon/index.ts deleted file mode 100644 index 88389a4..0000000 --- a/src/connectors/neon/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - neon, - type FullQueryResults, - type NeonQueryFunction, -} from "@neondatabase/serverless"; -import type { Connector, Primitive, Statement } from "db0"; - -import { BoundableStatement } from "../_internal/statement.ts"; - -export type ConnectorOptions = { - connectionString: string; -}; - -type InternalQuery = ( - sql: string, - params?: Primitive[], -) => Promise>; - -export default function neonServerlessConnector( - opts: ConnectorOptions, -): Connector> { - let _client: undefined | NeonQueryFunction; - - function getClient() { - if (_client) { - return _client; - } - - _client = neon(opts.connectionString, { fullResults: true }); - - return _client; - } - - const query: InternalQuery = async (sql, params) => { - const client = getClient(); - - return client.query(normalizeParams(sql), params); - }; - - return { - name: "neon", - dialect: "postgresql", - getInstance: (): NeonQueryFunction => getClient(), - exec: (sql: string) => query(sql), - prepare: (sql: string): Statement => new StatementWrapper(sql, query), - }; -} - -// // https://www.postgresql.org/docs/9.3/sql-prepare.html -function normalizeParams(sql: string) { - let i = 0; - return sql.replace(/\?/g, () => `$${++i}`); -} - -class StatementWrapper extends BoundableStatement { - #query: InternalQuery; - #sql: string; - - constructor(sql: string, query: InternalQuery) { - super(); - this.#sql = sql; - this.#query = query; - } - - async all(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return res.rows; - } - - async run(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return { - success: true, - ...res, - }; - } - - async get(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return res.rows[0]; - } -} diff --git a/src/connectors/neon/ws.ts b/src/connectors/neon/ws.ts deleted file mode 100644 index 614f0c4..0000000 --- a/src/connectors/neon/ws.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - neonConfig, - Pool as NeonPool, - type QueryResult, - type WebSocketConstructor, -} from "@neondatabase/serverless"; -import type { Connector, Primitive } from "db0"; - -import { BoundableStatement } from "../_internal/statement.ts"; - -export type ConnectorOptions = { - connectionString: string; - webSocketConstructor?: WebSocketConstructor; -}; - -type InternalQuery = ( - sql: string, - params?: Primitive[], -) => Promise; - -/** - * @description Creates a new Neon pool connector. - * @param opts - * @returns - */ -export default function neonPoolConnector( - opts: ConnectorOptions, -): Connector { - let _client: undefined | NeonPool | Promise; - function getClient() { - if (_client) { - return _client; - } - - const client = new NeonPool({ connectionString: opts.connectionString }); - _client = client.connect().then(() => { - /** - * @description Allow to override the WebSocket constructor or provide one when platform does not support it. - * @see https://github.com/neondatabase/serverless?tab=readme-ov-file#pool-and-client - */ - if (opts.webSocketConstructor) { - neonConfig.webSocketConstructor = opts.webSocketConstructor; - } - - _client = client; - return _client; - }); - - return _client; - } - - const query: InternalQuery = async (sql, params) => { - const client = getClient(); - - return (await client).query(normalizeParams(sql), params); - }; - - return { - name: "neon", - dialect: "postgresql", - getInstance: () => getClient(), - exec: (sql) => query(sql), - prepare: (sql) => new StatementWrapper(sql, query), - }; -} - -// // https://www.postgresql.org/docs/9.3/sql-prepare.html -function normalizeParams(sql: string) { - let i = 0; - return sql.replace(/\?/g, () => `$${++i}`); -} - -class StatementWrapper extends BoundableStatement { - #query: InternalQuery; - #sql: string; - - constructor(sql: string, query: InternalQuery) { - super(); - this.#sql = sql; - this.#query = query; - } - - async all(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return res.rows; - } - - async run(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return { - success: true, - ...res, - }; - } - - async get(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return res.rows[0]; - } -} diff --git a/test/connectors/neon.connstring.ts b/test/connectors/neon.connstring.ts new file mode 100644 index 0000000..f3c81fa --- /dev/null +++ b/test/connectors/neon.connstring.ts @@ -0,0 +1,45 @@ +import { describe, test, expect, vi } from "vitest"; + +vi.mock("get-db/sdk", () => { + return { + instantNeon: vi.fn().mockResolvedValue({ + poolerUrl: "postgres://mocked-host/db", + }), + }; +}); + +vi.mock("@neondatabase/serverless", () => { + const mockClient = { + connect: vi.fn().mockResolvedValue(undefined), + query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }), + end: vi.fn().mockResolvedValue(undefined), + }; + + class MockClient { + connect() { + return mockClient.connect(); + } + query() { + return mockClient.query(); + } + end() { + return mockClient.end(); + } + } + + return { + Client: MockClient, + }; +}); + +import neonConnector from "../../src/connectors/neon"; +import { createDatabase } from "../../src"; +import * as getDbSdk from "get-db/sdk"; + +describe("[Neon Connector] Connection string generation", () => { + test("should call `get-db` when connection string is not provided out of production environment", async () => { + const db = createDatabase(neonConnector()); + await db.getInstance(); + expect(vi.mocked(getDbSdk.instantNeon)).toHaveBeenCalledOnce(); + }); +}); diff --git a/test/connectors/neon.test.ts b/test/connectors/neon.test.ts new file mode 100644 index 0000000..25cd7a5 --- /dev/null +++ b/test/connectors/neon.test.ts @@ -0,0 +1,13 @@ +import { describe } from "vitest"; +import neonConnector from "../../src/connectors/neon"; +import { testConnector } from "./_tests"; + +describe.runIf(process.env.NEON_URL)("connectors: Neon", () => { + testConnector({ + dialect: "postgresql", + connector: neonConnector({ + connectionString: process.env.NEON_URL!, + neverGenerateConnectionString: true, + }), + }); +}); diff --git a/test/connectors/neon/http.test.ts b/test/connectors/neon/http.test.ts deleted file mode 100644 index e6e6148..0000000 --- a/test/connectors/neon/http.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe } from "vitest"; -import { testConnector } from "../_tests"; -import neonClientConnector from "../../../src/connectors/neon/http"; - -describe.runIf(process.env.NEON_URL_HTTP)("connectors: neon.test", () => { - testConnector({ - dialect: "postgresql", - connector: neonClientConnector({ - connectionString: process.env.NEON_URL_HTTP!, - }), - }); -}); diff --git a/test/connectors/neon/index.test.ts b/test/connectors/neon/index.test.ts deleted file mode 100644 index 92fe2df..0000000 --- a/test/connectors/neon/index.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe } from "vitest"; -import connector from "../../../src/connectors/neon"; -import { testConnector } from "../_tests"; - -describe.runIf(process.env.NEON_URL_SERVERLESS)( - "connectors: neon serverless (index)", - () => { - testConnector({ - dialect: "postgresql", - connector: connector({ - connectionString: process.env.NEON_URL_SERVERLESS!, - }), - }); - }, -); diff --git a/test/connectors/neon/ws.test.ts b/test/connectors/neon/ws.test.ts deleted file mode 100644 index fee4a4e..0000000 --- a/test/connectors/neon/ws.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe } from "vitest"; -import connector from "../../../src/connectors/neon/ws"; -import { testConnector } from "../_tests"; - -describe.runIf(process.env.NEON_URL_WS)("connectors: neon pool (ws)", () => { - testConnector({ - dialect: "postgresql", - connector: connector({ - connectionString: process.env.NEON_URL_WS!, - }), - }); -}); From a8ee790b191ed2050ecfcfed8e7afb4d7e5e4416 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 10 Nov 2025 19:31:54 +0100 Subject: [PATCH 4/5] docs --- docs/2.connectors/neon.md | 75 ++++++++++++++++++++++++++++++++++--- docs/2.connectors/vercel.md | 24 ++---------- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/docs/2.connectors/neon.md b/docs/2.connectors/neon.md index 695b676..2d3acb4 100644 --- a/docs/2.connectors/neon.md +++ b/docs/2.connectors/neon.md @@ -4,11 +4,76 @@ icon: cbi:neon # NEON -> Connect DB0 to Neon Serverless Postgres. +> Very similar to [Postgres connector](/connectors/postgresql), but optimized for serverless environments. -:read-more{to="https://neon.tech/"} +:read-more{to="https://neon.com"} -::read-more{to="https://github.com/unjs/db0/issues/32"} -This connector is planned to be supported. Follow up via [unjs/db0#32](https://github.com/unjs/db0/issues/32). -:: +## Why Neon Connector? +The fundamental difference is that Postgres Connector uses the [node-postgres](https://node-postgres.com/) driver, which uses a TCP connection, while Neon uses [neondatabase/serverless](https://neon.com/docs/serverless/serverless-driver) and uses a HTTP/Web-Sockets connector. While the drivers have feature parity, the connection type creates some runtime differences. + +The HTTP/WS connection is usually preferred over TCP for serverless environments because: + +- Historically, some runtimes did not work well with TCP connections. +- Reduced latency as a consequence of fewer required network trips per query. +- Reduce number of SCRAM authentication calls. + +Additionally to the runtime differences, Neon connector also allows to automatically seed and instantiate a fresh Postgres instance if initialized without a connection string. + +## Instant Postgres Provisioning + +If the connector's client is instantiated without a connection string **in development**, the Neon connector will automatically generate a connection string. It will also seed schema and data if provided with a `.sql` file. + +## Usage + +Install Neon Servleress Driver for the postgres connection and Instagres' `get-db` package to auto-generate the connection string in development. + +:pm-install{name="@neondatabse/serverless get-db"} + +With those dependencies installed, you can immediately start building: + +```ts +import { createDatabase } from "db0"; +import neon from "db0/connectors/neon"; + +const db = createDatabase( + neon({ + bindingName: "DB", + seed: "init.sql", + }), +); +``` + +```sql [init.sql] +CREATE TABLE IF NOT EXISTS xmen ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); + +INSERT INTO xmen (name) VALUES + ('Wolverine'), + ('Cyclops'), + ('Storm'), + ('Jean Grey'), + ('Beast'), + ('Professor X'), + ('Gambit'), + ('Rogue'), + ('Nightcrawler') +ON CONFLICT DO NOTHING; +``` + +## Options + +### `connectionString` or `url` + +- **Type:** `string` _(optional)_ +- Manually provide a connection string to your Neon database. +- If not provided, it will use the value from the environment variable or automatically provision a database (in development). + +### `seed` + +- **Type:** `string` _(optional)_ +- **Default:** `undefined` +- Path to a `.sql` file for seeding the database schema and initial data. +- If set to `false`, seeding will be disabled. diff --git a/docs/2.connectors/vercel.md b/docs/2.connectors/vercel.md index 28cb208..44c4be6 100644 --- a/docs/2.connectors/vercel.md +++ b/docs/2.connectors/vercel.md @@ -4,25 +4,9 @@ icon: radix-icons:vercel-logo # Vercel -> Connect DB0 to Vercel Postgres +> Vercel Postgres has migrated to Vercel Marketplace. -:read-more{to="https://vercel.com/docs/storage/vercel-postgres"} +Existing Vercel Postgres instances were migrated to [Neon](https://neon.com). +For best integration with db0, use the [Neon Connector](/connectors/neon). -::read-more{to="https://github.com/unjs/db0/issues/32"} -A dedicated `vercel` connector is planned to be supported. Follow up via [unjs/db0#32](https://github.com/unjs/db0/issues/32). -:: - -## Usage - -Use [`postgres`](/connectors/postgresql) connector: - -```js -import { createDatabase } from "db0"; -import postgres from "db0/connectors/postgres"; - -const db = createDatabase( - postgres({ - /* options */ - }), -); -``` +:read-more{to="https://neon.com/docs/guides/vercel-postgres-transition-guide"} From 59bf1bc7d7d5098d9233fa46c36fc4d9ef70e70d Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 13 Nov 2025 12:58:37 +0100 Subject: [PATCH 5/5] mv `get-db` to `devDependencies` --- package.json | 4 +--- pnpm-lock.yaml | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ebd2bb0..c91642b 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "drizzle-orm": "^0.44.7", "eslint": "^9.38.0", "eslint-config-unjs": "^0.5.0", + "get-db": "^0.9.2", "jiti": "^2.6.1", "mlly": "^1.8.0", "mysql2": "^3.15.3", @@ -109,8 +110,5 @@ "@parcel/watcher", "es5-ext" ] - }, - "dependencies": { - "get-db": "^0.9.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aeef10..b8a4e43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - get-db: - specifier: ^0.9.2 - version: 0.9.2 sqlite3: specifier: '*' version: 5.1.7 @@ -66,6 +63,9 @@ importers: eslint-config-unjs: specifier: ^0.5.0 version: 0.5.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + get-db: + specifier: ^0.9.2 + version: 0.9.2 jiti: specifier: ^2.6.1 version: 2.6.1