@thomasfosterau/effect-drizzle-ext-pg 0.0.0 → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,109 @@
1
+ import * as PgClient from "@effect/sql-pg/PgClient";
2
+ import { makeDatabaseServices } from "@thomasfosterau/effect-drizzle-ext/database";
3
+ import * as PgDrizzle from "drizzle-orm/effect-postgres";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ // src/index.ts
7
+ /**
8
+ * @copyright Thomas Foster
9
+ *
10
+ * The `@effect/sql-pg` binding of the driver-agnostic service factory: one
11
+ * call to {@link makePgDatabase} gives an application the `Database` and
12
+ * `CurrentTransaction` services, the `transact` write boundary and the
13
+ * `readHandle` resolver, all typed over its own `relations` graph, plus the
14
+ * `Effect` and `Layer` that build the Drizzle client itself.
15
+ *
16
+ * `drizzle-orm/effect-postgres` already ships the Effect-native Drizzle
17
+ * driver; nothing here re-implements it. What this module adds is the two
18
+ * pieces every consuming application was spelling out by hand:
19
+ *
20
+ * - the `Layer.effect(Database, PgDrizzle.make({ relations }).pipe(provide(
21
+ * DefaultServices)))` stanza, which appeared four times per application
22
+ * (request path, event path, test harness, proxied test harness), and
23
+ * - the choice of `PgClient` construction, which is a real decision rather
24
+ * than a detail — see {@link pgClientFromUrl} and {@link pgClientFromPool}.
25
+ *
26
+ * Deployment topology stays with the consuming application: resolving a
27
+ * connection string from a platform binding, degrading a failed binding to a
28
+ * chainable proxy client, and any proxy wrapper around the concrete client
29
+ * are all things only the application knows about.
30
+ *
31
+ * @module index
32
+ */
33
+ /**
34
+ * Builds one application's {@link PgDatabase} from its relations graph and a
35
+ * service key.
36
+ *
37
+ * Call it once, at module scope, and export what it returns; the keys are
38
+ * derived from `key`, so calling it twice with the same key produces services
39
+ * that share a runtime slot.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { makePgDatabase } from "@thomasfosterau/effect-drizzle-ext-pg";
44
+ *
45
+ * const relations = {}; // the application's drizzle relations graph
46
+ * const database = makePgDatabase({ relations, key: "Database" });
47
+ * export const { Database, CurrentTransaction, transact, readHandle } = database;
48
+ * export type Database = typeof database.Database.Identifier; // "Database"
49
+ *
50
+ * console.log(Database.key); // "Database"
51
+ * ```
52
+ */
53
+ const makePgDatabase = (options) => {
54
+ const services = makeDatabaseServices()({ key: options.key });
55
+ const make = PgDrizzle.make({ relations: options.relations }).pipe(Effect.provide(PgDrizzle.DefaultServices));
56
+ return {
57
+ ...services,
58
+ make,
59
+ layer: Layer.effect(services.Database, make)
60
+ };
61
+ };
62
+ /**
63
+ * A `PgClient` layer over a connection URL that **pings the database
64
+ * (`SELECT 1`) at layer BUILD**, failing the build with a `SqlError` if the
65
+ * ping does not answer within `connectTimeout`.
66
+ *
67
+ * That is the right trade for a long-lived process — a misconfigured URL
68
+ * fails once, loudly, at startup rather than on the first query — and the
69
+ * wrong one for a runtime that is built per request, where it opens a
70
+ * connection even for requests that never touch the database. Use
71
+ * {@link pgClientFromPool} there.
72
+ *
73
+ * `@effect/sql-pg` 4.0.0-rc.115 replaced the `pg` driver with its own wire
74
+ * protocol, and its `PgClient.layer` no longer pings on its own: the pool
75
+ * opens connections on first use. The ping is therefore this function's, run
76
+ * once over the freshly built client.
77
+ *
78
+ * The output includes `SqlClient` as well as `PgClient` because that is what
79
+ * `PgClient.layerFrom` provides; a consumer that only needs `PgClient` can
80
+ * ignore the extra service.
81
+ */
82
+ const pgClientFromUrl = (config) => PgClient.layerFrom(Effect.tap(PgClient.make(config), (client) => client`SELECT 1`));
83
+ /**
84
+ * A `PgClient` layer over a connection URL that **does not connect at layer
85
+ * build** — the lazy counterpart to {@link pgClientFromUrl}.
86
+ *
87
+ * The pool is `@effect/sql-pg`'s own: it opens connections on first use (up
88
+ * to `maxConnections`) and is shut down, every session released, when the
89
+ * layer's scope closes. A runtime built per request therefore disposes of its
90
+ * pool with that request, and a connection failure surfaces on the first
91
+ * query as a `SqlError` instead of at build. Platforms that pin I/O to the
92
+ * event that created it require exactly this — a pool must never outlive
93
+ * that event (website #455).
94
+ *
95
+ * Until `@effect/sql-pg` 4.0.0-rc.115 this wrapped a bare `pg.Pool` via
96
+ * `PgClient.fromPool`, because `PgClient.layer` pinged at build; rc.115
97
+ * dropped both the `pg` driver and `fromPool`, and its pool is lazy by
98
+ * construction. The name is kept for the contract, not the mechanism.
99
+ */
100
+ const pgClientFromPool = (options) => PgClient.layer({
101
+ url: options.connectionString,
102
+ maxConnections: options.maxConnections,
103
+ connectTimeout: options.connectTimeout,
104
+ types: options.types,
105
+ prepare: options.prepare
106
+ });
107
+ export { makePgDatabase, pgClientFromPool, pgClientFromUrl };
108
+
109
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiIA,MAAa,kBAA4E,YAGvD;CAChC,MAAM,WAAW,qBAA4E,CAAC,CAAC,EAC7F,KAAK,QAAQ,IACf,CAAC;CAED,MAAM,OAIF,UAAU,KAAgB,EAAE,WAAW,QAAQ,UAAU,CAAC,CAAC,CAAC,KAC9D,OAAO,QAAQ,UAAU,eAAe,CAC1C;CAEA,OAAO;EAAE,GAAG;EAAU;EAAM,OAAO,MAAM,OAAO,SAAS,UAAU,IAAI;CAAE;AAC3E;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,mBAAmB,WAiB9B,SAAS,UAAU,OAAO,IAAI,SAAS,KAAK,MAAM,IAAI,WAAW,MAAM,UAAU,CAAC;;;;;;;;;;;;;;;;;;AAoBpF,MAAa,oBAAoB,YAiB/B,SAAS,MAAM;CACb,KAAK,QAAQ;CACb,gBAAgB,QAAQ;CACxB,gBAAgB,QAAQ;CACxB,OAAO,QAAQ;CACf,SAAS,QAAQ;AACnB,CAAC"}
@@ -0,0 +1,68 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Path from "effect/Path";
4
+ import * as Schema from "effect/Schema";
5
+ import { SqlClient } from "effect/sql/SqlClient";
6
+ import { PlatformError } from "effect/PlatformError";
7
+ /** drizzle-kit's statement separator inside a `migration.sql`. */
8
+ declare const STATEMENT_BREAKPOINT = "--> statement-breakpoint";
9
+ /** One migration as the runner applies it: its directory name and its statements, in order. */
10
+ interface Migration {
11
+ readonly name: string;
12
+ readonly statements: ReadonlyArray<string>;
13
+ }
14
+ declare const MigrationError_base: Schema.Class<MigrationError, Schema.TaggedStruct<"MigrationError", {
15
+ readonly migration: Schema.String;
16
+ readonly cause: Schema.Defect;
17
+ }>, import("effect/Cause").YieldableError>;
18
+ /**
19
+ * A migration failed to apply. `migration` names the directory — a bare
20
+ * driver error says only which statement broke, not which file it came from
21
+ * — and `cause` is the driver's `SqlError`. The migration's transaction has
22
+ * been rolled back; the ones before it stay applied.
23
+ */
24
+ declare class MigrationError extends MigrationError_base {
25
+ get message(): string;
26
+ }
27
+ /**
28
+ * Split one `migration.sql` into its statements on
29
+ * {@link STATEMENT_BREAKPOINT}, dropping the empty chunks a trailing marker
30
+ * or blank file leaves behind.
31
+ */
32
+ declare const splitStatements: (sql: string) => ReadonlyArray<string>;
33
+ /**
34
+ * Every migration under `dir`, in apply order (directory-name order). Only
35
+ * directories count; one with no `migration.sql` is skipped.
36
+ */
37
+ declare const readMigrations: (dir: string) => Effect.Effect<ReadonlyArray<Migration>, PlatformError, FileSystem.FileSystem | Path.Path>;
38
+ /**
39
+ * Apply `migrations` in order on the ambient `SqlClient`, each inside its own
40
+ * transaction, returning how many ran. Stops at the first failure with a
41
+ * {@link MigrationError} naming the migration.
42
+ */
43
+ declare const applyMigrationList: (migrations: ReadonlyArray<Migration>) => Effect.Effect<number, MigrationError, SqlClient>;
44
+ /**
45
+ * Apply every migration under `dir` to the (brand-new) database behind the
46
+ * ambient `SqlClient` — {@link readMigrations} then {@link applyMigrationList}
47
+ * — returning how many ran.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import * as NodeServices from "@effect/platform-node/NodeServices";
52
+ * import * as PgClient from "@effect/sql-pg/PgClient";
53
+ * import { applyMigrations } from "@thomasfosterau/effect-drizzle-ext-pg/migrations";
54
+ * import * as Effect from "effect/Effect";
55
+ * import * as Redacted from "effect/Redacted";
56
+ *
57
+ * const url = Redacted.make("postgres://postgres@localhost:5432/app_test");
58
+ *
59
+ * // Building the Effect connects to nothing; running it applies the chain.
60
+ * export const applied = applyMigrations("packages/database/migrations").pipe(
61
+ * Effect.provide(PgClient.layer({ url, maxConnections: 1 })),
62
+ * Effect.provide(NodeServices.layer),
63
+ * );
64
+ * ```
65
+ */
66
+ declare const applyMigrations: (dir: string) => Effect.Effect<number, MigrationError | PlatformError, SqlClient | FileSystem.FileSystem | Path.Path>;
67
+ export { applyMigrations as a, applyMigrationList as i, MigrationError as n, readMigrations as o, STATEMENT_BREAKPOINT as r, splitStatements as s, Migration as t };
68
+ //# sourceMappingURL=migrations-Dp67FrXh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations-Dp67FrXh.d.ts","names":[],"sources":["../src/migrations.ts"],"mappings":";;;;;;;cAqDa;;UAGI;WACN;WACA,YAAY;;;;;;;;;;;;cASV,uBAAuB;MAIrB;;;;;;;cAUF,kBAAe,gBAAkB;;;;;cAUjC,iBAAc,gBAExB,OAAO,OAAO,cAAc,YAAY,eAAe,WAAW,aAAa,KAAK;;;;;;cA0B1E,qBAAkB,YACjB,cAAc,eACzB,OAAO,eAAe,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;cA6C5B,kBAAe,gBAEzB,OAAO,eAER,iBAAiB,eACjB,YAAY,WAAW,aAAa,KAAK"}
@@ -0,0 +1,2 @@
1
+ import { a as applyMigrations, i as applyMigrationList, n as MigrationError, o as readMigrations, r as STATEMENT_BREAKPOINT, s as splitStatements, t as Migration } from "./migrations-Dp67FrXh.js";
2
+ export { Migration, MigrationError, STATEMENT_BREAKPOINT, applyMigrationList, applyMigrations, readMigrations, splitStatements };
@@ -0,0 +1,136 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Arr from "effect/Array";
3
+ import * as FileSystem from "effect/FileSystem";
4
+ import * as Order from "effect/Order";
5
+ import * as Path from "effect/Path";
6
+ import * as Schema from "effect/Schema";
7
+ import { SqlClient } from "effect/sql/SqlClient";
8
+ import * as Str from "effect/String";
9
+ // src/migrations.ts
10
+ /**
11
+ * @copyright Thomas Foster
12
+ *
13
+ * **The ordered migration runner for a brand-new database.** Reads a
14
+ * drizzle-kit `migrations/` directory and applies every migration in it, in
15
+ * directory-name order, one transaction per migration, with no ledger.
16
+ *
17
+ * It is what an integration suite's provisioning runs against the database it
18
+ * has just created ({@link ../testing.ts}'s `provisionTestDatabase` calls it by
19
+ * default), and it mirrors the apply order and per-migration transaction
20
+ * shape of alchemy's deploy-time `migrationsDir` runner, minus its ledger:
21
+ * the database is **always** brand new, so every migration applies
22
+ * unconditionally and no bookkeeping table is needed. A database that already
23
+ * holds some of the migrations is not this runner's input — use the
24
+ * deployment's own migrator there.
25
+ *
26
+ * ## The layout it reads
27
+ *
28
+ * drizzle-kit writes one directory per migration, `<digits>_<slug>/`,
29
+ * holding `migration.sql` (and `snapshot.json`, which this runner ignores).
30
+ * The all-digit, fixed-width prefixes make name order chronological. A
31
+ * directory with no `migration.sql` is skipped rather than failing the run —
32
+ * drizzle-kit writes the snapshot before the SQL, and a sanctioned
33
+ * hand-written bootstrap directory may carry only SQL.
34
+ *
35
+ * ## Statements, not files
36
+ *
37
+ * `@effect/sql-pg` speaks the extended query protocol, which rejects more
38
+ * than one statement per query ("cannot insert multiple commands into a
39
+ * prepared statement"). drizzle-kit separates the statements of a migration
40
+ * with a `--> statement-breakpoint` line, and drizzle's own migrator splits
41
+ * on exactly that marker, so this runner does the same: each migration's
42
+ * statements run in order inside that migration's one transaction. A
43
+ * hand-written migration must keep the marker between statements.
44
+ *
45
+ * Idiomatic Effect: the directory is read through the `FileSystem` / `Path`
46
+ * services (a Node consumer provides `@effect/platform-node`'s
47
+ * `NodeServices.layer`), and the statements run on the ambient `SqlClient`.
48
+ *
49
+ * @module migrations
50
+ */
51
+ /** drizzle-kit's statement separator inside a `migration.sql`. */
52
+ const STATEMENT_BREAKPOINT = "--> statement-breakpoint";
53
+ /**
54
+ * A migration failed to apply. `migration` names the directory — a bare
55
+ * driver error says only which statement broke, not which file it came from
56
+ * — and `cause` is the driver's `SqlError`. The migration's transaction has
57
+ * been rolled back; the ones before it stay applied.
58
+ */
59
+ var MigrationError = class extends Schema.TaggedError()("MigrationError", {
60
+ migration: Schema.String,
61
+ cause: Schema.Defect()
62
+ }) {
63
+ get message() {
64
+ return `migration '${this.migration}' failed`;
65
+ }
66
+ };
67
+ /**
68
+ * Split one `migration.sql` into its statements on
69
+ * {@link STATEMENT_BREAKPOINT}, dropping the empty chunks a trailing marker
70
+ * or blank file leaves behind.
71
+ */
72
+ const splitStatements = (sql) => sql.split(STATEMENT_BREAKPOINT).map((statement) => statement.trim()).filter(Str.isNonEmpty);
73
+ /**
74
+ * Every migration under `dir`, in apply order (directory-name order). Only
75
+ * directories count; one with no `migration.sql` is skipped.
76
+ */
77
+ const readMigrations = (dir) => Effect.gen(function* () {
78
+ const fs = yield* FileSystem.FileSystem;
79
+ const path = yield* Path.Path;
80
+ const names = Arr.sort(yield* fs.readDirectory(dir), Order.String);
81
+ return (yield* Effect.forEach(names, (name) => Effect.gen(function* () {
82
+ if ((yield* fs.stat(path.join(dir, name))).type !== "Directory") return [];
83
+ const file = path.join(dir, name, "migration.sql");
84
+ if (!(yield* fs.exists(file))) return [];
85
+ const sql = yield* fs.readFileString(file);
86
+ return [{
87
+ name,
88
+ statements: splitStatements(sql)
89
+ }];
90
+ }), { concurrency: 1 })).flat();
91
+ });
92
+ /**
93
+ * Apply `migrations` in order on the ambient `SqlClient`, each inside its own
94
+ * transaction, returning how many ran. Stops at the first failure with a
95
+ * {@link MigrationError} naming the migration.
96
+ */
97
+ const applyMigrationList = (migrations) => Effect.gen(function* () {
98
+ const sql = yield* SqlClient;
99
+ yield* Effect.forEach(migrations, (migration) => sql.withTransaction(Effect.forEach(migration.statements, (statement) => sql.unsafe(statement), {
100
+ concurrency: 1,
101
+ discard: true
102
+ })).pipe(Effect.mapError((cause) => new MigrationError({
103
+ migration: migration.name,
104
+ cause
105
+ }))), {
106
+ concurrency: 1,
107
+ discard: true
108
+ });
109
+ return migrations.length;
110
+ });
111
+ /**
112
+ * Apply every migration under `dir` to the (brand-new) database behind the
113
+ * ambient `SqlClient` — {@link readMigrations} then {@link applyMigrationList}
114
+ * — returning how many ran.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * import * as NodeServices from "@effect/platform-node/NodeServices";
119
+ * import * as PgClient from "@effect/sql-pg/PgClient";
120
+ * import { applyMigrations } from "@thomasfosterau/effect-drizzle-ext-pg/migrations";
121
+ * import * as Effect from "effect/Effect";
122
+ * import * as Redacted from "effect/Redacted";
123
+ *
124
+ * const url = Redacted.make("postgres://postgres@localhost:5432/app_test");
125
+ *
126
+ * // Building the Effect connects to nothing; running it applies the chain.
127
+ * export const applied = applyMigrations("packages/database/migrations").pipe(
128
+ * Effect.provide(PgClient.layer({ url, maxConnections: 1 })),
129
+ * Effect.provide(NodeServices.layer),
130
+ * );
131
+ * ```
132
+ */
133
+ const applyMigrations = (dir) => Effect.flatMap(readMigrations(dir), applyMigrationList);
134
+ export { MigrationError, STATEMENT_BREAKPOINT, applyMigrationList, applyMigrations, readMigrations, splitStatements };
135
+
136
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","names":[],"sources":["../src/migrations.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,MAAa,uBAAuB;;;;;;;AAcpC,IAAa,iBAAb,cAAoC,OAAO,YAA4B,CAAC,CAAC,kBAAkB;CACzF,WAAW,OAAO;CAClB,OAAO,OAAO,OAAO;AACvB,CAAC,CAAC,CAAC;CACD,IAAa,UAAkB;EAC7B,OAAO,cAAc,KAAK,UAAU;CACtC;AACF;;;;;;AAOA,MAAa,mBAAmB,QAC9B,IACG,MAAM,oBAAoB,CAAC,CAC3B,KAAK,cAAc,UAAU,KAAK,CAAC,CAAC,CACpC,OAAO,IAAI,UAAU;;;;;AAM1B,MAAa,kBACX,QAEA,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,QAAQ,IAAI,KAAK,OAAO,GAAG,cAAc,GAAG,GAAG,MAAM,MAAM;CAcjE,QAAO,OAba,OAAO,QACzB,QACC,SACC,OAAO,IAAI,aAAa;EAEtB,KAAI,OADgB,GAAG,KAAK,KAAK,KAAK,KAAK,IAAI,CAAC,EAAA,CACvC,SAAS,aAAa,OAAO,CAAC;EACvC,MAAM,OAAO,KAAK,KAAK,KAAK,MAAM,eAAe;EACjD,IAAI,EAAE,OAAO,GAAG,OAAO,IAAI,IAAI,OAAO,CAAC;EACvC,MAAM,MAAM,OAAO,GAAG,eAAe,IAAI;EACzC,OAAO,CAAC;GAAE;GAAM,YAAY,gBAAgB,GAAG;EAAE,CAAC;CACpD,CAAC,GACH,EAAE,aAAa,EAAE,CACnB,EAAA,CACY,KAAK;AACnB,CAAC;;;;;;AAOH,MAAa,sBACX,eAEA,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,OAAO;CACnB,OAAO,OAAO,QACZ,aACC,cACC,IACG,gBAEC,OAAO,QAAQ,UAAU,aAAa,cAAc,IAAI,OAAO,SAAS,GAAG;EACzE,aAAa;EACb,SAAS;CACX,CAAC,CACH,CAAC,CACA,KACC,OAAO,UAAU,UAAU,IAAI,eAAe;EAAE,WAAW,UAAU;EAAM;CAAM,CAAC,CAAC,CACrF,GAEJ;EAAE,aAAa;EAAG,SAAS;CAAK,CAClC;CACA,OAAO,WAAW;AACpB,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBH,MAAa,mBACX,QAKG,OAAO,QAAQ,eAAe,GAAG,GAAG,kBAAkB"}
@@ -0,0 +1,270 @@
1
+ import { PgDatabase } from "./index.js";
2
+ import { n as MigrationError } from "./migrations-Dp67FrXh.js";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import * as FileSystem from "effect/FileSystem";
6
+ import * as Path from "effect/Path";
7
+ import * as Schema from "effect/Schema";
8
+ import * as Duration from "effect/Duration";
9
+ import * as Exit from "effect/Exit";
10
+ import { SqlError } from "effect/sql/SqlError";
11
+ import * as Redacted from "effect/Redacted";
12
+ import { AnyRelations } from "drizzle-orm/relations";
13
+ import { PlatformError } from "effect/PlatformError";
14
+ import * as ManagedRuntime from "effect/ManagedRuntime";
15
+ import * as Scope from "effect/Scope";
16
+ /**
17
+ * The environment the resolution reads — the shape of `process.env`, taken
18
+ * as a parameter so the order is testable against a fake.
19
+ */
20
+ interface ResolutionEnvironment {
21
+ readonly [name: string]: string | undefined;
22
+ }
23
+ /** The connection URL for `database` on the configured local server. */
24
+ export declare const connectionUrl: (env: ResolutionEnvironment, database: string) => Redacted.Redacted<string>;
25
+ declare const InvalidDatabasePrefix_base: Schema.Class<InvalidDatabasePrefix, Schema.TaggedStruct<"InvalidDatabasePrefix", {
26
+ readonly prefix: Schema.String;
27
+ }>, import("effect/Cause").YieldableError>;
28
+ /** The prefix is not a lower-case SQL identifier. */
29
+ export declare class InvalidDatabasePrefix extends InvalidDatabasePrefix_base {
30
+ get message(): string;
31
+ }
32
+ /**
33
+ * The deterministic per-checkout database name
34
+ * `<prefix>_test_<sha256(migrationsDir)[:8]>`: concurrent runs from different
35
+ * worktrees never collide, and a stale database self-heals on the next run.
36
+ */
37
+ export declare const testDatabaseName: (options: {
38
+ readonly prefix: string;
39
+ readonly migrationsDir: string;
40
+ }) => Effect.Effect<string, InvalidDatabasePrefix>;
41
+ declare const ServerUnreachable_base: Schema.Class<ServerUnreachable, Schema.TaggedStruct<"ServerUnreachable", {
42
+ readonly cause: Schema.Defect;
43
+ }>, import("effect/Cause").YieldableError>;
44
+ /** One connection attempt against a server's admin database failed. */
45
+ export declare class ServerUnreachable extends ServerUnreachable_base {}
46
+ /**
47
+ * One connection attempt against a server's admin (`postgres`) database.
48
+ * Injected into {@link resolveDatabaseSource} so the resolution order can be
49
+ * exercised without a server; {@link probeServer} is the real one.
50
+ */
51
+ type ServerProbe = (adminUrl: Redacted.Redacted<string>) => Effect.Effect<void, ServerUnreachable>;
52
+ /** A `SELECT 1` over one short-lived connection. */
53
+ export declare const probeServer: ServerProbe;
54
+ declare const DatabaseUnreachable_base: Schema.Class<DatabaseUnreachable, Schema.TaggedStruct<"DatabaseUnreachable", {
55
+ readonly server: Schema.String;
56
+ readonly cause: Schema.Defect;
57
+ }>, import("effect/Cause").YieldableError>;
58
+ /** An explicit but unreachable `PG*` target: this run will not fall back to a container. */
59
+ export declare class DatabaseUnreachable extends DatabaseUnreachable_base {
60
+ get message(): string;
61
+ }
62
+ /** Where a run's database comes from — the resolved decision, before anything is provisioned. */
63
+ type DatabaseSource =
64
+ /** An explicit, already-migrated database: used as-is, never provisioned or torn down. */
65
+ {
66
+ readonly _tag: "Explicit";
67
+ readonly url: Redacted.Redacted<string>;
68
+ } |
69
+ /** A reachable local server: this checkout's database is dropped, recreated and migrated on it. */
70
+ {
71
+ readonly _tag: "LocalServer";
72
+ readonly adminUrl: Redacted.Redacted<string>;
73
+ readonly database: string;
74
+ readonly url: Redacted.Redacted<string>;
75
+ } |
76
+ /** No explicit config and nothing listening: a fresh container is started via Testcontainers. */
77
+ {
78
+ readonly _tag: "Container";
79
+ readonly image: string;
80
+ };
81
+ /** The inputs {@link resolveDatabaseSource} reads. */
82
+ interface DatabaseSourceOptions {
83
+ /** The environment — `process.env` in a global setup. */
84
+ readonly env: ResolutionEnvironment;
85
+ /** The migrations directory, as an absolute path: the per-checkout key of the database name. */
86
+ readonly migrationsDir: string;
87
+ /** The database-name prefix, a lower-case SQL identifier (the application's name, say). */
88
+ readonly dbNamePrefix: string;
89
+ /** The container image the Testcontainers fallback starts (`pgvector/pgvector:pg17`, `postgres:17`, …). */
90
+ readonly image: string;
91
+ /** The server probe: {@link probeServer} when absent or `undefined`. A test substitutes its own. */
92
+ readonly probe?: ServerProbe | undefined;
93
+ }
94
+ /**
95
+ * Resolve where a run's database comes from, in the order the module doc
96
+ * describes: an explicit URL, then a reachable local server (an explicit but
97
+ * unreachable `PG*` target is a {@link DatabaseUnreachable}), then the
98
+ * Testcontainers fallback. Pure apart from the one probe, which only runs
99
+ * when no explicit URL is set.
100
+ */
101
+ export declare const resolveDatabaseSource: (options: DatabaseSourceOptions) => Effect.Effect<DatabaseSource, DatabaseUnreachable | InvalidDatabasePrefix>;
102
+ declare const ContainerStartFailed_base: Schema.Class<ContainerStartFailed, Schema.TaggedStruct<"ContainerStartFailed", {
103
+ readonly image: Schema.String;
104
+ readonly server: Schema.String;
105
+ readonly cause: Schema.Defect;
106
+ }>, import("effect/Cause").YieldableError>;
107
+ /** Starting the Testcontainers fallback failed — most often, no Docker daemon. */
108
+ export declare class ContainerStartFailed extends ContainerStartFailed_base {
109
+ get message(): string;
110
+ }
111
+ /** A provisioned database: its URL, and the source it was resolved from. */
112
+ interface ProvisionedDatabase {
113
+ readonly url: Redacted.Redacted<string>;
114
+ readonly source: DatabaseSource;
115
+ }
116
+ /**
117
+ * How a provisioned database is migrated: handed the new database's URL,
118
+ * returns how many migrations ran. The default applies the migrations under
119
+ * {@link ProvisionOptions.migrationsDir} with `./migrations`'
120
+ * `applyMigrations` over a one-connection `PgClient`.
121
+ */
122
+ type MigrationRunner<E, R> = (url: Redacted.Redacted<string>) => Effect.Effect<number, E, R>;
123
+ /** The inputs {@link provisionTestDatabase} takes. */
124
+ interface ProvisionOptions<E = never, R = never> extends DatabaseSourceOptions {
125
+ /**
126
+ * Replaces the default runner (for a harness whose migrations are not a
127
+ * drizzle-kit directory); absent or `undefined` keeps the default.
128
+ */
129
+ readonly applyMigrations?: MigrationRunner<E, R> | undefined;
130
+ }
131
+ /**
132
+ * Provision a run's fresh, fully-migrated database — resolve the source,
133
+ * then drop / create / migrate on a reachable server, or start and migrate a
134
+ * container — and return its URL. An explicit source is returned as-is.
135
+ *
136
+ * Scoped: a started container stops when the scope closes. From a vitest
137
+ * global setup, hold a `Scope` across the run and close it in the returned
138
+ * teardown:
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * // vitest global setup
143
+ * import * as NodeServices from "@effect/platform-node/NodeServices";
144
+ * import { provisionTestDatabase } from "@thomasfosterau/effect-drizzle-ext-pg/testing";
145
+ * import * as Effect from "effect/Effect";
146
+ * import * as Exit from "effect/Exit";
147
+ * import * as Redacted from "effect/Redacted";
148
+ * import * as Scope from "effect/Scope";
149
+ *
150
+ * const MIGRATIONS_DIR = "packages/database/migrations";
151
+ *
152
+ * export default async function setup() {
153
+ * const scope = Effect.runSync(Scope.make());
154
+ * const database = await Effect.runPromise(
155
+ * provisionTestDatabase({
156
+ * env: process.env,
157
+ * migrationsDir: MIGRATIONS_DIR,
158
+ * dbNamePrefix: "website",
159
+ * image: "pgvector/pgvector:pg17",
160
+ * }).pipe(Scope.provide(scope), Effect.provide(NodeServices.layer)),
161
+ * );
162
+ * process.env.DATABASE_URL_TEST = Redacted.value(database.url);
163
+ * return () => Effect.runPromise(Scope.close(scope, Exit.void));
164
+ * }
165
+ * ```
166
+ */
167
+ export declare const provisionTestDatabase: <E = MigrationError | PlatformError | SqlError, R = FileSystem.FileSystem | Path.Path>(options: ProvisionOptions<E, R>) => Effect.Effect<ProvisionedDatabase, E | DatabaseUnreachable | InvalidDatabasePrefix | ContainerStartFailed | SqlError | MigrationError | PlatformError, R | FileSystem.FileSystem | Path.Path | Scope.Scope>;
168
+ /**
169
+ * Whether `error` is a `SqlError` that failed at the connect/acquire seam and
170
+ * so is safe to retry (when the caller has also confirmed no test body ran).
171
+ *
172
+ * Returns `true` for:
173
+ * - any reason the driver already flags `isRetryable` (`ConnectionError` from
174
+ * an `08xxx` SQLSTATE, `DeadlockError`, `SerializationError`, the timeout
175
+ * reasons), and
176
+ * - a reason whose `operation` is a connect/acquire seam label, which catches
177
+ * the misclassified `UnknownError` the website #1055 flake surfaces as
178
+ * (pool "timeout exceeded when trying to connect" / `ECONNRESET`, neither
179
+ * of which carries an `08` SQLSTATE).
180
+ *
181
+ * Returns `false` for a `UniqueViolation` / `ConstraintError` /
182
+ * `SqlSyntaxError` raised by a test's own query (`operation === "execute"`),
183
+ * so genuine test failures are never retried.
184
+ */
185
+ export declare const isRetryableConnectFailure: (error: unknown) => boolean;
186
+ /**
187
+ * The always-rollback test transaction runner for one application's
188
+ * {@link PgDatabase}, built by {@link makeTestTransaction}.
189
+ *
190
+ * Only the service KEY matters to these signatures — the handle types are
191
+ * closed over by the factory — so the interface is parameterised by the key
192
+ * string alone.
193
+ */
194
+ export interface TestTransaction<Key extends string> {
195
+ /**
196
+ * Runs `effect` inside a transaction that is always rolled back once the
197
+ * test completes, against a caller-supplied runtime.
198
+ *
199
+ * `R` is the runtime's own context. It **must include the `Database`
200
+ * key**, which is what lets the program below yield the client; that
201
+ * relationship is not expressible as a generic constraint once `R` can be a
202
+ * union that also carries unrelated services (a harness's platform binding
203
+ * stubs, a spec's viewer services), so it is documented here rather than
204
+ * enforced by the type checker — the same trade-off the ported wrapper
205
+ * makes.
206
+ *
207
+ * `${Key}/CurrentTransaction` is in the ACCEPTED requirement, not in the
208
+ * runtime's catalogue: nothing provides it as a layer. The runner wraps the
209
+ * effect in `transact`, so the spec stands in for the handler that opens
210
+ * the boundary in production, and the requirement is discharged there.
211
+ */
212
+ readonly runInTestTransaction: <R, A, E>(runtime: ManagedRuntime.ManagedRuntime<R, SqlError>, effect: Effect.Effect<A, E, R | `${Key}/CurrentTransaction`>) => Promise<A>;
213
+ /**
214
+ * {@link runInTestTransaction} with the runtime bound once — the form a
215
+ * suite exports as its own `withTestTransaction(effect)` helper.
216
+ */
217
+ readonly withTestTransaction: <R>(runtime: ManagedRuntime.ManagedRuntime<R, SqlError>) => <A, E>(effect: Effect.Effect<A, E, R | `${Key}/CurrentTransaction`>) => Promise<A>;
218
+ /**
219
+ * Run `effect` inside a **SAVEPOINT** and hand back its `Exit`, leaving the
220
+ * enclosing transaction usable whether it succeeded or failed.
221
+ *
222
+ * This is what a spec asserting "the constraint rejects this write" needs.
223
+ * A statement that raises in PostgreSQL aborts the whole transaction, so a
224
+ * bare `Effect.exit(tx.insert(…))` captures the expected failure and then
225
+ * poisons every later statement in the test with `current transaction is
226
+ * aborted, commands ignored until end of transaction block` — which
227
+ * surfaces as the *next* query failing, several assertions away from the
228
+ * cause. Nesting the attempt (`tx.transaction`, a SAVEPOINT on the same
229
+ * pinned connection) confines the abort to the savepoint: the driver rolls
230
+ * back to it and the outer transaction, with every fixture the test has
231
+ * already written, carries on.
232
+ *
233
+ * `effect` keeps using the ambient `CurrentTransaction` handle — the
234
+ * savepoint is a property of the CONNECTION, which `maxConnections: 1`
235
+ * pins, so nothing has to be re-threaded through a nested handle.
236
+ *
237
+ * The `Exit` is returned rather than asserted on, so the caller keeps the
238
+ * choice: `expect(exit._tag).toBe("Failure")` for a rejection, or a look at
239
+ * the cause when the message matters.
240
+ */
241
+ readonly attemptInSavepoint: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<Exit.Exit<A, E | SqlError>, never, R | `${Key}/CurrentTransaction`>;
242
+ }
243
+ /**
244
+ * Builds the always-rollback test transaction runner for one application's
245
+ * database services.
246
+ *
247
+ * The services carry the key strings and the `transact` boundary, so the
248
+ * runner needs nothing else: a suite calls this once next to its
249
+ * {@link makePgDatabase} call and exports the three helpers.
250
+ */
251
+ export declare const makeTestTransaction: <Relations extends AnyRelations, Key extends string>(database: PgDatabase<Relations, Key>) => TestTransaction<Key>;
252
+ /**
253
+ * The `Database` layer an integration suite runs against: the application's
254
+ * own {@link PgDatabase.layer} over a `PgClient` built from `url`, with the
255
+ * pool pinned to one connection (see {@link TEST_POOL_MAX_CONNECTIONS}).
256
+ *
257
+ * Built once per process and reused by every spec in it. `url` is the
258
+ * caller's: this library does not read `DATABASE_URL_TEST` or decide how a
259
+ * test database is provisioned.
260
+ *
261
+ * Unlike production, this uses the pinging {@link pgClientFromUrl} on
262
+ * purpose — a suite that cannot reach its database should fail at layer build
263
+ * with one legible `SqlError`, not once per spec on the first query.
264
+ */
265
+ export declare const testDatabaseLayer: <Relations extends AnyRelations, Key extends string>(database: PgDatabase<Relations, Key>, options: {
266
+ readonly url: Redacted.Redacted<string>;
267
+ readonly connectTimeout?: Duration.Input | undefined;
268
+ }) => Layer.Layer<Key, SqlError>;
269
+ export type { DatabaseSource, DatabaseSourceOptions, MigrationRunner, ProvisionOptions, ProvisionedDatabase, ResolutionEnvironment, ServerProbe };
270
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","names":[],"sources":["../src/provisioning.ts","../src/testing.ts"],"mappings":";;;;;;;;;;;;;;;;;;;UA2DiB;YAEL;;;qBAsBC,gBAAa,KACnB,uBAAqB,qBAEzB,SAAS;;;;;qBAgBC,8BAA8B;MAI5B;;;;;;;qBAUF,mBAAgB;WAClB;WACA;MACP,OAAO,eAAe;;;;;qBAgBb,0BAA0B;;;;;;KAU3B,eACV,UAAU,SAAS,qBAChB,OAAO,aAAa;;qBAGZ,aAAa;;;;;;qBAYb,4BAA4B;MAI1B;;;KAWH;;;WAEG;WAA2B,KAAK,SAAS;;;;WAGzC;WACA,UAAU,SAAS;WACnB;WACA,KAAK,SAAS;;;;WAGd;WAA4B;;;UAG1B;;WAEN,KAAK;;WAEL;;WAEA;;WAEA;;WAGA,QAAQ;;;;;;;;;qBAUN,wBAAqB,SACvB,0BACR,OAAO,OAAO,gBAAgB,sBAAsB;;;;;;;qBAkC1C,6BAA6B;MAI3B;;;UAaE;WACN,KAAK,SAAS;WACd,QAAQ;;;;;;;;KASP,gBAAgB,GAAG,MAAM,KAAK,SAAS,qBAAqB,OAAO,eAAe,GAAG;;UAGhF,iBAAiB,WAAW,mBAAmB;;;;;WAMrD,kBAAkB,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAuFnC,wBACX,IAAI,iBAAiB,gBAAgB,UACrC,IAAI,WAAW,aAAa,KAAK,MAAI,SAE5B,iBAAiB,GAAG,OAC5B,OAAO,OACR,qBACE,IACA,sBACA,wBACA,uBACA,WACA,iBACA,eACF,IAAI,WAAW,aAAa,KAAK,OAAO,MAAM;;;;;;;;;;;;;;;;;;qBC7PnC,4BAAyB;;;;;;;;;iBAwGrB,gBAAgB;;;;;;;;;;;;;;;;;;WAkBtB,uBAAuB,GAAG,GAAG,GACpC,SAAS,eAAe,eAAe,GAAG,WAC1C,QAAQ,OAAO,OAAO,GAAG,GAAG,OAAO,8BAChC,QAAQ;;;;;WAMJ,sBAAsB,GAC7B,SAAS,eAAe,eAAe,GAAG,eACtC,GAAG,GAAG,QAAQ,OAAO,OAAO,GAAG,GAAG,OAAO,8BAA8B,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;WAyB5E,qBAAqB,GAAG,GAAG,GAClC,QAAQ,OAAO,OAAO,GAAG,GAAG,OACzB,OAAO,OAAO,KAAK,KAAK,GAAG,IAAI,kBAAkB,OAAO;;;;;;;;;;qBAWlD,sBAAuB,kBAAkB,cAAc,oBAAkB,UAC1E,WAAW,WAAW,SAC/B,gBAAgB;;;;;;;;;;;;;;qBA4HN,oBAAqB,kBAAkB,cAAc,oBAAkB,UACxE,WAAW,WAAW,MAAI;WAEzB,KAAK,SAAS;WACd,iBAAiB,SAAS;MAEpC,MAAM,MAAM,KAAK"}