@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/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 +76 -4
- 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/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"}
|