1import { assert, describe, it } from "@effect/vitest"
2import { Effect, FileSystem, Layer, Path, PlatformError } from "effect"
3import * as Migrator from "effect/sql/Migrator"
4
5const migrationNames = (
6 migrations: ReadonlyArray<Migrator.ResolvedMigration>
7): ReadonlyArray<readonly [id: number, name: string]> => migrations.map(([id, name]) => [id, name] as const)
8
9describe("Migrator", () => {
10 describe("loaders", () => {
11 it.effect("fromGlob accepts js, ts, mjs, and mts files", () =>
12 Effect.gen(function*() {
13 const migrations = yield* Migrator.fromGlob({
14 "./migrations/0004_fourth.mts": () => Promise.resolve(Effect.void),
15 "./migrations/0002_second.ts": () => Promise.resolve(Effect.void),
16 "./migrations/0003_third.mjs": () => Promise.resolve(Effect.void),
17 "./migrations/0001_first.js": () => Promise.resolve(Effect.void),
18 "./migrations/0005_ignored.cjs": () => Promise.resolve(Effect.void)
19 })
20
21 assert.deepStrictEqual(migrationNames(migrations), [
22 [1, "first"],
23 [2, "second"],
24 [3, "third"],
25 [4, "fourth"]
26 ])
27 }))
28
29 it.effect("fromBabelGlob accepts Js, Ts, Mjs, and Mts suffixes", () =>
30 Effect.gen(function*() {
31 const migrations = yield* Migrator.fromBabelGlob({
32 _0004_fourthMts: Effect.void,
33 _0002_secondTs: Effect.void,
34 _0003_thirdMjs: Effect.void,
35 _0001_firstJs: Effect.void
36 })
37
38 assert.deepStrictEqual(migrationNames(migrations), [
39 [1, "first"],
40 [2, "second"],
41 [3, "third"],
42 [4, "fourth"]
43 ])
44 }))
45
46 it.effect("fromFileSystem accepts js, ts, mjs, and mts files", () =>
47 Effect.gen(function*() {
48 const migrations = yield* Migrator.fromFileSystem("/migrations").pipe(
49 Effect.provide(FileSystem.layerNoop({
50 readDirectory: () =>
51 Effect.succeed([
52 "0004_fourth.mts",
53 "0002_second.ts",
54 "0003_third.mjs",
55 "0001_first.js",
56 "0005_ignored.cjs"
57 ])
58 })),
59 Effect.provide(Path.layer)
60 )
61
62 assert.deepStrictEqual(migrationNames(migrations), [
63 [1, "first"],
64 [2, "second"],
65 [3, "third"],
66 [4, "fourth"]
67 ])
68 }))
69
70 it.effect("fromFileSystem imports migrations through the file URL the platform resolves", () =>
71 Effect.gen(function*() {
72 const posix = yield* Effect.provide(Path.Path, Path.layer)
73 const migrationUrl = new URL("./fixtures/migrator/0001_first.js", import.meta.url)
74 let resolvedPath: string | undefined
75 // stand in for a Windows platform layer, which core has no implementation of
76 const windowsLike = Layer.succeed(Path.Path)({
77 ...posix,
78 join: (...segments: ReadonlyArray<string>) => segments.join("\\"),
79 toFileUrl: (path: string) => {
80 resolvedPath = path
81 return Effect.succeed(migrationUrl)
82 }
83 })
84
85 const migrations = yield* Migrator.fromFileSystem("C:\\migrations").pipe(
86 Effect.provide(FileSystem.layerNoop({
87 readDirectory: () => Effect.succeed(["0001_first.js"])
88 })),
89 Effect.provide(windowsLike)
90 )
91
92 // the loader is lazy, and `ResolvedMigration` declares a `SqlClient`
93 // requirement this loader never uses
94 const load = migrations[0]![2] as Effect.Effect<unknown, unknown>
95 const imported = yield* load
96
97 assert.strictEqual(resolvedPath, "C:\\migrations\\0001_first.js")
98 assert.strictEqual((imported as { readonly marker: string }).marker, "loaded")
99 }))
100
101 it.effect("fromFileSystem surfaces file URL failures as import errors", () =>
102 Effect.gen(function*() {
103 const posix = yield* Effect.provide(Path.Path, Path.layer)
104 const failingPath = Layer.succeed(Path.Path)({
105 ...posix,
106 toFileUrl: () => Effect.fail(new PlatformError.BadArgument({ module: "Path", method: "toFileUrl" }))
107 })
108
109 const migrations = yield* Migrator.fromFileSystem("/migrations").pipe(
110 Effect.provide(FileSystem.layerNoop({
111 readDirectory: () => Effect.succeed(["0001_first.js"])
112 })),
113 Effect.provide(failingPath)
114 )
115
116 const load = migrations[0]![2] as Effect.Effect<unknown, unknown>
117 const outcome = yield* load.pipe(
118 Effect.catchDefect((defect) => Effect.succeed(`defect: ${defect}`)),
119 Effect.orElseSucceed(() => "no defect")
120 )
121
122 // a typed failure here would escape `make`'s MigrationError | SqlError channel
123 assert.include(outcome, "defect:")
124 }))
125 })
126})