@thomasfosterau/effect-drizzle-ext-pg 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +442 -2
- package/dist/index.d.ts +159 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations-Dp67FrXh.d.ts +68 -0
- package/dist/migrations-Dp67FrXh.d.ts.map +1 -0
- package/dist/migrations.d.ts +2 -0
- package/dist/migrations.js +136 -0
- package/dist/migrations.js.map +1 -0
- package/dist/testing.d.ts +270 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +408 -0
- package/dist/testing.js.map +1 -0
- package/dist/tooling/index.d.ts +217 -0
- package/dist/tooling/index.d.ts.map +1 -0
- package/dist/tooling/index.js +479 -0
- package/dist/tooling/index.js.map +1 -0
- package/package.json +83 -3
- package/src/index.ts +231 -0
- package/src/migrations.ts +173 -0
- package/src/provisioning.ts +407 -0
- package/src/testing.ts +447 -0
- package/src/tooling/index.ts +46 -0
- package/src/tooling/migration-drift.ts +640 -0
- package/src/tooling/view-dependencies.ts +168 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Thomas Foster
|
|
3
|
+
*
|
|
4
|
+
* The `@effect/sql-pg` binding of the driver-agnostic service factory: one
|
|
5
|
+
* call to {@link makePgDatabase} gives an application the `Database` and
|
|
6
|
+
* `CurrentTransaction` services, the `transact` write boundary and the
|
|
7
|
+
* `readHandle` resolver, all typed over its own `relations` graph, plus the
|
|
8
|
+
* `Effect` and `Layer` that build the Drizzle client itself.
|
|
9
|
+
*
|
|
10
|
+
* `drizzle-orm/effect-postgres` already ships the Effect-native Drizzle
|
|
11
|
+
* driver; nothing here re-implements it. What this module adds is the two
|
|
12
|
+
* pieces every consuming application was spelling out by hand:
|
|
13
|
+
*
|
|
14
|
+
* - the `Layer.effect(Database, PgDrizzle.make({ relations }).pipe(provide(
|
|
15
|
+
* DefaultServices)))` stanza, which appeared four times per application
|
|
16
|
+
* (request path, event path, test harness, proxied test harness), and
|
|
17
|
+
* - the choice of `PgClient` construction, which is a real decision rather
|
|
18
|
+
* than a detail — see {@link pgClientFromUrl} and {@link pgClientFromPool}.
|
|
19
|
+
*
|
|
20
|
+
* Deployment topology stays with the consuming application: resolving a
|
|
21
|
+
* connection string from a platform binding, degrading a failed binding to a
|
|
22
|
+
* chainable proxy client, and any proxy wrapper around the concrete client
|
|
23
|
+
* are all things only the application knows about.
|
|
24
|
+
*
|
|
25
|
+
* @module index
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import * as PgClient from "@effect/sql-pg/PgClient";
|
|
29
|
+
import type * as PgTypes from "@effect/sql-pg/PgTypes";
|
|
30
|
+
import {
|
|
31
|
+
type DatabaseServices,
|
|
32
|
+
makeDatabaseServices,
|
|
33
|
+
} from "@thomasfosterau/effect-drizzle-ext/database";
|
|
34
|
+
import * as PgDrizzle from "drizzle-orm/effect-postgres";
|
|
35
|
+
import type {
|
|
36
|
+
EffectPgDatabase,
|
|
37
|
+
EffectPgQueryResultHKT,
|
|
38
|
+
EffectPgTransaction,
|
|
39
|
+
} from "drizzle-orm/effect-postgres";
|
|
40
|
+
import type { AnyRelations } from "drizzle-orm/relations";
|
|
41
|
+
import type * as Duration from "effect/Duration";
|
|
42
|
+
import * as Effect from "effect/Effect";
|
|
43
|
+
import * as Layer from "effect/Layer";
|
|
44
|
+
import type * as Redacted from "effect/Redacted";
|
|
45
|
+
import type { SqlClient } from "effect/sql/SqlClient";
|
|
46
|
+
import type { SqlError } from "effect/sql/SqlError";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The Effect-native Drizzle client an application's `Database` service holds,
|
|
50
|
+
* typed over its relations graph.
|
|
51
|
+
*
|
|
52
|
+
* `$client` is the `PgClient` the driver was built against; it is part of the
|
|
53
|
+
* value `drizzle-orm/effect-postgres`'s `make` returns, and is named here so
|
|
54
|
+
* the service payload type is self-contained rather than
|
|
55
|
+
* `Effect.Success<typeof make>` of a local binding.
|
|
56
|
+
*/
|
|
57
|
+
export type PgDatabaseClient<Relations extends AnyRelations> = EffectPgDatabase<Relations> & {
|
|
58
|
+
readonly $client: PgClient.PgClient;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The transaction handle the driver passes to a `db.transaction(...)`
|
|
63
|
+
* callback — the payload of the `CurrentTransaction` service.
|
|
64
|
+
*
|
|
65
|
+
* The query-result HKT is pinned to `EffectPgQueryResultHKT` (the `@effect/
|
|
66
|
+
* sql-pg` one) because that is what `EffectPgDatabase` opens its transactions
|
|
67
|
+
* with; a consumer never writes the parameter.
|
|
68
|
+
*/
|
|
69
|
+
export type PgTransaction<Relations extends AnyRelations> = EffectPgTransaction<
|
|
70
|
+
EffectPgQueryResultHKT,
|
|
71
|
+
Relations
|
|
72
|
+
>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One application's database services: the driver-agnostic
|
|
76
|
+
* {@link DatabaseServices} surface, plus the two things that are specific to
|
|
77
|
+
* having chosen a driver — the `Effect` that builds the client and the
|
|
78
|
+
* `Layer` that installs it under the `Database` key.
|
|
79
|
+
*
|
|
80
|
+
* Both carry `PgClient` in their requirement channel and nothing else: WHERE
|
|
81
|
+
* the pool comes from is the consuming application's decision, made by
|
|
82
|
+
* providing {@link pgClientFromUrl}, {@link pgClientFromPool}, or a client
|
|
83
|
+
* layer of its own.
|
|
84
|
+
*/
|
|
85
|
+
export interface PgDatabase<
|
|
86
|
+
Relations extends AnyRelations,
|
|
87
|
+
Key extends string,
|
|
88
|
+
> extends DatabaseServices<PgDatabaseClient<Relations>, PgTransaction<Relations>, Key> {
|
|
89
|
+
/**
|
|
90
|
+
* Builds the Drizzle client over an ambient `PgClient`.
|
|
91
|
+
*
|
|
92
|
+
* `DefaultServices` supplies the no-op logger and cache the driver
|
|
93
|
+
* requires; an application that wants real ones builds its own client and
|
|
94
|
+
* provides it under {@link DatabaseServices.Database} instead of using
|
|
95
|
+
* {@link layer}.
|
|
96
|
+
*/
|
|
97
|
+
readonly make: Effect.Effect<PgDatabaseClient<Relations>, never, PgClient.PgClient>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* {@link make} installed under the `Database` key — the stanza every
|
|
101
|
+
* consuming application was repeating once per runtime graph.
|
|
102
|
+
*
|
|
103
|
+
* Requires a `PgClient`, which is the seam the deployment topology plugs
|
|
104
|
+
* into: a request-scoped pool over a platform binding in production, a
|
|
105
|
+
* URL-configured pool in a harness.
|
|
106
|
+
*/
|
|
107
|
+
readonly layer: Layer.Layer<Key, never, PgClient.PgClient>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Builds one application's {@link PgDatabase} from its relations graph and a
|
|
112
|
+
* service key.
|
|
113
|
+
*
|
|
114
|
+
* Call it once, at module scope, and export what it returns; the keys are
|
|
115
|
+
* derived from `key`, so calling it twice with the same key produces services
|
|
116
|
+
* that share a runtime slot.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* import { makePgDatabase } from "@thomasfosterau/effect-drizzle-ext-pg";
|
|
121
|
+
*
|
|
122
|
+
* const relations = {}; // the application's drizzle relations graph
|
|
123
|
+
* const database = makePgDatabase({ relations, key: "Database" });
|
|
124
|
+
* export const { Database, CurrentTransaction, transact, readHandle } = database;
|
|
125
|
+
* export type Database = typeof database.Database.Identifier; // "Database"
|
|
126
|
+
*
|
|
127
|
+
* console.log(Database.key); // "Database"
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export const makePgDatabase = <Relations extends AnyRelations, const Key extends string>(options: {
|
|
131
|
+
readonly relations: Relations;
|
|
132
|
+
readonly key: Key;
|
|
133
|
+
}): PgDatabase<Relations, Key> => {
|
|
134
|
+
const services = makeDatabaseServices<PgDatabaseClient<Relations>, PgTransaction<Relations>>()({
|
|
135
|
+
key: options.key,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const make: Effect.Effect<
|
|
139
|
+
PgDatabaseClient<Relations>,
|
|
140
|
+
never,
|
|
141
|
+
PgClient.PgClient
|
|
142
|
+
> = PgDrizzle.make<Relations>({ relations: options.relations }).pipe(
|
|
143
|
+
Effect.provide(PgDrizzle.DefaultServices),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return { ...services, make, layer: Layer.effect(services.Database, make) };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A `PgClient` layer over a connection URL that **pings the database
|
|
151
|
+
* (`SELECT 1`) at layer BUILD**, failing the build with a `SqlError` if the
|
|
152
|
+
* ping does not answer within `connectTimeout`.
|
|
153
|
+
*
|
|
154
|
+
* That is the right trade for a long-lived process — a misconfigured URL
|
|
155
|
+
* fails once, loudly, at startup rather than on the first query — and the
|
|
156
|
+
* wrong one for a runtime that is built per request, where it opens a
|
|
157
|
+
* connection even for requests that never touch the database. Use
|
|
158
|
+
* {@link pgClientFromPool} there.
|
|
159
|
+
*
|
|
160
|
+
* `@effect/sql-pg` 4.0.0-rc.115 replaced the `pg` driver with its own wire
|
|
161
|
+
* protocol, and its `PgClient.layer` no longer pings on its own: the pool
|
|
162
|
+
* opens connections on first use. The ping is therefore this function's, run
|
|
163
|
+
* once over the freshly built client.
|
|
164
|
+
*
|
|
165
|
+
* The output includes `SqlClient` as well as `PgClient` because that is what
|
|
166
|
+
* `PgClient.layerFrom` provides; a consumer that only needs `PgClient` can
|
|
167
|
+
* ignore the extra service.
|
|
168
|
+
*/
|
|
169
|
+
/* oxlint-disable effect/prefer-option-over-null -- `config` is handed STRAIGHT to `@effect/sql-pg`'s `PgPoolConfig`, whose optional members are `?: T | undefined`: the driver reads `undefined` as "not supplied" and would treat an `Option` object as a value. Same boundary rule as the Drizzle-facing config in packages/core/src/derive.ts. */
|
|
170
|
+
export const pgClientFromUrl = (config: {
|
|
171
|
+
readonly url: Redacted.Redacted<string>;
|
|
172
|
+
readonly maxConnections?: number | undefined;
|
|
173
|
+
readonly connectTimeout?: Duration.Input | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* A per-client codec registry (`PgTypes.makeRegistry()` plus your
|
|
176
|
+
* `register` calls), forwarded to `@effect/sql-pg`. See the README's
|
|
177
|
+
* "Binary codecs" section for why Drizzle needs one.
|
|
178
|
+
*/
|
|
179
|
+
readonly types?: PgTypes.Registry | undefined;
|
|
180
|
+
/**
|
|
181
|
+
* Named prepared statements (`@effect/sql-pg` defaults this to `true`).
|
|
182
|
+
* Set `false` behind a pooler that cannot keep them across queries, such
|
|
183
|
+
* as Cloudflare Hyperdrive.
|
|
184
|
+
*/
|
|
185
|
+
readonly prepare?: boolean | undefined;
|
|
186
|
+
}): Layer.Layer<PgClient.PgClient | SqlClient, SqlError> =>
|
|
187
|
+
PgClient.layerFrom(Effect.tap(PgClient.make(config), (client) => client`SELECT 1`));
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A `PgClient` layer over a connection URL that **does not connect at layer
|
|
191
|
+
* build** — the lazy counterpart to {@link pgClientFromUrl}.
|
|
192
|
+
*
|
|
193
|
+
* The pool is `@effect/sql-pg`'s own: it opens connections on first use (up
|
|
194
|
+
* to `maxConnections`) and is shut down, every session released, when the
|
|
195
|
+
* layer's scope closes. A runtime built per request therefore disposes of its
|
|
196
|
+
* pool with that request, and a connection failure surfaces on the first
|
|
197
|
+
* query as a `SqlError` instead of at build. Platforms that pin I/O to the
|
|
198
|
+
* event that created it require exactly this — a pool must never outlive
|
|
199
|
+
* that event (website #455).
|
|
200
|
+
*
|
|
201
|
+
* Until `@effect/sql-pg` 4.0.0-rc.115 this wrapped a bare `pg.Pool` via
|
|
202
|
+
* `PgClient.fromPool`, because `PgClient.layer` pinged at build; rc.115
|
|
203
|
+
* dropped both the `pg` driver and `fromPool`, and its pool is lazy by
|
|
204
|
+
* construction. The name is kept for the contract, not the mechanism.
|
|
205
|
+
*/
|
|
206
|
+
/* oxlint-disable effect/prefer-option-over-null -- driver-facing config: every optional member is forwarded to `@effect/sql-pg`'s `PgPoolConfig`, which reads `undefined` as "not supplied". See `pgClientFromUrl`'s block. */
|
|
207
|
+
export const pgClientFromPool = (options: {
|
|
208
|
+
readonly connectionString: Redacted.Redacted<string>;
|
|
209
|
+
readonly maxConnections?: number | undefined;
|
|
210
|
+
readonly connectTimeout?: Duration.Input | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* A per-client codec registry (`PgTypes.makeRegistry()` plus your
|
|
213
|
+
* `register` calls), forwarded to `@effect/sql-pg`. See the README's
|
|
214
|
+
* "Binary codecs" section for why Drizzle needs one.
|
|
215
|
+
*/
|
|
216
|
+
readonly types?: PgTypes.Registry | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* Named prepared statements (`@effect/sql-pg` defaults this to `true`).
|
|
219
|
+
* Set `false` behind a pooler that cannot keep them across queries, such
|
|
220
|
+
* as Cloudflare Hyperdrive.
|
|
221
|
+
*/
|
|
222
|
+
readonly prepare?: boolean | undefined;
|
|
223
|
+
}): Layer.Layer<PgClient.PgClient | SqlClient, SqlError> =>
|
|
224
|
+
PgClient.layer({
|
|
225
|
+
url: options.connectionString,
|
|
226
|
+
maxConnections: options.maxConnections,
|
|
227
|
+
connectTimeout: options.connectTimeout,
|
|
228
|
+
types: options.types,
|
|
229
|
+
prepare: options.prepare,
|
|
230
|
+
});
|
|
231
|
+
/* oxlint-enable effect/prefer-option-over-null */
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Thomas Foster
|
|
3
|
+
*
|
|
4
|
+
* **The ordered migration runner for a brand-new database.** Reads a
|
|
5
|
+
* drizzle-kit `migrations/` directory and applies every migration in it, in
|
|
6
|
+
* directory-name order, one transaction per migration, with no ledger.
|
|
7
|
+
*
|
|
8
|
+
* It is what an integration suite's provisioning runs against the database it
|
|
9
|
+
* has just created ({@link ../testing.ts}'s `provisionTestDatabase` calls it by
|
|
10
|
+
* default), and it mirrors the apply order and per-migration transaction
|
|
11
|
+
* shape of alchemy's deploy-time `migrationsDir` runner, minus its ledger:
|
|
12
|
+
* the database is **always** brand new, so every migration applies
|
|
13
|
+
* unconditionally and no bookkeeping table is needed. A database that already
|
|
14
|
+
* holds some of the migrations is not this runner's input — use the
|
|
15
|
+
* deployment's own migrator there.
|
|
16
|
+
*
|
|
17
|
+
* ## The layout it reads
|
|
18
|
+
*
|
|
19
|
+
* drizzle-kit writes one directory per migration, `<digits>_<slug>/`,
|
|
20
|
+
* holding `migration.sql` (and `snapshot.json`, which this runner ignores).
|
|
21
|
+
* The all-digit, fixed-width prefixes make name order chronological. A
|
|
22
|
+
* directory with no `migration.sql` is skipped rather than failing the run —
|
|
23
|
+
* drizzle-kit writes the snapshot before the SQL, and a sanctioned
|
|
24
|
+
* hand-written bootstrap directory may carry only SQL.
|
|
25
|
+
*
|
|
26
|
+
* ## Statements, not files
|
|
27
|
+
*
|
|
28
|
+
* `@effect/sql-pg` speaks the extended query protocol, which rejects more
|
|
29
|
+
* than one statement per query ("cannot insert multiple commands into a
|
|
30
|
+
* prepared statement"). drizzle-kit separates the statements of a migration
|
|
31
|
+
* with a `--> statement-breakpoint` line, and drizzle's own migrator splits
|
|
32
|
+
* on exactly that marker, so this runner does the same: each migration's
|
|
33
|
+
* statements run in order inside that migration's one transaction. A
|
|
34
|
+
* hand-written migration must keep the marker between statements.
|
|
35
|
+
*
|
|
36
|
+
* Idiomatic Effect: the directory is read through the `FileSystem` / `Path`
|
|
37
|
+
* services (a Node consumer provides `@effect/platform-node`'s
|
|
38
|
+
* `NodeServices.layer`), and the statements run on the ambient `SqlClient`.
|
|
39
|
+
*
|
|
40
|
+
* @module migrations
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import * as Arr from "effect/Array";
|
|
44
|
+
import * as Effect from "effect/Effect";
|
|
45
|
+
import * as FileSystem from "effect/FileSystem";
|
|
46
|
+
import * as Order from "effect/Order";
|
|
47
|
+
import * as Path from "effect/Path";
|
|
48
|
+
import type { PlatformError } from "effect/PlatformError";
|
|
49
|
+
import * as Schema from "effect/Schema";
|
|
50
|
+
import { SqlClient } from "effect/sql/SqlClient";
|
|
51
|
+
import * as Str from "effect/String";
|
|
52
|
+
|
|
53
|
+
/** drizzle-kit's statement separator inside a `migration.sql`. */
|
|
54
|
+
export const STATEMENT_BREAKPOINT = "--> statement-breakpoint";
|
|
55
|
+
|
|
56
|
+
/** One migration as the runner applies it: its directory name and its statements, in order. */
|
|
57
|
+
export interface Migration {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly statements: ReadonlyArray<string>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A migration failed to apply. `migration` names the directory — a bare
|
|
64
|
+
* driver error says only which statement broke, not which file it came from
|
|
65
|
+
* — and `cause` is the driver's `SqlError`. The migration's transaction has
|
|
66
|
+
* been rolled back; the ones before it stay applied.
|
|
67
|
+
*/
|
|
68
|
+
export class MigrationError extends Schema.TaggedError<MigrationError>()("MigrationError", {
|
|
69
|
+
migration: Schema.String,
|
|
70
|
+
cause: Schema.Defect(),
|
|
71
|
+
}) {
|
|
72
|
+
override get message(): string {
|
|
73
|
+
return `migration '${this.migration}' failed`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Split one `migration.sql` into its statements on
|
|
79
|
+
* {@link STATEMENT_BREAKPOINT}, dropping the empty chunks a trailing marker
|
|
80
|
+
* or blank file leaves behind.
|
|
81
|
+
*/
|
|
82
|
+
export const splitStatements = (sql: string): ReadonlyArray<string> =>
|
|
83
|
+
sql
|
|
84
|
+
.split(STATEMENT_BREAKPOINT)
|
|
85
|
+
.map((statement) => statement.trim())
|
|
86
|
+
.filter(Str.isNonEmpty);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Every migration under `dir`, in apply order (directory-name order). Only
|
|
90
|
+
* directories count; one with no `migration.sql` is skipped.
|
|
91
|
+
*/
|
|
92
|
+
export const readMigrations = (
|
|
93
|
+
dir: string,
|
|
94
|
+
): Effect.Effect<ReadonlyArray<Migration>, PlatformError, FileSystem.FileSystem | Path.Path> =>
|
|
95
|
+
Effect.gen(function* () {
|
|
96
|
+
const fs = yield* FileSystem.FileSystem;
|
|
97
|
+
const path = yield* Path.Path;
|
|
98
|
+
const names = Arr.sort(yield* fs.readDirectory(dir), Order.String);
|
|
99
|
+
const read = yield* Effect.forEach(
|
|
100
|
+
names,
|
|
101
|
+
(name) =>
|
|
102
|
+
Effect.gen(function* () {
|
|
103
|
+
const info = yield* fs.stat(path.join(dir, name));
|
|
104
|
+
if (info.type !== "Directory") return [];
|
|
105
|
+
const file = path.join(dir, name, "migration.sql");
|
|
106
|
+
if (!(yield* fs.exists(file))) return [];
|
|
107
|
+
const sql = yield* fs.readFileString(file);
|
|
108
|
+
return [{ name, statements: splitStatements(sql) }];
|
|
109
|
+
}),
|
|
110
|
+
{ concurrency: 1 },
|
|
111
|
+
);
|
|
112
|
+
return read.flat();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Apply `migrations` in order on the ambient `SqlClient`, each inside its own
|
|
117
|
+
* transaction, returning how many ran. Stops at the first failure with a
|
|
118
|
+
* {@link MigrationError} naming the migration.
|
|
119
|
+
*/
|
|
120
|
+
export const applyMigrationList = (
|
|
121
|
+
migrations: ReadonlyArray<Migration>,
|
|
122
|
+
): Effect.Effect<number, MigrationError, SqlClient> =>
|
|
123
|
+
Effect.gen(function* () {
|
|
124
|
+
const sql = yield* SqlClient;
|
|
125
|
+
yield* Effect.forEach(
|
|
126
|
+
migrations,
|
|
127
|
+
(migration) =>
|
|
128
|
+
sql
|
|
129
|
+
.withTransaction(
|
|
130
|
+
// In order, one at a time: a migration's statements depend on each other.
|
|
131
|
+
Effect.forEach(migration.statements, (statement) => sql.unsafe(statement), {
|
|
132
|
+
concurrency: 1,
|
|
133
|
+
discard: true,
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
.pipe(
|
|
137
|
+
Effect.mapError((cause) => new MigrationError({ migration: migration.name, cause })),
|
|
138
|
+
),
|
|
139
|
+
// Strictly sequential: a later migration may depend on an earlier one.
|
|
140
|
+
{ concurrency: 1, discard: true },
|
|
141
|
+
);
|
|
142
|
+
return migrations.length;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Apply every migration under `dir` to the (brand-new) database behind the
|
|
147
|
+
* ambient `SqlClient` — {@link readMigrations} then {@link applyMigrationList}
|
|
148
|
+
* — returning how many ran.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
153
|
+
* import * as PgClient from "@effect/sql-pg/PgClient";
|
|
154
|
+
* import { applyMigrations } from "@thomasfosterau/effect-drizzle-ext-pg/migrations";
|
|
155
|
+
* import * as Effect from "effect/Effect";
|
|
156
|
+
* import * as Redacted from "effect/Redacted";
|
|
157
|
+
*
|
|
158
|
+
* const url = Redacted.make("postgres://postgres@localhost:5432/app_test");
|
|
159
|
+
*
|
|
160
|
+
* // Building the Effect connects to nothing; running it applies the chain.
|
|
161
|
+
* export const applied = applyMigrations("packages/database/migrations").pipe(
|
|
162
|
+
* Effect.provide(PgClient.layer({ url, maxConnections: 1 })),
|
|
163
|
+
* Effect.provide(NodeServices.layer),
|
|
164
|
+
* );
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export const applyMigrations = (
|
|
168
|
+
dir: string,
|
|
169
|
+
): Effect.Effect<
|
|
170
|
+
number,
|
|
171
|
+
MigrationError | PlatformError,
|
|
172
|
+
SqlClient | FileSystem.FileSystem | Path.Path
|
|
173
|
+
> => Effect.flatMap(readMigrations(dir), applyMigrationList);
|