@shivaedev/effect-sql 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/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +72 -1
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +9 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +19 -0
- package/dist/migrations.js.map +1 -0
- package/dist/transact.d.ts +15 -0
- package/dist/transact.d.ts.map +1 -0
- package/dist/transact.js +35 -0
- package/dist/transact.js.map +1 -0
- package/package.json +53 -4
- package/src/index.ts +76 -0
- package/src/migrations.ts +32 -0
- package/src/transact.ts +61 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add native Effect model repositories with schema-derived equality filters,
|
|
8
|
+
selected result decoding, ordering and limits.
|
|
9
|
+
- Treat `undefined` filter values as unconstrained and fail with `SchemaError`
|
|
10
|
+
for field names outside the model.
|
|
11
|
+
- Add `transact` and `invalidateOnCommit`, built on `@shivaedev/effect-changes`:
|
|
12
|
+
native transactions with mapped SQL errors and Reactivity invalidation only
|
|
13
|
+
after the outermost commit.
|
|
14
|
+
- Keep one invalidation set per `SqlClient`: `invalidateOnCommit` marks the set
|
|
15
|
+
of its own `SqlClient`, a transaction on another database nested inside
|
|
16
|
+
`transact` announces its own commit, and a `transact` re-entered on a database
|
|
17
|
+
with an open outer transaction joins it as a savepoint.
|
|
18
|
+
- Die when `invalidateOnCommit` runs after its transaction finished instead of
|
|
19
|
+
dropping the keys, and when `transact` or `invalidateOnCommit` runs inside a
|
|
20
|
+
native transaction that `transact` did not begin.
|
|
21
|
+
- Log an invalidation that fails after `COMMIT`, for example a Reactivity
|
|
22
|
+
subscriber that throws, as an error and return the committed result, so
|
|
23
|
+
callers do not retry writes that already landed. The same applies to
|
|
24
|
+
`invalidateOnCommit` outside a transaction.
|
|
25
|
+
- Announce the keys when a caller is interrupted while `COMMIT` is in flight and
|
|
26
|
+
the database committed.
|
|
27
|
+
- Add `migratePostgres`: run Effect SQL migrations under a transaction-scoped
|
|
28
|
+
advisory lock with a required `lockTimeout`. An infinite `lockTimeout` becomes
|
|
29
|
+
PostgreSQL's `lock_timeout = 0` (no timeout).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShivaeDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
1
|
# @shivaedev/effect-sql
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Small repositories over Effect's SQL client and schema models. The model supplies database codecs, insert/update variants, filter values and selected result types. No separate database interface, result schema or query-builder runtime is required.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Effect, Schema } from "effect";
|
|
7
|
+
import { Model } from "effect/unstable/schema";
|
|
8
|
+
import { makeRepository } from "@shivaedev/effect-sql";
|
|
9
|
+
|
|
10
|
+
class Food extends Model.Class<Food>("Food")({
|
|
11
|
+
id: Model.Field({ select: Schema.Number, update: Schema.Number, json: Schema.Number }),
|
|
12
|
+
name: Schema.String,
|
|
13
|
+
calories: Schema.NumberFromString,
|
|
14
|
+
}) {}
|
|
15
|
+
|
|
16
|
+
const program = Effect.gen(function* () {
|
|
17
|
+
const foods = yield* makeRepository(Food, {
|
|
18
|
+
tableName: "food",
|
|
19
|
+
idColumn: "id",
|
|
20
|
+
spanPrefix: "Food",
|
|
21
|
+
});
|
|
22
|
+
yield* foods.insert({ name: "Apple", calories: 52 });
|
|
23
|
+
return yield* foods.findMany({
|
|
24
|
+
where: { calories: 52 },
|
|
25
|
+
select: ["name", "calories"],
|
|
26
|
+
orderBy: { field: "name", direction: "asc" },
|
|
27
|
+
limit: 20,
|
|
28
|
+
});
|
|
29
|
+
// Array<{ readonly name: string; readonly calories: number }>
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Provide an Effect `SqlClient` Layer to run the program. The sample's database column for calories is text; `NumberFromString` performs both directions of conversion. Schemas must match actual driver representations.
|
|
34
|
+
|
|
35
|
+
## Supported behavior
|
|
36
|
+
|
|
37
|
+
`insert`, `insertVoid`, `update`, `updateVoid`, `findById` and `delete` come directly from Effect's `SqlModel.makeRepository`. `findMany` adds conjunctions of equality filters, a nonempty tuple of selected fields, one sort field and a nonnegative integer limit. Omitted selection returns all declared fields. Returned lists contain decoded plain records; native CRUD returns the model class. Null filters compile to `IS NULL`. A filter key that is omitted or set to `undefined` leaves that field unconstrained. Values are bound parameters and identifiers use the native SQL identifier constructor.
|
|
38
|
+
|
|
39
|
+
Database-generated primary keys must remain in the update variant. The example uses `Model.Field` to express this; `Model.GeneratedByDb` alone omits the update key and therefore cannot be the native repository's id column. Native update expects the model's update shape, not an arbitrary partial patch. Native missing-row update behavior is retained: `update` defects when no row is returned, while `findById` fails with `NoSuchElementError`.
|
|
40
|
+
|
|
41
|
+
Field encoding, result decoding and SQL failures stay in Effect's error channel. A selected, filtered or sorted name that is not a model field fails with `SchemaError` before any SQL runs. Additional services required by field codecs stay in the effect environment. The repository uses its captured native client, so caller-owned `sql.withTransaction` scope is preserved. It does not introduce pools.
|
|
42
|
+
|
|
43
|
+
## Transactions and invalidation
|
|
44
|
+
|
|
45
|
+
`transact` wraps native `sql.withTransaction`, maps every `SqlError` through the caller's mapper, and announces changed Reactivity keys only after the outermost transaction commits:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { invalidateOnCommit, transact } from "@shivaedev/effect-sql";
|
|
49
|
+
|
|
50
|
+
const save = (meal: Meal) =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const saved = yield* meals.update(meal);
|
|
53
|
+
yield* invalidateOnCommit({ meals: [meal.id] });
|
|
54
|
+
return saved;
|
|
55
|
+
}).pipe(transact({ onSqlError: () => new StorageUnavailable() }));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- Keys marked with `invalidateOnCommit` go into a set owned by the innermost `transact` on the same `SqlClient`. After the outermost commit, the set is passed once to `Reactivity.invalidate`. Keys use native Reactivity's form: an array, or a record expanded like `{ meals: [1] }` to `"meals"` and `"meals:1"`.
|
|
59
|
+
- A typed failure, defect or interruption before `COMMIT` rolls back; neither it nor a failed commit invalidates anything. A caller interrupted while `COMMIT` is in flight still invalidates if the database committed, and then sees the interruption. A nested `transact` on the same `SqlClient` uses a savepoint; its keys join the outer set only if it succeeds. A `transact` on a different `SqlClient` is its own top-level transaction with its own set, announced when that database commits, whatever the enclosing transaction does.
|
|
60
|
+
- Sets are kept per `SqlClient`. `invalidateOnCommit` adds its keys to the set of the `SqlClient` in its own context, not to the innermost `transact`: keys follow the database that wrote the rows, so they are announced when that database commits and dropped when it rolls back. Re-entering `transact` on a database whose transaction is still open further out (B inside A inside B) joins that transaction as a savepoint.
|
|
61
|
+
- The set closes when its `transact` finishes. A fiber forked inside the body that calls `invalidateOnCommit` afterwards dies instead of having its keys dropped; mark keys before the body returns.
|
|
62
|
+
- Outside any transaction, `invalidateOnCommit` invalidates immediately. Inside a native `sql.withTransaction` that `transact` does not own, both `transact` and `invalidateOnCommit` die, because the outer commit cannot be observed. Start the outermost transaction with `transact`.
|
|
63
|
+
- An invalidation that fails after `COMMIT`, such as a subscriber that throws, is logged as an error and the committed result is returned; the rows are committed either way.
|
|
64
|
+
- `Reactivity` is an in-process service. It does not deliver changes to other processes or browser clients.
|
|
65
|
+
|
|
66
|
+
`transact` is a channel from [`@shivaedev/effect-changes`](../effect-changes) over `sql.withTransaction`; see [commit-bound changes](../../docs/framework/changes.md) for the general mechanism.
|
|
67
|
+
|
|
68
|
+
Native SQLite leaves the connection inside the transaction when `COMMIT` itself fails (for example, a deferred foreign key); a later `BEGIN` on that connection fails. This is native `withTransaction` behavior; `transact` only guarantees that no invalidation is announced.
|
|
69
|
+
|
|
70
|
+
## Boundaries
|
|
71
|
+
|
|
72
|
+
The initial implementation is tested with Effect 4.0.0-rc.112, real in-memory SQLite and PostgreSQL. PostgreSQL coverage includes generated identities, field codecs, nullable filters, selected fields, ordering, limits, transaction rollback, and `transact` across savepoints, two pools, a failing deferred `COMMIT` and an interruption during `COMMIT`. Set `PLATFORM_EFFECT_SQL_TEST_DATABASE_URL` to a disposable PostgreSQL database to run its integration tests; without it they are skipped. There is no SQL schema inference, migration generation, joins, relation loading, range predicates, tenant scoping or soft-delete policy. SQL table/column names must match the model fields and client transforms; migration authors remain responsible for that agreement. Use native Effect SQL for queries beyond this small API.
|
|
73
|
+
|
|
74
|
+
Further executable examples cover [PostgreSQL codecs](../../docs/framework/postgres-codecs.md), [constraint errors](../../docs/framework/constraint-errors.md) and [migration concurrency](../../docs/framework/postgres-migrations.md). These document the tested driver representations and their limits.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { Model } from "effect/unstable/schema";
|
|
3
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
4
|
+
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
5
|
+
export { migratePostgres, type PostgresMigrationOptions } from "./migrations.ts";
|
|
6
|
+
type Key<S extends Model.Any> = Extract<keyof S["fields"] & keyof Row<S>, string>;
|
|
7
|
+
type Row<S extends Model.Any> = Schema.Struct.Type<S["fields"]>;
|
|
8
|
+
export interface FindMany<S extends Model.Any, K extends Key<S>> {
|
|
9
|
+
readonly select?: readonly [K, ...K[]];
|
|
10
|
+
readonly where?: {
|
|
11
|
+
readonly [F in keyof Row<S>]?: Row<S>[F] | undefined;
|
|
12
|
+
};
|
|
13
|
+
readonly orderBy?: {
|
|
14
|
+
readonly field: Key<S>;
|
|
15
|
+
readonly direction: "asc" | "desc";
|
|
16
|
+
};
|
|
17
|
+
readonly limit?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const makeRepository: <S extends Model.Any, Id extends keyof S["Type"] & keyof S["update"]["Type"] & keyof S["fields"]>(model: S, options: {
|
|
20
|
+
readonly tableName: string;
|
|
21
|
+
readonly idColumn: Id;
|
|
22
|
+
readonly spanPrefix: string;
|
|
23
|
+
}) => Effect.Effect<{
|
|
24
|
+
insert: (insert: S["insert"]["Type"]) => Effect.Effect<S["Type"], Schema.SchemaError | SqlError, S["DecodingServices"] | S["insert"]["EncodingServices"]>;
|
|
25
|
+
insertVoid: (insert: S["insert"]["Type"]) => Effect.Effect<void, Schema.SchemaError | SqlError, S["insert"]["EncodingServices"]>;
|
|
26
|
+
update: (update: S["update"]["Type"]) => Effect.Effect<S["Type"], Schema.SchemaError | SqlError, S["DecodingServices"] | S["update"]["EncodingServices"]>;
|
|
27
|
+
updateVoid: (update: S["update"]["Type"]) => Effect.Effect<void, Schema.SchemaError | SqlError, S["update"]["EncodingServices"]>;
|
|
28
|
+
findById: (id: S["fields"][Id]["Type"]) => Effect.Effect<S["Type"], import("effect/Cause").NoSuchElementError | Schema.SchemaError | SqlError, S["DecodingServices"] | S["fields"][Id]["EncodingServices"]>;
|
|
29
|
+
delete: (id: S["fields"][Id]["Type"]) => Effect.Effect<void, Schema.SchemaError | SqlError, S["fields"][Id]["EncodingServices"]>;
|
|
30
|
+
findMany: <K extends Key<S> = Extract<keyof S["fields"] & keyof Schema.Struct.View<S["fields"], "Type", Schema.Struct.TypeOptionalKeys<S["fields"]>, Schema.Struct.TypeMutableKeys<S["fields"]>>, string>>(query?: FindMany<S, K>) => Effect.Effect<Array<Pick<Row<S>, K>>, SqlError | Schema.SchemaError, S["fields"][Key<S>]["EncodingServices"] | S["fields"][K]["DecodingServices"]>;
|
|
31
|
+
}, never, SqlClient.SqlClient>;
|
|
32
|
+
export { type InvalidationKeys, invalidateOnCommit, type TransactOptions, transact } from "./transact.ts";
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAe,MAAM,QAAQ,CAAC;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAY,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,KAAK,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEjF,KAAK,GAAG,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAClF,KAAK,GAAG,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;AAEhE,MAAM,WAAW,QAAQ,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS;KAAE,CAAC;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE;QAClB,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACvB,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;KACnC,CAAC;IACF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,EAC7H,OAAO,CAAC,EACR,SAAS;IACR,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;;;;;;;eAsBkB,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,wLACzB,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,KACpB,MAAM,CAAC,MAAM,CACf,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EACtB,QAAQ,GAAG,MAAM,CAAC,WAAW,EAC7B,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAC5E;8BAkBA,CAAC;AACJ,OAAO,EAAE,KAAK,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Effect, Schema, SchemaIssue } from "effect";
|
|
2
|
+
import { SqlClient, SqlModel } from "effect/unstable/sql";
|
|
3
|
+
export { migratePostgres } from "./migrations.js";
|
|
4
|
+
const Limit = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
|
|
5
|
+
export const makeRepository = (model, options) => Effect.gen(function* () {
|
|
6
|
+
const sql = yield* SqlClient.SqlClient;
|
|
7
|
+
const crud = yield* SqlModel.makeRepository(model, options);
|
|
8
|
+
const fieldAt = (key) => {
|
|
9
|
+
const field = Object.hasOwn(model.fields, key) ? model.fields[key] : undefined;
|
|
10
|
+
return field === undefined
|
|
11
|
+
? Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(model.ast, key))))
|
|
12
|
+
: Effect.succeed(field);
|
|
13
|
+
};
|
|
14
|
+
const predicate = (key, value) => Effect.map(Effect.flatMap(fieldAt(key), (field) => Schema.encodeEffect(field)(value)), (encoded) => (encoded === null ? sql `${sql(key)} is null` : sql `${sql(key)} = ${encoded}`));
|
|
15
|
+
const orderClause = (orderBy) => orderBy === undefined
|
|
16
|
+
? Effect.succeed(sql ``)
|
|
17
|
+
: Effect.as(fieldAt(orderBy.field), sql `order by ${sql(orderBy.field)} ${sql.literal(orderBy.direction === "desc" ? "desc" : "asc")}`);
|
|
18
|
+
const limitClause = (limit) => limit === undefined ? Effect.succeed(sql ``) : Effect.map(Schema.decodeUnknownEffect(Limit)(limit), (count) => sql `limit ${count}`);
|
|
19
|
+
// Object.fromEntries erases the selected key-to-schema correspondence, so the typed overload restates it.
|
|
20
|
+
function findMany(query = {}) {
|
|
21
|
+
return Effect.gen(function* () {
|
|
22
|
+
const keys = query.select ?? Object.keys(model.fields);
|
|
23
|
+
const fields = Object.fromEntries(yield* Effect.forEach(keys, (key) => Effect.map(fieldAt(key), (field) => [key, field])));
|
|
24
|
+
const predicates = yield* Effect.forEach(Object.entries(query.where ?? {}).filter(([, value]) => value !== undefined), ([key, value]) => predicate(key, value));
|
|
25
|
+
const order = yield* orderClause(query.orderBy);
|
|
26
|
+
const limit = yield* limitClause(query.limit);
|
|
27
|
+
const rows = yield* sql `select ${sql.csv(keys.map((key) => sql `${sql(key)}`))} from ${sql(options.tableName)} where ${sql.and(predicates)} ${order} ${limit}`;
|
|
28
|
+
return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(fields)))(rows);
|
|
29
|
+
}).pipe(Effect.withSpan(`${options.spanPrefix}.findMany`));
|
|
30
|
+
}
|
|
31
|
+
return { ...crud, findMany };
|
|
32
|
+
});
|
|
33
|
+
export { invalidateOnCommit, transact } from "./transact.js";
|
|
34
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Duration, Effect } from "effect";
|
|
2
|
+
import { Migrator, SqlClient } from "effect/unstable/sql";
|
|
3
|
+
export interface PostgresMigrationOptions<R> {
|
|
4
|
+
readonly loader: Migrator.Loader<R>;
|
|
5
|
+
readonly table?: string;
|
|
6
|
+
readonly lockTimeout: Duration.Input;
|
|
7
|
+
}
|
|
8
|
+
export declare const migratePostgres: <R>(args_0: PostgresMigrationOptions<R>) => Effect.Effect<readonly (readonly [id: number, name: string])[], Migrator.MigrationError | import("effect/unstable/sql/SqlError").SqlError, R | SqlClient.SqlClient>;
|
|
9
|
+
//# sourceMappingURL=migrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,WAAW,wBAAwB,CAAC,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,KAAK,CAAC;CACrC;AAID,eAAO,MAAM,eAAe,GAAqD,CAAC,6MAoBhF,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Duration, Effect } from "effect";
|
|
2
|
+
import { Migrator, SqlClient } from "effect/unstable/sql";
|
|
3
|
+
const migrate = Migrator.make({});
|
|
4
|
+
export const migratePostgres = Effect.fn("EffectSql.migratePostgres")(function* ({ loader, table = "effect_sql_migrations", lockTimeout, }) {
|
|
5
|
+
const sql = yield* SqlClient.SqlClient;
|
|
6
|
+
const millis = Duration.toMillis(lockTimeout);
|
|
7
|
+
const timeout = millis === Number.POSITIVE_INFINITY ? "0" : `${Math.max(1, Math.ceil(millis))}ms`;
|
|
8
|
+
return yield* sql.withTransaction(Effect.gen(function* () {
|
|
9
|
+
yield* sql `select set_config('lock_timeout', ${timeout}, true)`;
|
|
10
|
+
yield* sql `select pg_advisory_xact_lock(hashtextextended(${`effect_sql_migrations:${table}`}, 0))`;
|
|
11
|
+
yield* sql `create table if not exists ${sql(table)} (
|
|
12
|
+
migration_id integer primary key,
|
|
13
|
+
created_at timestamp with time zone not null default now(),
|
|
14
|
+
name text not null
|
|
15
|
+
)`;
|
|
16
|
+
return yield* migrate({ table, loader });
|
|
17
|
+
}));
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAQ1D,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAElC,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,EAAE,CAAC,2BAA2B,CAAC,CAAC,QAAQ,CAAC,EAAK,EACnF,MAAM,EACN,KAAK,GAAG,uBAAuB,EAC/B,WAAW,GACkB;IAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;IACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,MAAM,KAAK,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC;IAClG,OAAO,KAAK,CAAC,CAAC,GAAG,CAAC,eAAe,CAChC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,KAAK,CAAC,CAAC,GAAG,CAAA,qCAAqC,OAAO,SAAS,CAAC;QAChE,KAAK,CAAC,CAAC,GAAG,CAAA,iDAAiD,yBAAyB,KAAK,EAAE,OAAO,CAAC;QACnG,KAAK,CAAC,CAAC,GAAG,CAAA,8BAA8B,GAAG,CAAC,KAAK,CAAC;;;;EAInD,CAAC;QACA,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1C,CAAC,CAAC,CACF,CAAC;AACH,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { ReadonlyRecord } from "effect/Record";
|
|
3
|
+
import * as Reactivity from "effect/unstable/reactivity/Reactivity";
|
|
4
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
5
|
+
import { type SqlError } from "effect/unstable/sql/SqlError";
|
|
6
|
+
export type InvalidationKeys = ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>>;
|
|
7
|
+
export declare const invalidateOnCommit: (keys: InvalidationKeys) => Effect.Effect<void, never, Reactivity.Reactivity | SqlClient.SqlClient>;
|
|
8
|
+
export interface TransactOptions<E2> {
|
|
9
|
+
readonly onSqlError: (error: SqlError) => E2;
|
|
10
|
+
}
|
|
11
|
+
export declare const transact: {
|
|
12
|
+
<E2>(options: TransactOptions<E2>): <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, Exclude<E, SqlError> | E2, R | SqlClient.SqlClient | Reactivity.Reactivity>;
|
|
13
|
+
<A, E, R, E2>(effect: Effect.Effect<A, E, R>, options: TransactOptions<E2>): Effect.Effect<A, Exclude<E, SqlError> | E2, R | SqlClient.SqlClient | Reactivity.Reactivity>;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=transact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transact.d.ts","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,UAAU,MAAM,uCAAuC,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAEzE,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;AA6BvG,eAAO,MAAM,kBAAkB,qGAE7B,CAAC;AAEH,MAAM,WAAW,eAAe,CAAC,EAAE;IAClC,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,EAAE,CAAC;CAC7C;AASD,eAAO,MAAM,QAAQ,EAAE;IACtB,CAAC,EAAE,EACF,OAAO,EAAE,eAAe,CAAC,EAAE,CAAC,GAC1B,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IAC7I,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,OAAO,EAAE,eAAe,CAAC,EAAE,CAAC,GAC1B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;CACjF,CAAC"}
|
package/dist/transact.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { makeChannel } from "@shivaedev/effect-changes";
|
|
2
|
+
import { Effect, Hash } from "effect";
|
|
3
|
+
import { dual } from "effect/Function";
|
|
4
|
+
import * as Reactivity from "effect/unstable/reactivity/Reactivity";
|
|
5
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
6
|
+
import { isSqlError } from "effect/unstable/sql/SqlError";
|
|
7
|
+
const hashOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean"
|
|
8
|
+
? String(value)
|
|
9
|
+
: Hash.hash(value);
|
|
10
|
+
const isList = (keys) => Array.isArray(keys);
|
|
11
|
+
const flatten = (keys) => isList(keys) ? keys : Object.entries(keys).flatMap(([name, ids]) => [name, ...ids.map((id) => `${name}:${hashOf(id)}`)]);
|
|
12
|
+
const nativeTransactionWithoutTransact = Effect.fn("Transact.guard")(function* () {
|
|
13
|
+
const sql = yield* SqlClient.SqlClient;
|
|
14
|
+
const native = yield* Effect.serviceOption(sql.transactionService);
|
|
15
|
+
if (native._tag === "Some") {
|
|
16
|
+
return yield* Effect.die(new Error("transact cannot announce changes after a transaction it does not own; begin the outer transaction with transact"));
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const invalidations = makeChannel({
|
|
20
|
+
name: "@shivaedev/effect-sql/transact",
|
|
21
|
+
owner: Effect.map(SqlClient.SqlClient, (sql) => sql.transactionService),
|
|
22
|
+
publish: (keys) => Reactivity.invalidate(keys),
|
|
23
|
+
unowned: nativeTransactionWithoutTransact(),
|
|
24
|
+
});
|
|
25
|
+
export const invalidateOnCommit = Effect.fn("Transact.invalidateOnCommit")(function* (keys) {
|
|
26
|
+
yield* invalidations.record(flatten(keys));
|
|
27
|
+
});
|
|
28
|
+
const run = Effect.fn("Transact.transact")(function* (effect, options) {
|
|
29
|
+
const sql = yield* SqlClient.SqlClient;
|
|
30
|
+
return yield* invalidations
|
|
31
|
+
.within(sql.withTransaction)(effect)
|
|
32
|
+
.pipe(Effect.catchIf(isSqlError, (error) => Effect.fail(options.onSqlError(error))));
|
|
33
|
+
});
|
|
34
|
+
export const transact = dual(2, run);
|
|
35
|
+
//# sourceMappingURL=transact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transact.js","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-sql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Schema-derived repositories over native Effect SQL",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/ShivaeDev/platform.git",
|
|
9
10
|
"directory": "packages/effect-sql"
|
|
10
11
|
},
|
|
12
|
+
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-sql#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ShivaeDev/platform/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=24"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"source": "./src/index.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
11
35
|
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@shivaedev/effect-changes": "0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"effect": "4.0.0-rc.112"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "24.10.1",
|
|
47
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
48
|
+
"effect": "4.0.0-rc.112",
|
|
49
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
50
|
+
"vitest": "4.1.9",
|
|
51
|
+
"@effect/sql-sqlite-node": "4.0.0-rc.112",
|
|
52
|
+
"@effect/sql-pg": "4.0.0-rc.112"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
|
|
56
|
+
"check": "biome check .",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:package": "node scripts/test-package.mjs",
|
|
59
|
+
"typecheck": "tsc --noEmit",
|
|
60
|
+
"typecheck:compat": "tsc6 --noEmit",
|
|
61
|
+
"ready": "pnpm check && pnpm typecheck && pnpm typecheck:compat && pnpm test && pnpm build && pnpm test:package"
|
|
13
62
|
}
|
|
14
|
-
}
|
|
63
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Effect, Schema, SchemaIssue } from "effect";
|
|
2
|
+
import type { Model } from "effect/unstable/schema";
|
|
3
|
+
import { SqlClient, SqlModel } from "effect/unstable/sql";
|
|
4
|
+
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
5
|
+
|
|
6
|
+
export { migratePostgres, type PostgresMigrationOptions } from "./migrations.ts";
|
|
7
|
+
|
|
8
|
+
type Key<S extends Model.Any> = Extract<keyof S["fields"] & keyof Row<S>, string>;
|
|
9
|
+
type Row<S extends Model.Any> = Schema.Struct.Type<S["fields"]>;
|
|
10
|
+
|
|
11
|
+
export interface FindMany<S extends Model.Any, K extends Key<S>> {
|
|
12
|
+
readonly select?: readonly [K, ...K[]];
|
|
13
|
+
readonly where?: { readonly [F in keyof Row<S>]?: Row<S>[F] | undefined };
|
|
14
|
+
readonly orderBy?: {
|
|
15
|
+
readonly field: Key<S>;
|
|
16
|
+
readonly direction: "asc" | "desc";
|
|
17
|
+
};
|
|
18
|
+
readonly limit?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Limit = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
|
|
22
|
+
|
|
23
|
+
export const makeRepository = <S extends Model.Any, Id extends keyof S["Type"] & keyof S["update"]["Type"] & keyof S["fields"]>(
|
|
24
|
+
model: S,
|
|
25
|
+
options: {
|
|
26
|
+
readonly tableName: string;
|
|
27
|
+
readonly idColumn: Id;
|
|
28
|
+
readonly spanPrefix: string;
|
|
29
|
+
},
|
|
30
|
+
) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
const sql = yield* SqlClient.SqlClient;
|
|
33
|
+
const crud = yield* SqlModel.makeRepository(model, options);
|
|
34
|
+
const fieldAt = (key: string) => {
|
|
35
|
+
const field = Object.hasOwn(model.fields, key) ? model.fields[key] : undefined;
|
|
36
|
+
return field === undefined
|
|
37
|
+
? Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(model.ast, key))))
|
|
38
|
+
: Effect.succeed(field);
|
|
39
|
+
};
|
|
40
|
+
const predicate = (key: string, value: unknown) =>
|
|
41
|
+
Effect.map(
|
|
42
|
+
Effect.flatMap(fieldAt(key), (field) => Schema.encodeEffect(field)(value)),
|
|
43
|
+
(encoded) => (encoded === null ? sql`${sql(key)} is null` : sql`${sql(key)} = ${encoded}`),
|
|
44
|
+
);
|
|
45
|
+
const orderClause = (orderBy: FindMany<S, Key<S>>["orderBy"]) =>
|
|
46
|
+
orderBy === undefined
|
|
47
|
+
? Effect.succeed(sql``)
|
|
48
|
+
: Effect.as(fieldAt(orderBy.field), sql`order by ${sql(orderBy.field)} ${sql.literal(orderBy.direction === "desc" ? "desc" : "asc")}`);
|
|
49
|
+
const limitClause = (limit: number | undefined) =>
|
|
50
|
+
limit === undefined ? Effect.succeed(sql``) : Effect.map(Schema.decodeUnknownEffect(Limit)(limit), (count) => sql`limit ${count}`);
|
|
51
|
+
function findMany<K extends Key<S> = Key<S>>(
|
|
52
|
+
query?: FindMany<S, K>,
|
|
53
|
+
): Effect.Effect<
|
|
54
|
+
Array<Pick<Row<S>, K>>,
|
|
55
|
+
SqlError | Schema.SchemaError,
|
|
56
|
+
S["fields"][Key<S>]["EncodingServices"] | S["fields"][K]["DecodingServices"]
|
|
57
|
+
>;
|
|
58
|
+
// Object.fromEntries erases the selected key-to-schema correspondence, so the typed overload restates it.
|
|
59
|
+
function findMany(query: FindMany<S, Key<S>> = {}): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> {
|
|
60
|
+
return Effect.gen(function* () {
|
|
61
|
+
const keys = query.select ?? Object.keys(model.fields);
|
|
62
|
+
const fields = Object.fromEntries(yield* Effect.forEach(keys, (key) => Effect.map(fieldAt(key), (field) => [key, field] as const)));
|
|
63
|
+
const predicates = yield* Effect.forEach(
|
|
64
|
+
Object.entries(query.where ?? {}).filter(([, value]) => value !== undefined),
|
|
65
|
+
([key, value]) => predicate(key, value),
|
|
66
|
+
);
|
|
67
|
+
const order = yield* orderClause(query.orderBy);
|
|
68
|
+
const limit = yield* limitClause(query.limit);
|
|
69
|
+
const rows =
|
|
70
|
+
yield* sql`select ${sql.csv(keys.map((key) => sql`${sql(key)}`))} from ${sql(options.tableName)} where ${sql.and(predicates)} ${order} ${limit}`;
|
|
71
|
+
return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(fields)))(rows);
|
|
72
|
+
}).pipe(Effect.withSpan(`${options.spanPrefix}.findMany`));
|
|
73
|
+
}
|
|
74
|
+
return { ...crud, findMany };
|
|
75
|
+
});
|
|
76
|
+
export { type InvalidationKeys, invalidateOnCommit, type TransactOptions, transact } from "./transact.ts";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Duration, Effect } from "effect";
|
|
2
|
+
import { Migrator, SqlClient } from "effect/unstable/sql";
|
|
3
|
+
|
|
4
|
+
export interface PostgresMigrationOptions<R> {
|
|
5
|
+
readonly loader: Migrator.Loader<R>;
|
|
6
|
+
readonly table?: string;
|
|
7
|
+
readonly lockTimeout: Duration.Input;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const migrate = Migrator.make({});
|
|
11
|
+
|
|
12
|
+
export const migratePostgres = Effect.fn("EffectSql.migratePostgres")(function* <R>({
|
|
13
|
+
loader,
|
|
14
|
+
table = "effect_sql_migrations",
|
|
15
|
+
lockTimeout,
|
|
16
|
+
}: PostgresMigrationOptions<R>) {
|
|
17
|
+
const sql = yield* SqlClient.SqlClient;
|
|
18
|
+
const millis = Duration.toMillis(lockTimeout);
|
|
19
|
+
const timeout = millis === Number.POSITIVE_INFINITY ? "0" : `${Math.max(1, Math.ceil(millis))}ms`;
|
|
20
|
+
return yield* sql.withTransaction(
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
yield* sql`select set_config('lock_timeout', ${timeout}, true)`;
|
|
23
|
+
yield* sql`select pg_advisory_xact_lock(hashtextextended(${`effect_sql_migrations:${table}`}, 0))`;
|
|
24
|
+
yield* sql`create table if not exists ${sql(table)} (
|
|
25
|
+
migration_id integer primary key,
|
|
26
|
+
created_at timestamp with time zone not null default now(),
|
|
27
|
+
name text not null
|
|
28
|
+
)`;
|
|
29
|
+
return yield* migrate({ table, loader });
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
});
|
package/src/transact.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { makeChannel } from "@shivaedev/effect-changes";
|
|
2
|
+
import { Effect, Hash } from "effect";
|
|
3
|
+
import { dual } from "effect/Function";
|
|
4
|
+
import type { ReadonlyRecord } from "effect/Record";
|
|
5
|
+
import * as Reactivity from "effect/unstable/reactivity/Reactivity";
|
|
6
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
7
|
+
import { isSqlError, type SqlError } from "effect/unstable/sql/SqlError";
|
|
8
|
+
|
|
9
|
+
export type InvalidationKeys = ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>>;
|
|
10
|
+
|
|
11
|
+
const hashOf = (value: unknown): unknown =>
|
|
12
|
+
typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean"
|
|
13
|
+
? String(value)
|
|
14
|
+
: Hash.hash(value);
|
|
15
|
+
|
|
16
|
+
const isList = (keys: InvalidationKeys): keys is ReadonlyArray<unknown> => Array.isArray(keys);
|
|
17
|
+
|
|
18
|
+
const flatten = (keys: InvalidationKeys): ReadonlyArray<unknown> =>
|
|
19
|
+
isList(keys) ? keys : Object.entries(keys).flatMap(([name, ids]) => [name, ...ids.map((id) => `${name}:${hashOf(id)}`)]);
|
|
20
|
+
|
|
21
|
+
const nativeTransactionWithoutTransact = Effect.fn("Transact.guard")(function* () {
|
|
22
|
+
const sql = yield* SqlClient.SqlClient;
|
|
23
|
+
const native = yield* Effect.serviceOption(sql.transactionService);
|
|
24
|
+
if (native._tag === "Some") {
|
|
25
|
+
return yield* Effect.die(
|
|
26
|
+
new Error("transact cannot announce changes after a transaction it does not own; begin the outer transaction with transact"),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const invalidations = makeChannel<unknown, SqlClient.SqlClient | Reactivity.Reactivity>({
|
|
32
|
+
name: "@shivaedev/effect-sql/transact",
|
|
33
|
+
owner: Effect.map(SqlClient.SqlClient, (sql) => sql.transactionService),
|
|
34
|
+
publish: (keys) => Reactivity.invalidate(keys),
|
|
35
|
+
unowned: nativeTransactionWithoutTransact(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const invalidateOnCommit = Effect.fn("Transact.invalidateOnCommit")(function* (keys: InvalidationKeys) {
|
|
39
|
+
yield* invalidations.record(flatten(keys));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export interface TransactOptions<E2> {
|
|
43
|
+
readonly onSqlError: (error: SqlError) => E2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const run = Effect.fn("Transact.transact")(function* <A, E, R, E2>(effect: Effect.Effect<A, E, R>, options: TransactOptions<E2>) {
|
|
47
|
+
const sql = yield* SqlClient.SqlClient;
|
|
48
|
+
return yield* invalidations
|
|
49
|
+
.within(sql.withTransaction)(effect)
|
|
50
|
+
.pipe(Effect.catchIf(isSqlError, (error) => Effect.fail(options.onSqlError(error))));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const transact: {
|
|
54
|
+
<E2>(
|
|
55
|
+
options: TransactOptions<E2>,
|
|
56
|
+
): <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, Exclude<E, SqlError> | E2, R | SqlClient.SqlClient | Reactivity.Reactivity>;
|
|
57
|
+
<A, E, R, E2>(
|
|
58
|
+
effect: Effect.Effect<A, E, R>,
|
|
59
|
+
options: TransactOptions<E2>,
|
|
60
|
+
): Effect.Effect<A, Exclude<E, SqlError> | E2, R | SqlClient.SqlClient | Reactivity.Reactivity>;
|
|
61
|
+
} = dual(2, run);
|