diff --git a/docs/2.connectors/1.index.md b/docs/2.connectors/1.index.md index f993a907..683ea57f 100644 --- a/docs/2.connectors/1.index.md +++ b/docs/2.connectors/1.index.md @@ -14,6 +14,7 @@ Currently supported connectors: - [PlanetScale](/connectors/planetscale) - [PostgreSQL](/connectors/postgresql) - [MySQL](/connectors/mysql) +- [Neon](/connectors/neon) - [SQLite](/connectors/sqlite) ::read-more{to="https://github.com/unjs/db0/issues/32"} diff --git a/docs/2.connectors/neon.md b/docs/2.connectors/neon.md index 695b6762..3eadee48 100644 --- a/docs/2.connectors/neon.md +++ b/docs/2.connectors/neon.md @@ -6,9 +6,33 @@ icon: cbi:neon > Connect DB0 to Neon Serverless Postgres. -:read-more{to="https://neon.tech/"} +:read-more{to="https://neon.tech"} -::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). -:: +## Usage +For this connector, you need to install [`@neondatabase/serverless`](https://www.npmjs.com/package/@neondatabase/serverless) dependency: + +:pm-install{name="@neondatabase/serverless"} + +Use `neon` connector: + +```js +import { createDatabase } from "db0"; +import neonConnector from "db0/connectors/neon"; + +const db = createDatabase( + neonConnector({ + /* options */ + }), +); +``` + +## Options + +### `url` + +The URL of the Neon Serverless Postgres instance. + +**Type:** `string` + +:read-more{to="https://neon.tech/docs/serverless/serverless-driver#neon-function-configuration-options"} diff --git a/package.json b/package.json index 53145d0b..61d9b12b 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@cloudflare/workers-types": "^4.20260316.1", "@electric-sql/pglite": "^0.3.16", "@libsql/client": "^0.17.0", + "@neondatabase/serverless": "^1.0.2", "@planetscale/database": "^1.19.0", "@types/better-sqlite3": "^7.6.13", "@types/bun": "^1.3.10", @@ -74,6 +75,7 @@ "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", + "@neondatabase/serverless": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", @@ -83,6 +85,9 @@ "@libsql/client": { "optional": true }, + "@neondatabase/serverless": { + "optional": true + }, "better-sqlite3": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bddd0d6d..fcf269d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@libsql/client': specifier: ^0.17.0 version: 0.17.0(encoding@0.1.13) + '@neondatabase/serverless': + specifier: ^1.0.2 + version: 1.0.2 '@planetscale/database': specifier: ^1.19.0 version: 1.19.0 @@ -4277,7 +4280,6 @@ snapshots: dependencies: '@types/node': 22.19.15 '@types/pg': 8.18.0 - optional: true '@npmcli/agent@4.0.0': dependencies: @@ -4545,7 +4547,6 @@ snapshots: '@types/node@22.19.15': dependencies: undici-types: 6.21.0 - optional: true '@types/node@25.5.0': dependencies: @@ -6799,8 +6800,7 @@ snapshots: ufo@1.6.3: {} - undici-types@6.21.0: - optional: true + undici-types@6.21.0: {} undici-types@7.18.2: {} diff --git a/src/_connectors.ts b/src/_connectors.ts index e0171c60..6f18179e 100644 --- a/src/_connectors.ts +++ b/src/_connectors.ts @@ -10,13 +10,14 @@ import type { ConnectorOptions as LibSQLHttpOptions } from "db0/connectors/libsq import type { ConnectorOptions as LibSQLNodeOptions } from "db0/connectors/libsql/node"; import type { ConnectorOptions as LibSQLWebOptions } from "db0/connectors/libsql/web"; import type { ConnectorOptions as MySQL2Options } from "db0/connectors/mysql2"; +import type { ConnectorOptions as NeonOptions } from "db0/connectors/neon"; import type { ConnectorOptions as NodeSQLiteOptions } from "db0/connectors/node-sqlite"; import type { ConnectorOptions as PgliteOptions } from "db0/connectors/pglite"; import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/planetscale"; import type { ConnectorOptions as PostgreSQLOptions } from "db0/connectors/postgresql"; import type { ConnectorOptions as SQLite3Options } from "db0/connectors/sqlite3"; -export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; +export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "neon" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; export type ConnectorOptions = { "better-sqlite3": BetterSQLite3Options; @@ -33,6 +34,7 @@ export type ConnectorOptions = { "libsql": LibSQLNodeOptions; "libsql-web": LibSQLWebOptions; "mysql2": MySQL2Options; + "neon": NeonOptions; "node-sqlite": NodeSQLiteOptions; /** alias of node-sqlite */ "sqlite": NodeSQLiteOptions; @@ -57,6 +59,7 @@ export const connectors: Record = Object.freeze({ "libsql": "db0/connectors/libsql/node", "libsql-web": "db0/connectors/libsql/web", "mysql2": "db0/connectors/mysql2", + "neon": "db0/connectors/neon", "node-sqlite": "db0/connectors/node-sqlite", /** alias of node-sqlite */ "sqlite": "db0/connectors/node-sqlite", diff --git a/src/connectors/neon.ts b/src/connectors/neon.ts new file mode 100644 index 00000000..f6860474 --- /dev/null +++ b/src/connectors/neon.ts @@ -0,0 +1,80 @@ +import { neon } from "@neondatabase/serverless"; +import type { + NeonQueryFunction, + HTTPTransactionOptions, +} from "@neondatabase/serverless"; +import type { Connector, Primitive } from "db0"; +import { BoundableStatement } from "./_internal/statement.ts"; + +export interface ConnectorOptions extends HTTPTransactionOptions { + /** + * The URL of the Neon Serverless Postgres instance. + * + * @required + */ + url: string; +} + +type InternalQuery = (sql: string, params?: Primitive[]) => Promise; + +export default function neonConnector( + opts: ConnectorOptions, +): Connector> { + let _connection: NeonQueryFunction; + + function getConnection() { + if (_connection) { + return _connection; + } + const { url, ...transactionOptions } = opts; + _connection = neon(url, transactionOptions); + return _connection; + } + + const query: InternalQuery = async (sql, params) => { + const connection = getConnection(); + return connection.query(normalizeParams(sql), params); + }; + + return { + name: "neon", + dialect: "postgresql", + getInstance: () => getConnection(), + 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[]) { + return this.#query(this.#sql, params); + } + + 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[0]; + } +} diff --git a/test/connectors/neon.test.ts b/test/connectors/neon.test.ts new file mode 100644 index 00000000..2cf2e35e --- /dev/null +++ b/test/connectors/neon.test.ts @@ -0,0 +1,28 @@ +import { vi, describe } from "vitest"; +import { testConnector } from "./_tests"; + +vi.mock("@neondatabase/serverless", async () => { + const { PGlite } = await import("@electric-sql/pglite"); + const pglite = await PGlite.create(); + return { + neon: () => { + const queryFn = async (sql: string, params?: unknown[]) => { + const result = await pglite.query(sql, params); + return result.rows; + }; + queryFn.query = queryFn; + return queryFn; + }, + }; +}); + +const { default: neonConnector } = await import("../../src/connectors/neon"); + +describe("connectors: neon (mocked with pglite)", () => { + testConnector({ + dialect: "postgresql", + connector: neonConnector({ + url: "postgresql://mock@localhost/mock", + }), + }); +});