@shivaedev/platform 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.24.0 - 2026-10-21
4
+
5
+ ### Added
6
+
7
+ - Expose test-only Better Auth database readouts and faults for the default auth schema so application stories do not issue SQL against provider-owned tables.
8
+
9
+ ## 0.23.1 - 2026-10-19
10
+
11
+ ### Fixed
12
+
13
+ - Wait for application database query and listener sockets to disconnect before host and worker scopes finish closing.
14
+
3
15
  ## 0.23.0 - 2026-10-19
4
16
 
5
17
  ### Added
package/README.md CHANGED
@@ -631,6 +631,8 @@ Without `signIn`, the mount installs no hooks, `admittedSessions` returns the lo
631
631
 
632
632
  Use `authTestUtils()` from `@shivaedev/platform/server/better-auth/testing.ts` in a test-only Better Auth instance with the host's database, secret and cookie policy. The wrapper preserves the plugin's typed `$context.test` under `exactOptionalPropertyTypes`. Keep the plugin out of production auth options.
633
633
 
634
+ Use `authTestDatabase(pool)` from `@shivaedev/platform/server/better-auth/testDatabase.ts` when a story needs observations or faults in the default Better Auth PostgreSQL schema. It returns Effect readouts `users` (IDs and emails) and `sessions` (IDs, user IDs and emails), plus the test-only faults `expireSessions` and `omitImageColumn`. It never returns tokens, password hashes or provider credentials. These fixtures assume the default table and column mappings and operate on the supplied pool; use only an isolated test database. Application stories keep their auth SQL in this provider-owned boundary.
635
+
634
636
  ```ts
635
637
  import { betterAuth } from "better-auth";
636
638
  import { authTestUtils, signedInUser } from "@shivaedev/platform/server/better-auth/testing.ts";
@@ -747,6 +749,7 @@ Import the module that defines the name; there is no package-root entry. Paths b
747
749
  | `server/better-auth/allowedAccounts.ts` | `AllowedAccounts`, `allowedAccounts`: the provider-account list Schema and its `AUTH_ALLOWED_ACCOUNTS` Config. |
748
750
  | `server/better-auth/signInGate.ts` | `SignInPolicy`: the `accounts`, `rule` and `recheckInterval` of a sign-in policy. |
749
751
  | `server/better-auth/revokeUnadmitted.ts` | `RevokedSessions`: the `{ users, sessions }` counts of a revocation. |
752
+ | `server/better-auth/testDatabase.ts` | `authTestDatabase(pool)`: test-only identity/session readouts and faults for the default PostgreSQL auth schema. |
750
753
  | `server/better-auth/testing.ts` | `authTestUtils(options?)`, `signedInUser(auth, fields)`: test-only typed Better Auth helpers and minted user sessions. |
751
754
  | `server/better-auth/adapter.ts` | `effectPrismaAdapter(Database, runtime, options?)`: a Better Auth adapter over Effect Prisma; `modelName` maps model names. |
752
755
  | `server/testing/makePlatformIt.ts` | `makePlatformIt(Database)({ adapter, createCaller, layer, extend? })`. |
@@ -0,0 +1,16 @@
1
+ import { Effect } from "effect";
2
+ import type { Pool } from "pg";
3
+ export declare function authTestDatabase(pool: Pool): {
4
+ expireSessions: Effect.Effect<void, import("effect/Cause").UnknownError, never>;
5
+ omitImageColumn: Effect.Effect<void, import("effect/Cause").UnknownError, never>;
6
+ sessions: Effect.Effect<{
7
+ email: string;
8
+ id: string;
9
+ userId: string;
10
+ }[], import("effect/Cause").UnknownError, never>;
11
+ users: Effect.Effect<{
12
+ email: string;
13
+ id: string;
14
+ }[], import("effect/Cause").UnknownError, never>;
15
+ };
16
+ //# sourceMappingURL=testDatabase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testDatabase.d.ts","sourceRoot":"","sources":["../../../src/server/better-auth/testDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI;IAEzC,cAAc;IAGd,eAAe;IAGf,QAAQ;eACkC,MAAM;YAAM,MAAM;gBAAU,MAAM;;IAK5E,KAAK;eACqC,MAAM;YAAM,MAAM;;EAI7D"}
@@ -0,0 +1,20 @@
1
+ import { Effect } from "effect";
2
+ export function authTestDatabase(pool) {
3
+ return {
4
+ expireSessions: Effect.tryPromise(async () => {
5
+ await pool.query('update "session" set "expiresAt" = now() - interval \'1 second\'');
6
+ }),
7
+ omitImageColumn: Effect.tryPromise(async () => {
8
+ await pool.query('alter table "user" drop column image');
9
+ }),
10
+ sessions: Effect.tryPromise(async () => {
11
+ const result = await pool.query('select s.id, s."userId", u.email from "session" s join "user" u on u.id = s."userId" order by u.email, s.id');
12
+ return result.rows;
13
+ }),
14
+ users: Effect.tryPromise(async () => {
15
+ const result = await pool.query('select id, email from "user" order by email');
16
+ return result.rows;
17
+ }),
18
+ };
19
+ }
20
+ //# sourceMappingURL=testDatabase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testDatabase.js","sourceRoot":"","sources":["../../../src/server/better-auth/testDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,MAAM,UAAU,gBAAgB,CAAC,IAAU;IAC1C,OAAO;QACN,cAAc,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK,IAAI,EAAE;YAC5C,MAAM,IAAI,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAC;QACtF,CAAC,CAAC;QACF,eAAe,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK,IAAI,EAAE;YAC7C,MAAM,IAAI,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC1D,CAAC,CAAC;QACF,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK,IAAI,EAAE;YACtC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC9B,6GAA6G,CAC7G,CAAC;YACF,OAAO,MAAM,CAAC,IAAI,CAAC;QACpB,CAAC,CAAC;QACF,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK,IAAI,EAAE;YACnC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAgC,6CAA6C,CAAC,CAAC;YAC9G,OAAO,MAAM,CAAC,IAAI,CAAC;QACpB,CAAC,CAAC;KACF,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"databaseLayer.d.ts","sourceRoot":"","sources":["../../src/server/databaseLayer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAU,KAAK,EAAE,QAAQ,EAAY,MAAM,QAAQ,CAAC;AACnE,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAG3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAEhF,OAAO,EAAE,KAAK,sBAAsB,EAAmB,MAAM,0CAA0C,CAAC;AACxG,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGzE,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACxD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,UAAU,+UA4BjG"}
1
+ {"version":3,"file":"databaseLayer.d.ts","sourceRoot":"","sources":["../../src/server/databaseLayer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAU,KAAK,EAAE,QAAQ,EAAY,MAAM,QAAQ,CAAC;AACnE,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAG3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAEhF,OAAO,EAAE,KAAK,sBAAsB,EAAmB,MAAM,0CAA0C,CAAC;AACxG,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGzE,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACxD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,UAAU,+UA8CjG"}
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { createConnection } from "node:net";
2
3
  import * as PgClient from "@effect/sql-pg/PgClient";
3
4
  import { Config, Effect, Layer, Redacted, Schedule } from "effect";
4
5
  import * as Reactivity from "effect/reactivity/Reactivity";
@@ -10,11 +11,27 @@ import { databaseAddress } from "./databaseAddress.js";
10
11
  export function databaseLayer(options, changesService) {
11
12
  const pool = Layer.unwrap(Effect.gen(function* () {
12
13
  const url = options.config?.databaseUrl ?? (yield* Config.Redacted("DATABASE_URL"));
14
+ const address = databaseAddress(url);
15
+ const disconnected = new Set();
16
+ // Native pool shutdown sends Terminate without waiting for its sockets to close.
17
+ yield* Effect.addFinalizer(() => Effect.promise(() => Promise.all([...disconnected])));
13
18
  return PgClient.layer({
19
+ stream: () => {
20
+ const target = new URL(Redacted.value(url));
21
+ const host = address.host ?? target.searchParams.getAll("host").at(-1) ?? (decodeURIComponent(target.hostname) || "localhost");
22
+ const port = address.port ?? 5432;
23
+ const socket = createConnection(host.startsWith("/") ? { path: `${host}/.s.PGSQL.${port}` } : { host, noDelay: true, port });
24
+ const closed = new Promise((resolve) => socket.once("close", () => {
25
+ disconnected.delete(closed);
26
+ resolve();
27
+ }));
28
+ disconnected.add(closed);
29
+ return socket;
30
+ },
14
31
  transformQueryNames: options.config?.transformQueryNames,
15
32
  transformResultNames: options.config?.transformResultNames,
16
33
  url,
17
- ...databaseAddress(url),
34
+ ...address,
18
35
  ...(options.config?.maxConnections === undefined ? {} : { maxConnections: options.config.maxConnections }),
19
36
  ...(options.config?.applicationName === undefined ? {} : { applicationName: options.config.applicationName }),
20
37
  });
@@ -1 +1 @@
1
- {"version":3,"file":"databaseLayer.js","sourceRoot":"","sources":["../../src/server/databaseLayer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,8CAA8C,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAC;AAE7F,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACtE,OAAO,EAA+B,eAAe,EAAE,MAAM,0CAA0C,CAAC;AAExG,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AASvD,MAAM,UAAU,aAAa,CAAI,OAA8B,EAAE,cAAiC;IACjG,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CACxB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,WAAW,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC;QACpF,OAAO,QAAQ,CAAC,KAAK,CAAC;YACrB,mBAAmB,EAAE,OAAO,CAAC,MAAM,EAAE,mBAAmB;YACxD,oBAAoB,EAAE,OAAO,CAAC,MAAM,EAAE,oBAAoB;YAC1D,GAAG;YACH,GAAG,eAAe,CAAC,GAAG,CAAC;YACvB,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;YAC1G,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;SAC7G,CAAC,CAAC;IACJ,CAAC,CAAC,CACF,CAAC;IACF,MAAM,QAAQ,GAAG,KAAK,CAAC,aAAa,CAAC,iBAAiB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3G,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;IACrG,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC;IACxC,OAAO,KAAK,CAAC,MAAM,CAClB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC;QACpC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,WAAW,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC;QACpF,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC9G,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,WAAW,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAC9G,OAAO,eAAe,CAAC,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/E,CAAC,CAAC,CACF,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,iBAAiB,CAAI,UAA+C;IAC5E,QAAQ,UAAU,CAAC,IAAI,EAAE,CAAC;QACzB,KAAK,OAAO;YACX,OAAO,wBAAwB,CAAC,UAAU,CAAC,CAAC;QAC7C,KAAK,MAAM;YACV,OAAO,iBAAiB,CAAC,UAAU,CAAC,CAAC;QACtC;YACC,OAAO,eAAe,CAAC,EAAE,GAAG,UAAU,EAAE,WAAW,EAAE,UAAU,CAAC,WAAW,IAAI,WAAW,EAAE,CAAC,CAAC;IAChG,CAAC;AACF,CAAC;AAED,SAAS,iBAAiB,CAAI,UAA+C;IAC5E,OAAO,wBAAwB,CAAC,UAAU,CAAC,CAAC,IAAI,CAC/C,MAAM,CAAC,KAAK,CAAC;QACZ,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,IAAI,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,IAAI,WAAW,CAAC,CAAC,CAAC;QACxI,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,gBAAgB,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;KAC9E,CAAC,CACF,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"databaseLayer.js","sourceRoot":"","sources":["../../src/server/databaseLayer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/platform",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Opinionated Effect application infrastructure for ShivaeDev services",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/platform#readme",
6
6
  "bugs": {
@@ -58,8 +58,8 @@
58
58
  "@shivaedev/effect-test": "0.7.1",
59
59
  "@shivaedev/effect-test-sql": "0.8.2",
60
60
  "@shivaedev/effect-trpc": "0.8.2",
61
- "@shivaedev/quality": "0.31.0",
62
- "@shivaedev/test-story": "0.17.0",
61
+ "@shivaedev/quality": "0.33.1",
62
+ "@shivaedev/test-story": "0.18.0",
63
63
  "@trpc/client": "11.19.0",
64
64
  "@trpc/server": "11.19.0",
65
65
  "@types/node": "24.19.2",
@@ -69,7 +69,7 @@
69
69
  "pg": "8.23.0",
70
70
  "superjson": "2.2.6",
71
71
  "typescript": "7.0.2",
72
- "vitest": "5.0.3"
72
+ "vitest": "5.0.4"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "@effect/platform-node": "4.0.4",
@@ -0,0 +1,23 @@
1
+ import { Effect } from "effect";
2
+ import type { Pool } from "pg";
3
+
4
+ export function authTestDatabase(pool: Pool) {
5
+ return {
6
+ expireSessions: Effect.tryPromise(async () => {
7
+ await pool.query('update "session" set "expiresAt" = now() - interval \'1 second\'');
8
+ }),
9
+ omitImageColumn: Effect.tryPromise(async () => {
10
+ await pool.query('alter table "user" drop column image');
11
+ }),
12
+ sessions: Effect.tryPromise(async () => {
13
+ const result = await pool.query<{ email: string; id: string; userId: string }>(
14
+ 'select s.id, s."userId", u.email from "session" s join "user" u on u.id = s."userId" order by u.email, s.id',
15
+ );
16
+ return result.rows;
17
+ }),
18
+ users: Effect.tryPromise(async () => {
19
+ const result = await pool.query<{ email: string; id: string }>('select id, email from "user" order by email');
20
+ return result.rows;
21
+ }),
22
+ };
23
+ }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { createConnection } from "node:net";
2
3
  import * as PgClient from "@effect/sql-pg/PgClient";
3
4
  import { Config, Effect, Layer, Redacted, Schedule } from "effect";
4
5
  import * as Reactivity from "effect/reactivity/Reactivity";
@@ -21,11 +22,29 @@ export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService:
21
22
  const pool = Layer.unwrap(
22
23
  Effect.gen(function* () {
23
24
  const url = options.config?.databaseUrl ?? (yield* Config.Redacted("DATABASE_URL"));
25
+ const address = databaseAddress(url);
26
+ const disconnected = new Set<Promise<void>>();
27
+ // Native pool shutdown sends Terminate without waiting for its sockets to close.
28
+ yield* Effect.addFinalizer(() => Effect.promise(() => Promise.all([...disconnected])));
24
29
  return PgClient.layer({
30
+ stream: () => {
31
+ const target = new URL(Redacted.value(url));
32
+ const host = address.host ?? target.searchParams.getAll("host").at(-1) ?? (decodeURIComponent(target.hostname) || "localhost");
33
+ const port = address.port ?? 5432;
34
+ const socket = createConnection(host.startsWith("/") ? { path: `${host}/.s.PGSQL.${port}` } : { host, noDelay: true, port });
35
+ const closed = new Promise<void>((resolve) =>
36
+ socket.once("close", () => {
37
+ disconnected.delete(closed);
38
+ resolve();
39
+ }),
40
+ );
41
+ disconnected.add(closed);
42
+ return socket;
43
+ },
25
44
  transformQueryNames: options.config?.transformQueryNames,
26
45
  transformResultNames: options.config?.transformResultNames,
27
46
  url,
28
- ...databaseAddress(url),
47
+ ...address,
29
48
  ...(options.config?.maxConnections === undefined ? {} : { maxConnections: options.config.maxConnections }),
30
49
  ...(options.config?.applicationName === undefined ? {} : { applicationName: options.config.applicationName }),
31
50
  });