1import { PgClient, PgMigrator } from "@effect/sql-pg"
2import { assert, describe, it } from "@effect/vitest"
3import { Effect, FileSystem, Path, Redacted, Stream } from "effect"
4import { ChildProcess, ChildProcessSpawner } from "effect/process"
5import { Reactivity } from "effect/reactivity"
6import * as SqlClient from "effect/sql/SqlClient"
7
8const makeMigrator = (password: PgClient.PgClientConfig["password"]) =>
9 Effect.gen(function*() {
10 const commands: Array<ChildProcess.StandardCommand> = []
11 const files: Array<string> = []
12 const pg = yield* PgClient.make({ password })
13 const sql = yield* SqlClient.make({
14 acquirer: Effect.succeed({
15 execute: () => Effect.succeed([]),
16 executeRaw: () => Effect.succeed([]),
17 executeValues: () => Effect.succeed([]),
18 executeUnprepared: () => Effect.succeed([]),
19 executeValuesUnprepared: () => Effect.succeed([]),
20 executeStream: () => Stream.empty
21 }),
22 compiler: PgClient.makeCompiler(),
23 spanAttributes: []
24 })
25 const run = PgMigrator.run({
26 loader: PgMigrator.fromRecord({ "1_init": Effect.void }),
27 schemaDirectory: "migrations"
28 }).pipe(
29 Effect.provideService(PgClient.PgClient, pg),
30 Effect.provideService(SqlClient.SqlClient, sql),
31 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, {
32 ...ChildProcessSpawner.make(() => Effect.die("unexpected spawn")),
33 string: (command) =>
34 Effect.sync(() => {
35 assert(ChildProcess.isStandardCommand(command))
36 commands.push(command)
37 return "SELECT 1;"
38 })
39 }),
40 Effect.provide(FileSystem.layerNoop({
41 makeDirectory: () => Effect.void,
42 writeFileString: (path) =>
43 Effect.sync(() => {
44 files.push(path)
45 })
46 })),
47 Effect.provide(Path.layer)
48 )
49 return { commands, files, run }
50 }).pipe(Effect.provide(Reactivity.layer))
51
52describe("PgMigrator", () => {
53 it.effect("shares one password per schema dump and refreshes it for the next dump", () =>
54 Effect.gen(function*() {
55 let calls = 0
56 const { commands, files, run } = yield* makeMigrator(
57 Effect.map(Effect.yieldNow, () => Redacted.make(`secret-${++calls}`))
58 )
59
60 yield* run
61
62 assert.strictEqual(calls, 1)
63 assert.deepStrictEqual(commands.map((command) => command.command), ["pg_dump", "pg_dump"])
64 assert.deepStrictEqual(commands.map((command) => command.options.env?.PGPASSWORD), ["secret-1", "secret-1"])
65
66 yield* run
67
68 assert.strictEqual(calls, 2)
69 assert.deepStrictEqual(commands.map((command) => command.options.env?.PGPASSWORD), [
70 "secret-1",
71 "secret-1",
72 "secret-2",
73 "secret-2"
74 ])
75 assert.deepStrictEqual(files, ["migrations/_schema.sql", "migrations/_schema.sql"])
76 }))
77
78 it.effect("passes a static password to both dump processes", () =>
79 Effect.gen(function*() {
80 const { commands, files, run } = yield* makeMigrator(Redacted.make("static"))
81 yield* run
82
83 assert.deepStrictEqual(commands.map((command) => command.options.env?.PGPASSWORD), ["static", "static"])
84 assert.deepStrictEqual(files, ["migrations/_schema.sql"])
85 }))
86})