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"} diff --git a/package.json b/package.json index af8793b..848b22c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@cloudflare/workers-types": "^4.20251120.0", "@electric-sql/pglite": "^0.3.14", "@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.2", @@ -58,6 +59,7 @@ "drizzle-orm": "^0.44.7", "eslint": "^9.39.1", "eslint-config-unjs": "^0.5.0", + "get-db": "^0.9.2", "jiti": "^2.6.1", "mlly": "^1.8.0", "mysql2": "^3.15.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dff25f4..84227be 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,13 +56,16 @@ importers: version: 17.2.3 drizzle-orm: specifier: ^0.44.7 - version: 0.44.7(@cloudflare/workers-types@4.20251120.0)(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.20251120.0)(@electric-sql/pglite@0.3.14)(@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.6)(better-sqlite3@12.4.1)(bun-types@1.3.2(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7) eslint: specifier: ^9.39.1 version: 9.39.1(jiti@2.6.1) eslint-config-unjs: specifier: ^0.5.0 version: 0.5.0(eslint@9.39.1(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 @@ -95,13 +101,13 @@ importers: devDependencies: db0: specifier: latest - version: 0.3.4(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251120.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.14)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251120.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.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.20251120.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.20251120.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.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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 @@ -137,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'} @@ -1048,6 +1060,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'} @@ -1667,6 +1683,9 @@ packages: '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + '@types/node@22.18.12': + resolution: {integrity: sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==} + '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} @@ -1676,6 +1695,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==} @@ -2147,6 +2169,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'} @@ -2586,6 +2612,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.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} @@ -2627,6 +2658,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==} @@ -3127,6 +3162,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'} @@ -3503,6 +3546,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==} @@ -3514,6 +3560,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'} @@ -3556,6 +3605,9 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -3790,6 +3842,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 @@ -4380,6 +4443,11 @@ snapshots: '@neon-rs/load@0.0.4': {} + '@neondatabase/serverless@1.0.2': + dependencies: + '@types/node': 22.18.12 + '@types/pg': 8.15.6 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4772,6 +4840,10 @@ snapshots: dependencies: '@types/unist': 2.0.11 + '@types/node@22.18.12': + dependencies: + undici-types: 6.21.0 + '@types/node@24.10.1': dependencies: undici-types: 7.16.0 @@ -4786,6 +4858,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/tinycolor2@1.4.6': {} + '@types/unist@2.0.11': {} '@types/ws@8.18.1': @@ -5266,12 +5340,12 @@ snapshots: data-uri-to-buffer@4.0.1: {} - db0@0.3.4(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251120.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.14)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251120.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.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.14 '@libsql/client': 0.15.15 better-sqlite3: 12.4.1 - drizzle-orm: 0.29.5(@cloudflare/workers-types@4.20251120.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.20251120.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.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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 @@ -5321,6 +5395,8 @@ snapshots: dependencies: heap: 0.2.7 + dotenv@16.6.1: {} + dotenv@17.2.3: {} dreamopt@0.8.0: @@ -5349,10 +5425,11 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.29.5(@cloudflare/workers-types@4.20251120.0)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.20251120.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.6)(@types/react@19.1.13)(better-sqlite3@12.4.1)(bun-types@1.3.2(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7): optionalDependencies: '@cloudflare/workers-types': 4.20251120.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.6 @@ -5363,11 +5440,12 @@ snapshots: pg: 8.16.3 sqlite3: 5.1.7 - drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251120.0)(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.4.1)(bun-types@1.3.2(@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.20251120.0)(@electric-sql/pglite@0.3.14)(@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.6)(better-sqlite3@12.4.1)(bun-types@1.3.2(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7): optionalDependencies: '@cloudflare/workers-types': 4.20251120.0 '@electric-sql/pglite': 0.3.14 '@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.6 @@ -5771,6 +5849,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.13.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -5821,6 +5908,11 @@ snapshots: graceful-fs@4.2.11: optional: true + gradient-string@3.0.0: + dependencies: + chalk: 5.6.2 + tinygradient: 1.1.5 + graphemer@1.4.0: {} hanji@0.0.5: @@ -6437,6 +6529,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 @@ -6867,6 +6965,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} tinyexec@1.0.2: {} @@ -6876,6 +6976,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: @@ -6914,6 +7019,8 @@ snapshots: ufo@1.6.1: {} + undici-types@6.21.0: {} + undici-types@7.16.0: {} undici@7.14.0: {} 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/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/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) +