@shivaedev/effect-test-sql 0.3.0 → 0.5.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/CHANGELOG.md +20 -0
- package/README.md +62 -4
- package/dist/server/internal/connectionLimitHint.d.ts +3 -0
- package/dist/server/internal/connectionLimitHint.d.ts.map +1 -0
- package/dist/server/internal/connectionLimitHint.js +15 -0
- package/dist/server/internal/connectionLimitHint.js.map +1 -0
- package/dist/server/internal/databases.d.ts +4 -3
- package/dist/server/internal/databases.d.ts.map +1 -1
- package/dist/server/internal/databases.js +8 -4
- package/dist/server/internal/databases.js.map +1 -1
- package/dist/server/migrationSteps.d.ts +8 -0
- package/dist/server/migrationSteps.d.ts.map +1 -0
- package/dist/server/migrationSteps.js +44 -0
- package/dist/server/migrationSteps.js.map +1 -0
- package/dist/server/postgres.d.ts +4 -3
- package/dist/server/postgres.d.ts.map +1 -1
- package/dist/server/postgres.js +18 -16
- package/dist/server/postgres.js.map +1 -1
- package/dist/server/withIsolatedDatabase.d.ts.map +1 -1
- package/dist/server/withIsolatedDatabase.js +2 -1
- package/dist/server/withIsolatedDatabase.js.map +1 -1
- package/dist/server/withTestTransaction.d.ts.map +1 -1
- package/dist/server/withTestTransaction.js +2 -1
- package/dist/server/withTestTransaction.js.map +1 -1
- package/package.json +5 -4
- package/src/server/internal/connectionLimitHint.ts +16 -0
- package/src/server/internal/databases.ts +17 -7
- package/src/server/migrationSteps.ts +46 -0
- package/src/server/postgres.ts +22 -18
- package/src/server/withIsolatedDatabase.ts +2 -1
- package/src/server/withTestTransaction.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-10-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `migrationSteps(appMigrations)` from `migrationSteps.ts` provides `migrateTo("1_name")` and `migrateToLatest` on the current PostgreSQL client, so tests can seed old-schema rows before running a backfill. Unknown names fail with a typed `MigrationError`; loader dependencies and scope stay explicit.
|
|
8
|
+
- `makePostgresTestLayer({ prepare: false, ... })` skips migration and fixture preparation, giving isolated tests and test-story stories empty databases without a second migration list.
|
|
9
|
+
|
|
10
|
+
## 0.4.0 - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `PostgresTestOptions.maxConnections` configures the preparation, shared and isolated database pool limit per suite.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `makePostgresTestLayer` keeps its administration connection alive until fixture cleanup, so an idle period cannot leave database finalizers waiting for a new pooled connection during scope closure.
|
|
19
|
+
- Test database pools default to two connections, and administrative connections close after each create or drop, so concurrent database copies use a smaller PostgreSQL connection budget. Fresh scoped administration connections preserve database cleanup after idle periods without retaining a worker-lifetime connection.
|
|
20
|
+
- Setup, rollback and isolated test wrappers explain PostgreSQL connection-limit failures with `max_connections` and a `vitest run --maxWorkers=4` suggestion while retaining SQLSTATE `53300`.
|
|
21
|
+
- Publish against `@shivaedev/effect-sql` 0.5.0 so a generated app shares the same SQL service tags as Platform.
|
|
22
|
+
|
|
3
23
|
## 0.3.0 - 2026-10-13
|
|
4
24
|
|
|
5
25
|
### Breaking
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ The next test still sees the original name. Tests that must observe a committed
|
|
|
22
22
|
|
|
23
23
|
### Prepare one fixture
|
|
24
24
|
|
|
25
|
-
`makePostgresTestLayer` creates a template database through an administrative connection, runs `migratePostgres` and optional fixtures there, then closes its preparation connections. It creates a shared test database from that template and provides `SqlClient`, `PgClient`, `Reactivity` and `PostgresTestDatabase`. The template remains closed and unchanged while tests run. Each isolated database is another copy of that template; its migrations and fixtures do not run again.
|
|
25
|
+
`makePostgresTestLayer` creates a template database through an administrative connection, runs `migratePostgres` and optional fixtures there, then closes its preparation connections. It creates a shared test database from that template and provides `SqlClient`, `PgClient`, `Reactivity` and `PostgresTestDatabase`. The template remains closed and unchanged while tests run. Each isolated database is another copy of that template; its migrations and fixtures do not run again. Set `prepare: false` for migration tests: both migrations and fixtures are skipped, so the shared database and each clone start empty.
|
|
26
26
|
|
|
27
27
|
The Layer's scope owns the template, shared database and their connections. `makeEffectIt` builds the Layer once for its worker fixture. Each test's wrapper owns the transaction or isolated database. Closing those scopes releases the connections and drops the disposable databases; the administrative database named by `url` remains in place.
|
|
28
28
|
|
|
@@ -63,6 +63,24 @@ export const { effectApp } = makeEffectIt({
|
|
|
63
63
|
|
|
64
64
|
Use the application's migration loader in place of the example. `migrations` takes the existing `PostgresMigrationOptions`: `loader`, `lockTimeout`, and an optional ledger `table`. Fixtures are an Effect with access to the native PostgreSQL services. Additional migration or fixture dependencies remain requirements of the Layer and must be supplied before passing it to the runner. Setup failures remain failures; acquired databases are dropped when the acquisition scope closes.
|
|
65
65
|
|
|
66
|
+
### Budget PostgreSQL connections
|
|
67
|
+
|
|
68
|
+
Each database pool opens connections on demand, with a default maximum of **two**: the shared database can use two, and each active per-test or per-story clone can use another two. Preparation uses the same limit and closes before tests start. An administrative connection exists only during a create or drop operation and closes as soon as that operation finishes; idle workers keep no administrative connection. Set `maxConnections` on `makePostgresTestLayer` to override the limit for the suite's preparation, shared and isolated pools:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const layer = makePostgresTestLayer({
|
|
72
|
+
url,
|
|
73
|
+
migrations,
|
|
74
|
+
maxConnections: 3,
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Allow at least three shared connections when two tests hold transactions while a third connection inspects locks. A listener, stream or reserved transaction holds a pool slot until its scope closes; a suite that waits for more simultaneous reservations than its pool allows can stall. Additional clients that the application creates have their own connection budgets.
|
|
79
|
+
|
|
80
|
+
For `W` active workers with one fixture and at most `C` simultaneous clones each, allow up to `W × maxConnections × (1 + C)` pooled connections, plus one transient administrative connection per concurrent create/drop operation. Add other application clients and leave room for PostgreSQL's reserved slots and other users of the server. With stock `max_connections = 100`, four workers with one clone each use at most 16 pooled connections at the default limit, plus transient administration connections. Vitest's default worker count depends on the machine; use this budget when a larger machine or concurrent stories can exceed the server's capacity.
|
|
81
|
+
|
|
82
|
+
Setup and the test wrappers report PostgreSQL's connection-limit failure (`53300`) with `max_connections` and a suggestion to try `vitest run --maxWorkers=4`. Reduce `maxConnections` or concurrent clones, or increase the server budget, if four workers still need too many connections. Other SQL failures retain their original errors.
|
|
83
|
+
|
|
66
84
|
### Ordinary tests: rollback
|
|
67
85
|
|
|
68
86
|
`around: withTestTransaction` wraps harness acquisition, the body and their scope finalizers. Success returns the body's value after rollback. Typed failures, defects and interruption also roll back and retain their failure kind. A nested `transact` uses the native savepoint path: a rejected nested write can roll back without losing earlier writes in the test. Commit-bound invalidations stay buffered and are discarded when the wrapping transaction rolls back.
|
|
@@ -92,6 +110,45 @@ The clone starts from the template, including the original fixtures, even if the
|
|
|
92
110
|
|
|
93
111
|
Inside an isolated block, `PostgresTestDatabase.url` names that clone. Use it to acquire additional native PostgreSQL connections in the block's scope. Native SQL services are replaced in the block's Context; application services acquired earlier retain the client they captured. Build application service Layers inside the isolated block when they acquire SQL clients; use `Layer.fresh` when reusing a Layer definition already acquired by the worker. Keep servers and background fibers in that scope so they finish before the database is dropped. Separate databases isolate database contents; external services and server capacity need separate planning.
|
|
94
112
|
|
|
113
|
+
### Test a backfill against existing rows
|
|
114
|
+
|
|
115
|
+
A backfill needs rows in the old schema. Prepare an empty template with the application's own `PostgresMigrationOptions`, then use `migrationSteps` inside `withIsolatedDatabase`. `migrateTo` takes the full migration name, including its numeric prefix and without a file extension. It commits migrations through that name; seed rows with the native SQL client, then yield `migrateToLatest` to apply the remaining migrations on the same database.
|
|
116
|
+
|
|
117
|
+
This example uses an application loader whose `1_create_contacts` migration creates `contacts(id, email)`, `2_backfill_emails` creates `emails(contact_id, address)` and copies the contact email addresses into it, and `3_drop_email` drops `contacts.email`:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { migrationSteps } from "@shivaedev/effect-test-sql/migrationSteps.ts";
|
|
121
|
+
import { withIsolatedDatabase } from "@shivaedev/effect-test-sql/withIsolatedDatabase.ts";
|
|
122
|
+
import { appMigrations } from "./migrations.ts";
|
|
123
|
+
|
|
124
|
+
const migrationLayer = makePostgresTestLayer({
|
|
125
|
+
url,
|
|
126
|
+
migrations: appMigrations,
|
|
127
|
+
prepare: false,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const { effectApp: migrationTest } = makeEffectIt({
|
|
131
|
+
layer: migrationLayer,
|
|
132
|
+
clock: "live",
|
|
133
|
+
around: withIsolatedDatabase,
|
|
134
|
+
makeHarness: () => migrationSteps(appMigrations),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
migrationTest("preserves contact emails in the new table", function* (steps) {
|
|
138
|
+
yield* steps.migrateTo("1_create_contacts");
|
|
139
|
+
const sql = yield* SqlClient.SqlClient;
|
|
140
|
+
yield* sql`insert into contacts (id, email) values (1, 'alice@example.test')`;
|
|
141
|
+
yield* steps.migrateToLatest;
|
|
142
|
+
expect(yield* sql`select contact_id, address from emails`).toEqual([
|
|
143
|
+
{ contact_id: 1, address: "alice@example.test" },
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The helper resolves the loader once and uses `migratePostgres` for each step, preserving its ledger table and lock timeout. Unknown names fail with a typed `MigrationError` that names the requested migration. Moving behind an already applied migration and running inside a wrapping transaction also fail with `MigrationError`. Loader dependencies, including `Scope`, remain requirements of `migrationSteps`; its resources live until the caller's scope closes. Use the returned steps inside that database's scope. SQL and loader failures retain their failure channels.
|
|
149
|
+
|
|
150
|
+
For test-story, use the same Layer and `around: withIsolatedDatabase`, acquire `migrationSteps(appMigrations)` in the kit's `create`, and let schema and data traits call `migrateTo` and seed native SQL. A verb can yield `migrateToLatest` before the story asserts the backfilled rows.
|
|
151
|
+
|
|
95
152
|
### Check upgrades against the current model
|
|
96
153
|
|
|
97
154
|
A fresh-database test cannot prove that older databases upgrade to the current model. Run `migrationUpgrade` in a disposable database with an empty migration ledger, outside a wrapping transaction. It resolves the application's loader once, commits migrations through the existing `upTo` id, then applies the remaining migrations through `migratePostgres`. It returns the target table's column names and PostgreSQL `format_type` strings, including type modifiers such as `numeric(10,2)`; dropped columns are excluded.
|
|
@@ -112,7 +169,7 @@ yield* assertModelColumns(columns, Item, {
|
|
|
112
169
|
});
|
|
113
170
|
```
|
|
114
171
|
|
|
115
|
-
Use
|
|
172
|
+
Use `prepare: false` when preparing the test Layer, and `withIsolatedDatabase` for the upgrade test. The helper requires the native `SqlClient`; loader dependencies remain Effect requirements. Unknown starting ids, populated ledgers and wrapping transactions fail with `MigrationError`. Native loader and SQL failures retain their failure channel.
|
|
116
173
|
|
|
117
174
|
The assertion uses the SQL `Model`'s selected database fields, including generated ids and excluding JSON-only fields. Supply an exact PostgreSQL type for every field: schemas such as `Schema.Number` do not determine whether storage is `integer`, `numeric` or `double precision`. It fails with a typed `ModelColumnMismatch` containing all detected missing, extra and wrongly typed column problems. The type map is checked against the model both by TypeScript and at runtime. This check covers names and types; it does not check defaults, nullability, constraints or application data transformations.
|
|
118
175
|
|
|
@@ -124,10 +181,11 @@ Import each name from the module that defines it. The package has no root entry.
|
|
|
124
181
|
|
|
125
182
|
| Module | Exports |
|
|
126
183
|
| --- | --- |
|
|
127
|
-
| `@shivaedev/effect-test-sql/postgres.ts` | `makePostgresTestLayer({ url, migrations, fixtures? })`, `PostgresTestOptions` |
|
|
184
|
+
| `@shivaedev/effect-test-sql/postgres.ts` | `makePostgresTestLayer({ url, migrations, fixtures?, maxConnections?, prepare? })`, `PostgresTestOptions` |
|
|
128
185
|
| `@shivaedev/effect-test-sql/withTestTransaction.ts` | `withTestTransaction(effect)` |
|
|
129
186
|
| `@shivaedev/effect-test-sql/withIsolatedDatabase.ts` | `withIsolatedDatabase(effect)` |
|
|
130
187
|
| `@shivaedev/effect-test-sql/database.ts` | `PostgresTestDatabase`, `PostgresTestDatabaseService`, `PostgresTestServices` |
|
|
188
|
+
| `@shivaedev/effect-test-sql/migrationSteps.ts` | `migrationSteps(appMigrations)` returning `{ migrateTo(name), migrateToLatest }` |
|
|
131
189
|
| `@shivaedev/effect-test-sql/migrationUpgrade.ts` | `migrationUpgrade({ migrations, upTo, table })`, `MigrationUpgradeOptions`, `PostgresColumn` |
|
|
132
190
|
| `@shivaedev/effect-test-sql/assertModelColumns.ts` | `assertModelColumns(columns, model, types)`, `ModelColumnMismatch` |
|
|
133
191
|
|
|
@@ -141,4 +199,4 @@ pnpm add --save-dev @shivaedev/effect-test-sql @shivaedev/effect-test @effect/sq
|
|
|
141
199
|
|
|
142
200
|
Use versions compatible with the package's peer dependencies. The package requires Node.js 24 or later. The PostgreSQL role must be able to create and drop databases; use a dedicated test server. PostgreSQL 13 or later supports the forced database drops used for cleanup. The migrations and fixtures must complete their work during preparation; their scoped resources are closed before cloning.
|
|
143
201
|
|
|
144
|
-
The package tests use `PLATFORM_EFFECT_TEST_SQL_TEST_DATABASE_URL`. Repository setup and CI prepare that database on real PostgreSQL. Local tests skip when that variable is absent; CI rejects its absence. The PostgreSQL tests prove overlapping same-row visibility, shared-key lock contention and release, rollback, nested savepoints, runner/harness scope composition, one-time template preparation, commits visible through another connection, and database cleanup. Compiler and packed-consumer tests prove the exercised public type and export contracts. These are library checks, not evidence for an application's servers or notification delivery.
|
|
202
|
+
The package tests use `PLATFORM_EFFECT_TEST_SQL_TEST_DATABASE_URL`. Repository setup and CI prepare that database on real PostgreSQL. Local tests skip when that variable is absent; CI rejects its absence. The PostgreSQL tests prove overlapping same-row visibility, shared-key lock contention and release, rollback, nested savepoints, runner/harness scope composition, one-time template preparation, commits visible through another connection, and database cleanup. They also measure twelve concurrent clones through `pg_stat_activity` at both the default and overridden pool limits, verify administrative connection release, and exercise connection-limit diagnostics using restricted PostgreSQL roles. Compiler and packed-consumer tests prove the exercised public type and export contracts. These are library checks, not evidence for an application's servers or notification delivery.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectionLimitHint.d.ts","sourceRoot":"","sources":["../../../src/server/internal/connectionLimitHint.ts"],"names":[],"mappings":"AACA,OAAO,EAA+B,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5E,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,QAAQ,CAY7D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Predicate } from "effect";
|
|
2
|
+
import { ConnectionError, isSqlError, SqlError } from "effect/sql/SqlError";
|
|
3
|
+
export function connectionLimitHint(error) {
|
|
4
|
+
if (!(isSqlError(error) && Predicate.hasProperty(error.reason.cause, "code")) || error.reason.cause.code !== "53300") {
|
|
5
|
+
return error;
|
|
6
|
+
}
|
|
7
|
+
return new SqlError({
|
|
8
|
+
reason: new ConnectionError({
|
|
9
|
+
cause: error.reason.cause,
|
|
10
|
+
message: "PostgreSQL connection limit reached (SQLSTATE 53300): check max_connections and role connection limits. Try vitest run --maxWorkers=4, reduce the suite's maxConnections, or increase the server's connection budget.",
|
|
11
|
+
operation: error.reason.operation,
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=connectionLimitHint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectionLimitHint.js","sourceRoot":"","sources":["../../../src/server/internal/connectionLimitHint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,UAAU,mBAAmB,CAAI,KAAQ;IAC9C,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACtH,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,IAAI,QAAQ,CAAC;QACnB,MAAM,EAAE,IAAI,eAAe,CAAC;YAC3B,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK;YACzB,OAAO,EACN,uNAAuN;YACxN,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,SAAS;SACjC,CAAC;KACF,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -2,9 +2,10 @@ import * as PgClient from "@effect/sql-pg/PgClient";
|
|
|
2
2
|
import { Effect, Layer, Redacted } from "effect";
|
|
3
3
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
4
|
import { SqlClient } from "effect/sql";
|
|
5
|
-
|
|
6
|
-
export declare
|
|
5
|
+
import type { SqlError } from "effect/sql/SqlError";
|
|
6
|
+
export declare function connection(url: Redacted.Redacted<string>, maxConnections: number): Layer.Layer<PgClient.PgClient | Reactivity.Reactivity | SqlClient.SqlClient, SqlError, never>;
|
|
7
|
+
export declare const database: (url: Redacted.Redacted<string>, template?: any) => Effect.Effect<{
|
|
7
8
|
name: string;
|
|
8
9
|
url: Redacted.Redacted<string>;
|
|
9
|
-
},
|
|
10
|
+
}, SqlError, import("effect/Scope").Scope>;
|
|
10
11
|
//# sourceMappingURL=databases.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"databases.d.ts","sourceRoot":"","sources":["../../../src/server/internal/databases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAW,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"databases.d.ts","sourceRoot":"","sources":["../../../src/server/internal/databases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAW,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,MAAM,iGAMhF;AAWD,eAAO,MAAM,QAAQ;;;0CAUnB,CAAC"}
|
|
@@ -2,15 +2,19 @@ import * as PgClient from "@effect/sql-pg/PgClient";
|
|
|
2
2
|
import { Context, Effect, Layer, Redacted } from "effect";
|
|
3
3
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
4
|
import { SqlClient } from "effect/sql";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { connectionLimitHint } from "./connectionLimitHint.js";
|
|
6
|
+
export function connection(url, maxConnections) {
|
|
7
|
+
return Layer.effectContext(Effect.map(PgClient.make({ maxConnections, url }), (client) => Context.make(SqlClient.SqlClient, client).pipe(Context.add(PgClient.PgClient, client)))).pipe(Layer.provideMerge(Layer.fresh(Reactivity.layer)));
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
+
const administer = Effect.fn("PostgresTest.administer")(function* (url, operation) {
|
|
10
|
+
return yield* Effect.scoped(Effect.flatMap(PgClient.makeClient({ url }), operation).pipe(Effect.mapError(connectionLimitHint))).pipe(Effect.provide(Reactivity.layer));
|
|
11
|
+
});
|
|
12
|
+
export const database = Effect.fn("PostgresTest.database")(function* (url, template = "template0") {
|
|
9
13
|
const name = `effect_test_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
10
14
|
const target = new URL(Redacted.value(url));
|
|
11
15
|
target.pathname = `/${name}`;
|
|
12
16
|
target.searchParams.delete("database");
|
|
13
|
-
yield* Effect.acquireRelease(admin `create database ${admin(name)} template ${admin(template)}
|
|
17
|
+
yield* Effect.acquireRelease(administer(url, (admin) => admin `create database ${admin(name)} template ${admin(template)}`), () => Effect.orDie(administer(url, (admin) => admin `drop database ${admin(name)} with (force)`)));
|
|
14
18
|
return { name, url: Redacted.make(target.toString()) };
|
|
15
19
|
});
|
|
16
20
|
//# sourceMappingURL=databases.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"databases.js","sourceRoot":"","sources":["../../../src/server/internal/databases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,UAAU,UAAU,CAAC,GAA8B;
|
|
1
|
+
{"version":3,"file":"databases.js","sourceRoot":"","sources":["../../../src/server/internal/databases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,MAAM,UAAU,UAAU,CAAC,GAA8B,EAAE,cAAsB;IAChF,OAAO,KAAK,CAAC,aAAa,CACzB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAC7D,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CACtF,CACD,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,yBAAyB,CAAC,CAAC,QAAQ,CAAC,EAChE,GAA8B,EAC9B,SAAyE;IAEzE,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,IAAI,CACnI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAChC,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,uBAAuB,CAAC,CAAC,QAAQ,CAAC,EAAE,GAA8B,EAAE,QAAQ,GAAG,WAAW;IAC3H,MAAM,IAAI,GAAG,eAAe,MAAM,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;IACtE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,MAAM,CAAC,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC;IAC7B,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACvC,KAAK,CAAC,CAAC,MAAM,CAAC,cAAc,CAC3B,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAA,mBAAmB,KAAK,CAAC,IAAI,CAAC,aAAa,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,EAC7F,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAA,iBAAiB,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAChG,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Migrator, SqlClient } from "effect/sql";
|
|
3
|
+
import { type PostgresMigrationOptions } from "@shivaedev/effect-sql/migrations.ts";
|
|
4
|
+
export declare const migrationSteps: <R>(migrations: PostgresMigrationOptions<R>) => Effect.Effect<{
|
|
5
|
+
migrateTo: (name: string) => Effect.Effect<readonly (readonly [id: number, name: string])[], Migrator.MigrationError | import("effect/sql/SqlError").SqlError, never>;
|
|
6
|
+
migrateToLatest: Effect.Effect<readonly (readonly [id: number, name: string])[], Migrator.MigrationError | import("effect/sql/SqlError").SqlError, never>;
|
|
7
|
+
}, Migrator.MigrationError, R | SqlClient.SqlClient>;
|
|
8
|
+
//# sourceMappingURL=migrationSteps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrationSteps.d.ts","sourceRoot":"","sources":["../../src/server/migrationSteps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAmB,KAAK,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAYrG,eAAO,MAAM,cAAc,GAAuD,CAAC;sBAuB/D,MAAM;;oDAQxB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { Migrator, SqlClient } from "effect/sql";
|
|
3
|
+
import { migratePostgres } from "@shivaedev/effect-sql/migrations.ts";
|
|
4
|
+
function invalid(message) {
|
|
5
|
+
return Effect.fail(new Migrator.MigrationError({ kind: "BadState", message }));
|
|
6
|
+
}
|
|
7
|
+
const outsideTransaction = Effect.fnUntraced(function* (sql) {
|
|
8
|
+
if (Option.isSome(yield* Effect.serviceOption(sql.transactionService))) {
|
|
9
|
+
return yield* invalid("Migration steps must run outside a wrapping transaction.");
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
export const migrationSteps = Effect.fn("PostgresTest.migrationSteps")(function* (migrations) {
|
|
13
|
+
const sql = yield* SqlClient.SqlClient;
|
|
14
|
+
yield* sql.onDialectOrElse({ orElse: () => invalid("Migration steps require PostgreSQL."), pg: () => Effect.void });
|
|
15
|
+
yield* outsideTransaction(sql);
|
|
16
|
+
const resolved = yield* migrations.loader;
|
|
17
|
+
const ledger = migrations.table ?? "effect_sql_migrations";
|
|
18
|
+
const identifier = sql `${sql(ledger)}`.compile()[0];
|
|
19
|
+
function run(loader, target) {
|
|
20
|
+
return Effect.gen(function* () {
|
|
21
|
+
yield* outsideTransaction(sql);
|
|
22
|
+
if (target !== undefined) {
|
|
23
|
+
const [existing] = yield* sql `select to_regclass(${identifier})::text as name`.withoutTransform;
|
|
24
|
+
if (existing !== undefined && existing.name !== null) {
|
|
25
|
+
const later = yield* sql `select migration_id from ${sql(ledger)} where migration_id > ${target} limit 1`;
|
|
26
|
+
if (later.length > 0) {
|
|
27
|
+
return yield* invalid(`Cannot migrate to ${target}: later migrations are already applied.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return yield* migratePostgres({ ...migrations, loader }).pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
migrateTo: (name) => {
|
|
36
|
+
const target = resolved.find(([id, label]) => `${id}_${label}` === name);
|
|
37
|
+
return target === undefined
|
|
38
|
+
? invalid(`Migration "${name}" is not in the loader.`)
|
|
39
|
+
: run(Effect.succeed(resolved.filter(([id]) => id <= target[0])), target[0]);
|
|
40
|
+
},
|
|
41
|
+
migrateToLatest: run(Effect.succeed(resolved)),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=migrationSteps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrationSteps.js","sourceRoot":"","sources":["../../src/server/migrationSteps.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import * as PgClient from "@effect/sql-pg/PgClient";
|
|
2
1
|
import { Effect, Layer, type Redacted } from "effect";
|
|
3
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
3
|
import { type PostgresMigrationOptions } from "@shivaedev/effect-sql/migrations.ts";
|
|
5
|
-
import { PostgresTestDatabase, type PostgresTestServices } from "
|
|
4
|
+
import { PostgresTestDatabase, type PostgresTestServices } from "#database.ts";
|
|
6
5
|
export interface PostgresTestOptions<E = never, R = never, TFixtures = never> {
|
|
7
6
|
readonly fixtures?: Effect.Effect<unknown, E, TFixtures | PostgresTestServices>;
|
|
7
|
+
readonly maxConnections?: number;
|
|
8
8
|
readonly migrations: PostgresMigrationOptions<R>;
|
|
9
|
+
readonly prepare?: boolean;
|
|
9
10
|
readonly url: Redacted.Redacted<string>;
|
|
10
11
|
}
|
|
11
|
-
export declare function makePostgresTestLayer<E = never, R = never, TFixtures = never>(options: PostgresTestOptions<E, R, TFixtures>): Layer.Layer<PgClient.PgClient | PostgresTestDatabase | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient, E | import("effect/sql/Migrator").MigrationError | import("effect/sql/SqlError").SqlError, Exclude<Exclude<Exclude<Exclude<R, PgClient.PgClient | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient>, import("effect/Scope").Scope>, import("effect/Scope").Scope>, Reactivity.Reactivity> | Exclude<Exclude<Exclude<Exclude<TFixtures, PgClient.PgClient | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient>, import("effect/Scope").Scope>, import("effect/Scope").Scope>, Reactivity.Reactivity>>;
|
|
12
|
+
export declare function makePostgresTestLayer<E = never, R = never, TFixtures = never>(options: PostgresTestOptions<E, R, TFixtures>): Layer.Layer<import("@effect/sql-pg/PgClient").PgClient | PostgresTestDatabase | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient, E | import("effect/sql/Migrator").MigrationError | import("effect/sql/SqlError").SqlError, Exclude<Exclude<Exclude<Exclude<R, import("@effect/sql-pg/PgClient").PgClient | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient>, import("effect/Scope").Scope>, import("effect/Scope").Scope>, Reactivity.Reactivity> | Exclude<Exclude<Exclude<Exclude<TFixtures, import("@effect/sql-pg/PgClient").PgClient | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient>, import("effect/Scope").Scope>, import("effect/Scope").Scope>, Reactivity.Reactivity>>;
|
|
12
13
|
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/server/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/server/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,MAAM,EAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC/D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAmB,KAAK,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AACrG,OAAO,EAAE,oBAAoB,EAAoC,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAIjH,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,GAAG,KAAK;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,SAAS,GAAG,oBAAoB,CAAC,CAAC;IAChF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CACxC;AAED,wBAAgB,qBAAqB,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,GAAG,KAAK,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,2sBA0B3H"}
|
package/dist/server/postgres.js
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
import * as PgClient from "@effect/sql-pg/PgClient";
|
|
2
1
|
import { Context, Effect, Layer } from "effect";
|
|
3
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
3
|
import { migratePostgres } from "@shivaedev/effect-sql/migrations.ts";
|
|
4
|
+
import { PostgresTestDatabase } from "#database.ts";
|
|
5
|
+
import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
|
|
5
6
|
import { connection, database } from "#internal/databases.ts";
|
|
6
|
-
import { PostgresTestDatabase } from "./database.js";
|
|
7
7
|
export function makePostgresTestLayer(options) {
|
|
8
8
|
return Layer.effectContext(Effect.gen(function* () {
|
|
9
|
-
const
|
|
10
|
-
const template = yield* database(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
const maxConnections = options.maxConnections ?? 2;
|
|
10
|
+
const template = yield* database(options.url);
|
|
11
|
+
if (options.prepare !== false) {
|
|
12
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
13
|
+
const services = yield* Layer.build(connection(template.url, maxConnections));
|
|
14
|
+
yield* migratePostgres(options.migrations).pipe(Effect.provideContext(services));
|
|
15
|
+
if (options.fixtures !== undefined) {
|
|
16
|
+
yield* options.fixtures.pipe(Effect.provideContext(services));
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
const shared = yield* database(options.url, template.name);
|
|
21
|
+
const services = yield* Layer.build(connection(shared.url, maxConnections));
|
|
20
22
|
const isolated = Effect.gen(function* () {
|
|
21
|
-
const clone = yield* database(
|
|
22
|
-
const context = yield* Layer.build(connection(clone.url));
|
|
23
|
+
const clone = yield* database(options.url, template.name);
|
|
24
|
+
const context = yield* Layer.build(connection(clone.url, maxConnections));
|
|
23
25
|
return Context.add(context, PostgresTestDatabase, { isolated, url: clone.url });
|
|
24
26
|
});
|
|
25
27
|
return Context.add(services, PostgresTestDatabase, { isolated, url: shared.url });
|
|
26
|
-
})).pipe(Layer.provide(Reactivity.layer));
|
|
28
|
+
}).pipe(Effect.mapError(connectionLimitHint))).pipe(Layer.provide(Reactivity.layer));
|
|
27
29
|
}
|
|
28
30
|
//# sourceMappingURL=postgres.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/server/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/server/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAiB,MAAM,QAAQ,CAAC;AAC/D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAiC,MAAM,qCAAqC,CAAC;AACrG,OAAO,EAAE,oBAAoB,EAA+D,MAAM,cAAc,CAAC;AACjH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAU9D,MAAM,UAAU,qBAAqB,CAA0C,OAA6C;IAC3H,OAAO,KAAK,CAAC,aAAa,CACzB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YAC/B,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;gBAC9E,KAAK,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACjF,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;oBACpC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC/D,CAAC;YACF,CAAC,CAAC,CACF,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;QAC5E,MAAM,QAAQ,GAA4C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC7E,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC1D,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;YAC1E,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,oBAAoB,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;QACjF,CAAC,CAAC,CAAC;QACH,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAC7C,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withIsolatedDatabase.d.ts","sourceRoot":"","sources":["../../src/server/withIsolatedDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"withIsolatedDatabase.d.ts","sourceRoot":"","sources":["../../src/server/withIsolatedDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,+MAQ3E"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
+
import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
|
|
2
3
|
import { PostgresTestDatabase } from "./database.js";
|
|
3
4
|
export function withIsolatedDatabase(effect) {
|
|
4
5
|
return Effect.scoped(Effect.gen(function* () {
|
|
5
6
|
const database = yield* PostgresTestDatabase;
|
|
6
7
|
const services = yield* database.isolated;
|
|
7
8
|
return yield* effect.pipe(Effect.provideContext(services));
|
|
8
|
-
}));
|
|
9
|
+
})).pipe(Effect.mapError(connectionLimitHint));
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=withIsolatedDatabase.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withIsolatedDatabase.js","sourceRoot":"","sources":["../../src/server/withIsolatedDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,UAAU,oBAAoB,CAAU,MAA8B;IAC3E,OAAO,MAAM,CAAC,MAAM,CACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,oBAAoB,CAAC;QAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC1C,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5D,CAAC,CAAC,CACF,CAAC;
|
|
1
|
+
{"version":3,"file":"withIsolatedDatabase.js","sourceRoot":"","sources":["../../src/server/withIsolatedDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,UAAU,oBAAoB,CAAU,MAA8B;IAC3E,OAAO,MAAM,CAAC,MAAM,CACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,oBAAoB,CAAC;QAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC1C,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5D,CAAC,CAAC,CACF,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withTestTransaction.d.ts","sourceRoot":"","sources":["../../src/server/withTestTransaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"withTestTransaction.d.ts","sourceRoot":"","sources":["../../src/server/withTestTransaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAQpD,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,2KAO1E"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Effect, Exit } from "effect";
|
|
2
2
|
import { transact } from "@shivaedev/effect-sql/transact.ts";
|
|
3
|
+
import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
|
|
3
4
|
function isTestExit(value) {
|
|
4
5
|
return Exit.isExit(value);
|
|
5
6
|
}
|
|
6
7
|
export function withTestTransaction(effect) {
|
|
7
|
-
return Effect.uninterruptibleMask((restore) => transact(Effect.flatMap(Effect.exit(Effect.scoped(restore(effect))), Effect.fail), { onSqlError: (error) => error }).pipe(Effect.catchIf(isTestExit, (exit) => exit)));
|
|
8
|
+
return Effect.uninterruptibleMask((restore) => transact(Effect.flatMap(Effect.exit(Effect.scoped(restore(effect))), Effect.fail), { onSqlError: (error) => error }).pipe(Effect.catchIf(isTestExit, (exit) => exit), Effect.mapError(connectionLimitHint)));
|
|
8
9
|
}
|
|
9
10
|
//# sourceMappingURL=withTestTransaction.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withTestTransaction.js","sourceRoot":"","sources":["../../src/server/withTestTransaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"withTestTransaction.js","sourceRoot":"","sources":["../../src/server/withTestTransaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAEvE,SAAS,UAAU,CAAO,KAAiC;IAC1D,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAU,MAA8B;IAC1E,OAAO,MAAM,CAAC,mBAAmB,CAAC,CAAC,OAAO,EAAE,EAAE,CAC7C,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CACxH,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAC1C,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CACpC,CACD,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-test-sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "PostgreSQL rollback and real-commit test isolation for Effect",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-test-sql#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -42,13 +42,14 @@
|
|
|
42
42
|
"README.md"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@shivaedev/effect-sql": "0.
|
|
45
|
+
"@shivaedev/effect-sql": "0.5.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@effect/sql-pg": "4.0.4",
|
|
49
49
|
"@effect/vitest": "4.0.4",
|
|
50
|
-
"@shivaedev/effect-test": "0.
|
|
51
|
-
"@shivaedev/quality": "0.
|
|
50
|
+
"@shivaedev/effect-test": "0.5.0",
|
|
51
|
+
"@shivaedev/quality": "0.22.0",
|
|
52
|
+
"@shivaedev/test-story": "0.4.1",
|
|
52
53
|
"@types/node": "24.19.1",
|
|
53
54
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
54
55
|
"effect": "4.0.4",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Predicate } from "effect";
|
|
2
|
+
import { ConnectionError, isSqlError, SqlError } from "effect/sql/SqlError";
|
|
3
|
+
|
|
4
|
+
export function connectionLimitHint<E>(error: E): E | SqlError {
|
|
5
|
+
if (!(isSqlError(error) && Predicate.hasProperty(error.reason.cause, "code")) || error.reason.cause.code !== "53300") {
|
|
6
|
+
return error;
|
|
7
|
+
}
|
|
8
|
+
return new SqlError({
|
|
9
|
+
reason: new ConnectionError({
|
|
10
|
+
cause: error.reason.cause,
|
|
11
|
+
message:
|
|
12
|
+
"PostgreSQL connection limit reached (SQLSTATE 53300): check max_connections and role connection limits. Try vitest run --maxWorkers=4, reduce the suite's maxConnections, or increase the server's connection budget.",
|
|
13
|
+
operation: error.reason.operation,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -2,24 +2,34 @@ import * as PgClient from "@effect/sql-pg/PgClient";
|
|
|
2
2
|
import { Context, Effect, Layer, Redacted } from "effect";
|
|
3
3
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
4
|
import { SqlClient } from "effect/sql";
|
|
5
|
+
import type { SqlError } from "effect/sql/SqlError";
|
|
6
|
+
import { connectionLimitHint } from "./connectionLimitHint.ts";
|
|
5
7
|
|
|
6
|
-
export function connection(url: Redacted.Redacted<string
|
|
8
|
+
export function connection(url: Redacted.Redacted<string>, maxConnections: number) {
|
|
7
9
|
return Layer.effectContext(
|
|
8
|
-
Effect.map(PgClient.make({ url }), (client) =>
|
|
10
|
+
Effect.map(PgClient.make({ maxConnections, url }), (client) =>
|
|
11
|
+
Context.make(SqlClient.SqlClient, client).pipe(Context.add(PgClient.PgClient, client)),
|
|
12
|
+
),
|
|
9
13
|
).pipe(Layer.provideMerge(Layer.fresh(Reactivity.layer)));
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
admin: PgClient.PgClient,
|
|
16
|
+
const administer = Effect.fn("PostgresTest.administer")(function* (
|
|
14
17
|
url: Redacted.Redacted<string>,
|
|
15
|
-
|
|
18
|
+
operation: (admin: PgClient.PgClient) => Effect.Effect<unknown, SqlError>,
|
|
16
19
|
) {
|
|
20
|
+
return yield* Effect.scoped(Effect.flatMap(PgClient.makeClient({ url }), operation).pipe(Effect.mapError(connectionLimitHint))).pipe(
|
|
21
|
+
Effect.provide(Reactivity.layer),
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const database = Effect.fn("PostgresTest.database")(function* (url: Redacted.Redacted<string>, template = "template0") {
|
|
17
26
|
const name = `effect_test_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
18
27
|
const target = new URL(Redacted.value(url));
|
|
19
28
|
target.pathname = `/${name}`;
|
|
20
29
|
target.searchParams.delete("database");
|
|
21
|
-
yield* Effect.acquireRelease(
|
|
22
|
-
|
|
30
|
+
yield* Effect.acquireRelease(
|
|
31
|
+
administer(url, (admin) => admin`create database ${admin(name)} template ${admin(template)}`),
|
|
32
|
+
() => Effect.orDie(administer(url, (admin) => admin`drop database ${admin(name)} with (force)`)),
|
|
23
33
|
);
|
|
24
34
|
return { name, url: Redacted.make(target.toString()) };
|
|
25
35
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { Migrator, SqlClient } from "effect/sql";
|
|
3
|
+
import { migratePostgres, type PostgresMigrationOptions } from "@shivaedev/effect-sql/migrations.ts";
|
|
4
|
+
|
|
5
|
+
function invalid(message: string) {
|
|
6
|
+
return Effect.fail(new Migrator.MigrationError({ kind: "BadState", message }));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const outsideTransaction = Effect.fnUntraced(function* (sql: SqlClient.SqlClient) {
|
|
10
|
+
if (Option.isSome(yield* Effect.serviceOption(sql.transactionService))) {
|
|
11
|
+
return yield* invalid("Migration steps must run outside a wrapping transaction.");
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const migrationSteps = Effect.fn("PostgresTest.migrationSteps")(function* <R>(migrations: PostgresMigrationOptions<R>) {
|
|
16
|
+
const sql = yield* SqlClient.SqlClient;
|
|
17
|
+
yield* sql.onDialectOrElse({ orElse: () => invalid("Migration steps require PostgreSQL."), pg: () => Effect.void });
|
|
18
|
+
yield* outsideTransaction(sql);
|
|
19
|
+
const resolved = yield* migrations.loader;
|
|
20
|
+
const ledger = migrations.table ?? "effect_sql_migrations";
|
|
21
|
+
const identifier = sql`${sql(ledger)}`.compile()[0];
|
|
22
|
+
function run(loader: Migrator.Loader, target?: number) {
|
|
23
|
+
return Effect.gen(function* () {
|
|
24
|
+
yield* outsideTransaction(sql);
|
|
25
|
+
if (target !== undefined) {
|
|
26
|
+
const [existing] = yield* sql<{ readonly name: string | null }>`select to_regclass(${identifier})::text as name`.withoutTransform;
|
|
27
|
+
if (existing !== undefined && existing.name !== null) {
|
|
28
|
+
const later = yield* sql`select migration_id from ${sql(ledger)} where migration_id > ${target} limit 1`;
|
|
29
|
+
if (later.length > 0) {
|
|
30
|
+
return yield* invalid(`Cannot migrate to ${target}: later migrations are already applied.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return yield* migratePostgres({ ...migrations, loader }).pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
migrateTo: (name: string) => {
|
|
39
|
+
const target = resolved.find(([id, label]) => `${id}_${label}` === name);
|
|
40
|
+
return target === undefined
|
|
41
|
+
? invalid(`Migration "${name}" is not in the loader.`)
|
|
42
|
+
: run(Effect.succeed(resolved.filter(([id]) => id <= target[0])), target[0]);
|
|
43
|
+
},
|
|
44
|
+
migrateToLatest: run(Effect.succeed(resolved)),
|
|
45
|
+
};
|
|
46
|
+
});
|
package/src/server/postgres.ts
CHANGED
|
@@ -1,38 +1,42 @@
|
|
|
1
|
-
import * as PgClient from "@effect/sql-pg/PgClient";
|
|
2
1
|
import { Context, Effect, Layer, type Redacted } from "effect";
|
|
3
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
3
|
import { migratePostgres, type PostgresMigrationOptions } from "@shivaedev/effect-sql/migrations.ts";
|
|
4
|
+
import { PostgresTestDatabase, type PostgresTestDatabaseService, type PostgresTestServices } from "#database.ts";
|
|
5
|
+
import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
|
|
5
6
|
import { connection, database } from "#internal/databases.ts";
|
|
6
|
-
import { PostgresTestDatabase, type PostgresTestDatabaseService, type PostgresTestServices } from "./database.ts";
|
|
7
7
|
|
|
8
8
|
export interface PostgresTestOptions<E = never, R = never, TFixtures = never> {
|
|
9
9
|
readonly fixtures?: Effect.Effect<unknown, E, TFixtures | PostgresTestServices>;
|
|
10
|
+
readonly maxConnections?: number;
|
|
10
11
|
readonly migrations: PostgresMigrationOptions<R>;
|
|
12
|
+
readonly prepare?: boolean;
|
|
11
13
|
readonly url: Redacted.Redacted<string>;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function makePostgresTestLayer<E = never, R = never, TFixtures = never>(options: PostgresTestOptions<E, R, TFixtures>) {
|
|
15
17
|
return Layer.effectContext(
|
|
16
18
|
Effect.gen(function* () {
|
|
17
|
-
const
|
|
18
|
-
const template = yield* database(
|
|
19
|
-
|
|
20
|
-
Effect.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
const maxConnections = options.maxConnections ?? 2;
|
|
20
|
+
const template = yield* database(options.url);
|
|
21
|
+
if (options.prepare !== false) {
|
|
22
|
+
yield* Effect.scoped(
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const services = yield* Layer.build(connection(template.url, maxConnections));
|
|
25
|
+
yield* migratePostgres(options.migrations).pipe(Effect.provideContext(services));
|
|
26
|
+
if (options.fixtures !== undefined) {
|
|
27
|
+
yield* options.fixtures.pipe(Effect.provideContext(services));
|
|
28
|
+
}
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const shared = yield* database(options.url, template.name);
|
|
33
|
+
const services = yield* Layer.build(connection(shared.url, maxConnections));
|
|
30
34
|
const isolated: PostgresTestDatabaseService["isolated"] = Effect.gen(function* () {
|
|
31
|
-
const clone = yield* database(
|
|
32
|
-
const context = yield* Layer.build(connection(clone.url));
|
|
35
|
+
const clone = yield* database(options.url, template.name);
|
|
36
|
+
const context = yield* Layer.build(connection(clone.url, maxConnections));
|
|
33
37
|
return Context.add(context, PostgresTestDatabase, { isolated, url: clone.url });
|
|
34
38
|
});
|
|
35
39
|
return Context.add(services, PostgresTestDatabase, { isolated, url: shared.url });
|
|
36
|
-
}),
|
|
40
|
+
}).pipe(Effect.mapError(connectionLimitHint)),
|
|
37
41
|
).pipe(Layer.provide(Reactivity.layer));
|
|
38
42
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
+
import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
|
|
2
3
|
import { PostgresTestDatabase } from "./database.ts";
|
|
3
4
|
|
|
4
5
|
export function withIsolatedDatabase<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|
@@ -8,5 +9,5 @@ export function withIsolatedDatabase<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|
|
8
9
|
const services = yield* database.isolated;
|
|
9
10
|
return yield* effect.pipe(Effect.provideContext(services));
|
|
10
11
|
}),
|
|
11
|
-
);
|
|
12
|
+
).pipe(Effect.mapError(connectionLimitHint));
|
|
12
13
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect, Exit } from "effect";
|
|
2
2
|
import type { SqlError } from "effect/sql/SqlError";
|
|
3
3
|
import { transact } from "@shivaedev/effect-sql/transact.ts";
|
|
4
|
+
import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
|
|
4
5
|
|
|
5
6
|
function isTestExit<A, E>(value: Exit.Exit<A, E> | SqlError): value is Exit.Exit<A, E> {
|
|
6
7
|
return Exit.isExit(value);
|
|
@@ -10,6 +11,7 @@ export function withTestTransaction<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|
|
10
11
|
return Effect.uninterruptibleMask((restore) =>
|
|
11
12
|
transact(Effect.flatMap(Effect.exit(Effect.scoped(restore(effect))), Effect.fail), { onSqlError: (error) => error }).pipe(
|
|
12
13
|
Effect.catchIf(isTestExit, (exit) => exit),
|
|
14
|
+
Effect.mapError(connectionLimitHint),
|
|
13
15
|
),
|
|
14
16
|
);
|
|
15
17
|
}
|