@shivaedev/sql 0.39.0 → 0.41.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 +16 -0
- package/README.md +91 -3
- package/dist/{testing/WorkerRelease.d.ts → WorkerReleaseClock.d.ts} +3 -3
- package/dist/WorkerReleaseClock.d.ts.map +1 -0
- package/dist/WorkerReleaseClock.js +4 -0
- package/dist/WorkerReleaseClock.js.map +1 -0
- package/dist/coordinateWorkerRelease.js +2 -2
- package/dist/coordinateWorkerRelease.js.map +1 -1
- package/dist/defineRepository.d.ts +8 -0
- package/dist/defineRepository.d.ts.map +1 -0
- package/dist/defineRepository.js +6 -0
- package/dist/defineRepository.js.map +1 -0
- package/dist/keysetPage.d.ts +5 -6
- package/dist/keysetPage.d.ts.map +1 -1
- package/dist/keysetPage.js +20 -11
- package/dist/keysetPage.js.map +1 -1
- package/dist/releaseWorkers.js +2 -2
- package/dist/releaseWorkers.js.map +1 -1
- package/package.json +7 -7
- package/src/{testing/WorkerRelease.ts → WorkerReleaseClock.ts} +3 -3
- package/src/coordinateWorkerRelease.ts +2 -2
- package/src/defineRepository.ts +9 -0
- package/src/keysetPage.ts +28 -20
- package/src/releaseWorkers.ts +2 -2
- package/dist/testing/WorkerRelease.d.ts.map +0 -1
- package/dist/testing/WorkerRelease.js +0 -4
- package/dist/testing/WorkerRelease.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.41.0 - 2026-11-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `defineRepository` exposes a repository and its domain operations through one native Effect service and Layer, shared by services that declare it in `requires`. Initialization retains its typed errors, dependencies and Layer-owned scope; repository operations retain caller transactions.
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- The worker scheduling port is `WorkerReleaseClock` from `@shivaedev/sql/WorkerReleaseClock.ts`. Replace imports and uses of `WorkerReleaseTesting`; use `hostTime` from test-kits for controlled story time.
|
|
12
|
+
|
|
13
|
+
## 0.40.0 - 2026-11-04
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `keysetPage` builds native statement fragments without requiring a `SqlClient` service; its read callback retains its own service requirements.
|
|
18
|
+
|
|
3
19
|
## 0.39.0 - 2026-11-04
|
|
4
20
|
|
|
5
21
|
### Added
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ const backend = Effect.gen(function* () {
|
|
|
69
69
|
}).pipe(Effect.scoped, Effect.provide(database));
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
The Layer owns the pool and waits for every socket to close during shutdown. A reserved connection sees committed writes from client transactions in the same database; it does not implicitly join a client's open transaction. `appDatabase` from `@shivaedev/server/appDatabase.ts` and database-backed `appHost` from `@shivaedev/server/
|
|
72
|
+
The Layer owns the pool and waits for every socket to close during shutdown. A reserved connection sees committed writes from client transactions in the same database; it does not implicitly join a client's open transaction. `appDatabase` from `@shivaedev/server/appDatabase.ts` and database-backed `appHost` from `@shivaedev/server/appDatabase.ts` compose this Layer and expose the pool to application services.
|
|
73
73
|
|
|
74
74
|
### SQL names: configure the client once
|
|
75
75
|
|
|
@@ -87,7 +87,7 @@ const database = PgClient.layer(
|
|
|
87
87
|
|
|
88
88
|
`clientOptions` enables Effect SQL's `transformQueryNames` with Effect's `String.camelToSnake` and `transformResultNames` with `String.snakeToCamel` by default. A model field `createdAt` writes the migration-created `created_at` column, and returned rows contain `createdAt`. Inserts, selects, filters, ordering, updates and raw SQL share the client's transforms; column aliases are unnecessary. Write literal migration SQL with snake_case columns. In tagged SQL, interpolated identifiers such as `sql("createdAt")` are transformed; literal SQL text stays as written.
|
|
89
89
|
|
|
90
|
-
To opt out for an existing database with quoted camelCase columns or models with snake_case fields, use `PgClient.layer(clientOptions(options, { snakeCase: false }))`. Existing native clients are unchanged until configured with this helper. Native `transformQueryNames` and `transformResultNames` supplied in `options` take precedence. For `appHost` from `@shivaedev/server/
|
|
90
|
+
To opt out for an existing database with quoted camelCase columns or models with snake_case fields, use `PgClient.layer(clientOptions(options, { snakeCase: false }))`. Existing native clients are unchanged until configured with this helper. Native `transformQueryNames` and `transformResultNames` supplied in `options` take precedence. For `appHost` from `@shivaedev/server/appDatabase.ts` or `appDatabase` from `@shivaedev/server/appDatabase.ts`, use `clientOptions` on their `config` object; the host forwards those native transforms to its client.
|
|
91
91
|
|
|
92
92
|
### Grouped values in flat columns
|
|
93
93
|
|
|
@@ -189,6 +189,93 @@ class Receipt extends Model.Class<Receipt>("Receipt")({
|
|
|
189
189
|
|
|
190
190
|
`jsonb` uses native `PgTypes.jsonb` parameters. The supplied schema encodes nested values for storage and decodes PostgreSQL's returned object through the repository, including projections. Here `amount` is a number in application code and a string inside the stored JSON. Invalid input or stored data fails with `SchemaError`. Use `jsonb(Schema.NullOr(Details))` for a nullable column: a null input binds SQL `NULL`. The application still owns the column's DDL.
|
|
191
191
|
|
|
192
|
+
### Share a table's repository and domain reads between services
|
|
193
|
+
|
|
194
|
+
Use `defineRepository` from `@shivaedev/sql/defineRepository.ts` to give the table's repository and its domain operations one native Effect service and Layer. Its initializer returns the whole public shape: values, repositories and functions are all allowed. Services declare that repository tag in `requires`, then read it from the environment. Keep repository construction out of each service's initializer.
|
|
195
|
+
|
|
196
|
+
For a pot with `id`, `amount` and `pending` integer fields, its repository module can own loading and replaying pending portions:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { Effect } from "effect";
|
|
200
|
+
import { defineRepository } from "@shivaedev/sql/defineRepository.ts";
|
|
201
|
+
import { makeRepository } from "@shivaedev/sql/repository.ts";
|
|
202
|
+
import { Pot } from "#pots/Pot.ts";
|
|
203
|
+
|
|
204
|
+
const initializePots = Effect.gen(function* () {
|
|
205
|
+
const rows = yield* makeRepository(Pot, {
|
|
206
|
+
idColumn: "id",
|
|
207
|
+
spanPrefix: "Pots",
|
|
208
|
+
tableName: "pots",
|
|
209
|
+
});
|
|
210
|
+
const load = Effect.fn("Pots.load")((id: number) => rows.findById(id));
|
|
211
|
+
const settle = Effect.fn("Pots.settle")(function* (id: number) {
|
|
212
|
+
const pot = yield* load(id);
|
|
213
|
+
return yield* rows.update({ ...pot, amount: pot.amount + pot.pending, pending: 0 });
|
|
214
|
+
});
|
|
215
|
+
return { rows, load, settle };
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
export const Pots = defineRepository({ id: "app/Pots", initialize: initializePots });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`Pots.layer` requires the initializer's services, including `SqlClient`. Its initialization errors stay in the Layer's error type, and resources acquired by initialization belong to the Layer's scope. The returned operations keep their own types and caller requirements; `defineRepository` does not bind their dependencies. Here `load` and `settle` close over `rows`, which captures the native SQL client.
|
|
222
|
+
|
|
223
|
+
Put service operations in their operation modules, reading the shared tag:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { Effect } from "effect";
|
|
227
|
+
import { Pots } from "#pots/Pots.ts";
|
|
228
|
+
|
|
229
|
+
export const loadPot = Effect.fn("Plates.loadPot")(function* (id: number) {
|
|
230
|
+
return yield* (yield* Pots).load(id);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export const settlePot = Effect.fn("PotLifecycle.settlePot")(function* (id: number) {
|
|
234
|
+
return yield* (yield* Pots).settle(id);
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Keep each operation in its own module. The two service assemblies import those operations:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { Effect, Layer } from "effect";
|
|
242
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
243
|
+
import { loadPot } from "#pots/loadPot.ts";
|
|
244
|
+
import { Pots } from "#pots/Pots.ts";
|
|
245
|
+
import { settlePot } from "#pots/settlePot.ts";
|
|
246
|
+
|
|
247
|
+
const Plates = defineService({
|
|
248
|
+
id: "app/Plates",
|
|
249
|
+
requires: [Pots],
|
|
250
|
+
initialize: Effect.void,
|
|
251
|
+
methods: () => ({ loadPot }),
|
|
252
|
+
});
|
|
253
|
+
const PotLifecycle = defineService({
|
|
254
|
+
id: "app/PotLifecycle",
|
|
255
|
+
requires: [Pots],
|
|
256
|
+
initialize: Effect.void,
|
|
257
|
+
methods: () => ({ settlePot }),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const features = Layer.merge(Plates.layer, PotLifecycle.layer).pipe(Layer.provide(Pots.layer));
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Provide the application's PostgreSQL Layer to `features` once. Both services receive the same repository instance within that Layer build. Reuse the same `Pots.layer` value so native Layer memoization shares construction; `Layer.fresh` and separate builds intentionally create separate lifetimes. The repository does not open another pool or a transaction. Wrap work spanning both services in `transact`; repository reads and writes keep the caller's transaction and savepoints, and a failure rolls back the work across both services. The application chooses concurrency and locking for a read followed by an update. Closing the Layer scope releases its repository initialization resources.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { transact } from "@shivaedev/sql/transact.ts";
|
|
267
|
+
|
|
268
|
+
const application = features.pipe(Layer.provideMerge(database));
|
|
269
|
+
const settleAndLoad = Effect.gen(function* () {
|
|
270
|
+
const lifecycle = yield* PotLifecycle;
|
|
271
|
+
const plates = yield* Plates;
|
|
272
|
+
yield* lifecycle.settlePot(1);
|
|
273
|
+
return yield* plates.loadPot(1);
|
|
274
|
+
}).pipe(transact({ onSqlError: (error) => error }), Effect.provide(application));
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`database` is the application's `postgresLayer`. `Layer.provideMerge` also keeps its SQL and Reactivity services available to the outer `transact`.
|
|
278
|
+
|
|
192
279
|
### 2. Per feature: write and query through the model
|
|
193
280
|
|
|
194
281
|
```ts
|
|
@@ -398,7 +485,7 @@ const rows = yield* paymentsBySequence([]);
|
|
|
398
485
|
|
|
399
486
|
### Keyset pages over native SQL
|
|
400
487
|
|
|
401
|
-
`keysetPage` builds seek and ordering fragments and asks your `read` callback for one extra row. It returns `{ rows, next }`, where `next` is an `Option` containing the last returned row's cursor only when another row exists. Your query keeps its predicates, joins and result schema:
|
|
488
|
+
`keysetPage` builds seek and ordering fragments and asks your `read` callback for one extra row. It returns `{ rows, next }`, where `next` is an `Option` containing the last returned row's cursor only when another row exists. Your query keeps its predicates, joins and result schema. The helper builds native statement fragments without reading `SqlClient`; its service requirements come from your `read` callback:
|
|
402
489
|
|
|
403
490
|
```ts
|
|
404
491
|
import { Effect, Schema } from "effect";
|
|
@@ -917,6 +1004,7 @@ Import the module that defines the name; there is no root entry.
|
|
|
917
1004
|
- `@shivaedev/sql/int8.ts`: `int8(field)` — PostgreSQL bigint storage codec using the supplied field schema.
|
|
918
1005
|
- `@shivaedev/sql/int4Array.ts`: `int4Array(field)` — PostgreSQL integer-array storage codec, including typed empty arrays.
|
|
919
1006
|
- `@shivaedev/sql/compositeRecordKey.ts`: `compositeRecordKey(model, { tableName, idColumn }, value)` — Encodes decoded composite key fields into the published record key.
|
|
1007
|
+
- `@shivaedev/sql/defineRepository.ts`: `defineRepository({ id, initialize })` — Native Context service and `.layer` for a shared repository shape; the initializer's dependencies, errors and scoped resources belong to its Layer.
|
|
920
1008
|
- `@shivaedev/sql/repository.ts`: `makeRepository(model, options)` — `tableName`, scalar or composite `idColumn`, `spanPrefix`, optional `versionColumn`, `conflictColumns` and `conflictWhere`, and `collection` required for bucket models; requires native `SqlClient`.
|
|
921
1009
|
- `@shivaedev/sql/repository.ts`: `OptimisticConcurrencyError` — Typed stale or missing-row failure for versioned writes, with `tableName`, `id`, `expectedVersion`, `reason: "missing" | "stale"`.
|
|
922
1010
|
- `@shivaedev/sql/tupleIn.ts`: `tupleIn(model, fields, tuples, types)` — Filter entry matching two or more fields together against a list of tuples, for `where` in reads and unversioned deletes.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type Clock, Context, type Effect, type Schema, type Scope } from "effect";
|
|
2
2
|
import type { SqlError } from "effect/sql/SqlError";
|
|
3
|
-
declare const
|
|
3
|
+
declare const WorkerReleaseClock_base: Context.ServiceClass<WorkerReleaseClock, "@shivaedev/sql/WorkerReleaseClock", {
|
|
4
4
|
readonly clock: Clock.Clock;
|
|
5
5
|
readonly register: (tick: {
|
|
6
6
|
readonly heartbeat: Effect.Effect<void, SqlError | Schema.SchemaError>;
|
|
7
7
|
readonly poll: Effect.Effect<void, SqlError | Schema.SchemaError>;
|
|
8
8
|
}) => Effect.Effect<void, never, Scope.Scope>;
|
|
9
9
|
}>;
|
|
10
|
-
export declare class
|
|
10
|
+
export declare class WorkerReleaseClock extends WorkerReleaseClock_base {
|
|
11
11
|
}
|
|
12
12
|
export {};
|
|
13
|
-
//# sourceMappingURL=
|
|
13
|
+
//# sourceMappingURL=WorkerReleaseClock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkerReleaseClock.d.ts","sourceRoot":"","sources":["../src/WorkerReleaseClock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AACnF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;;oBAKlC,KAAK,CAAC,KAAK;uBACR,CAAC,IAAI,EAAE;QACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;KAClE,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;;AAP/C,qBAAa,kBAAmB,SAAQ,uBASA;CAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkerReleaseClock.js","sourceRoot":"","sources":["../src/WorkerReleaseClock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,OAAO,EAAwC,MAAM,QAAQ,CAAC;AAGnF,MAAM,OAAO,kBAAmB,SAAQ,OAAO,CAAC,OAAO,EASpD,CAAC,mCAAmC,CAAC;CAAG"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Duration, Effect, Exit, Option, Predicate, Schedule, Scope } from "effect";
|
|
2
2
|
import { isSqlError } from "effect/sql/SqlError";
|
|
3
|
-
import {
|
|
3
|
+
import { WorkerReleaseClock } from "#WorkerReleaseClock.ts";
|
|
4
4
|
import { makeWorkerRelease } from "./makeWorkerRelease.js";
|
|
5
5
|
import { readPostgresRelease } from "./postgresRelease.js";
|
|
6
6
|
import { releaseWorkers } from "./releaseWorkers.js";
|
|
@@ -42,7 +42,7 @@ export const coordinateWorkerRelease = Effect.fn("EffectSql.coordinateWorkerRele
|
|
|
42
42
|
yield* loop("heartbeat", rows.heartbeat);
|
|
43
43
|
yield* tick("poll", poll).pipe(Scope.provide(scope));
|
|
44
44
|
yield* loop("poll", poll);
|
|
45
|
-
const testing = yield* Effect.serviceOption(
|
|
45
|
+
const testing = yield* Effect.serviceOption(WorkerReleaseClock);
|
|
46
46
|
if (Option.isSome(testing)) {
|
|
47
47
|
const context = yield* Effect.context();
|
|
48
48
|
yield* testing.value.register({ heartbeat: rows.heartbeat, poll: poll.pipe(Effect.provideContext(context)) }).pipe(Scope.provide(scope));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordinateWorkerRelease.js","sourceRoot":"","sources":["../src/coordinateWorkerRelease.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACpF,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"coordinateWorkerRelease.js","sourceRoot":"","sources":["../src/coordinateWorkerRelease.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Context, type Effect, Layer } from "effect";
|
|
2
|
+
export declare function defineRepository<const TIdentifier extends string, TShape, TError, TRequirements>(definition: {
|
|
3
|
+
readonly id: TIdentifier;
|
|
4
|
+
readonly initialize: Effect.Effect<TShape, TError, TRequirements>;
|
|
5
|
+
}): Context.Service<TIdentifier, TShape> & {
|
|
6
|
+
layer: Layer.Layer<TIdentifier, TError, Exclude<TRequirements, import("effect/Scope").Scope>>;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=defineRepository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defineRepository.d.ts","sourceRoot":"","sources":["../src/defineRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAErD,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,WAAW,SAAS,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE;IAC7G,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;CAClE;;EAGA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Context, Layer } from "effect";
|
|
2
|
+
export function defineRepository(definition) {
|
|
3
|
+
const repository = Context.Service(definition.id);
|
|
4
|
+
return Object.assign(repository, { layer: Layer.effect(repository)(definition.initialize) });
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=defineRepository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defineRepository.js","sourceRoot":"","sources":["../src/defineRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAe,KAAK,EAAE,MAAM,QAAQ,CAAC;AAErD,MAAM,UAAU,gBAAgB,CAAkE,UAGjG;IACA,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAsB,UAAU,CAAC,EAAE,CAAC,CAAC;IACvE,OAAO,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;AAC9F,CAAC"}
|
package/dist/keysetPage.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import
|
|
3
|
-
import type { Fragment } from "effect/sql/Statement";
|
|
2
|
+
import * as Statement from "effect/sql/Statement";
|
|
4
3
|
export interface KeysetOrder<TCursor> {
|
|
5
|
-
readonly column: string | Fragment;
|
|
4
|
+
readonly column: string | Statement.Fragment;
|
|
6
5
|
readonly direction: "asc" | "desc";
|
|
7
6
|
readonly value: (cursor: TCursor) => string | number | bigint | boolean;
|
|
8
7
|
}
|
|
9
8
|
export interface KeysetPageQuery {
|
|
10
9
|
readonly limit: number;
|
|
11
|
-
readonly orderBy: Fragment;
|
|
12
|
-
readonly seek: Fragment;
|
|
10
|
+
readonly orderBy: Statement.Fragment;
|
|
11
|
+
readonly seek: Statement.Fragment;
|
|
13
12
|
}
|
|
14
13
|
export interface KeysetPage<TRow, TCursor> {
|
|
15
14
|
readonly next: Option.Option<TCursor>;
|
|
@@ -22,5 +21,5 @@ export declare const keysetPage: <TRow, TCursor, E, R>(options: {
|
|
|
22
21
|
readonly tieBreaker: KeysetOrder<TCursor>;
|
|
23
22
|
readonly cursorFor: (row: TRow) => TCursor;
|
|
24
23
|
readonly read: (query: KeysetPageQuery) => Effect.Effect<readonly TRow[], E, R>;
|
|
25
|
-
}) => Effect.Effect<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R
|
|
24
|
+
}) => Effect.Effect<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R>;
|
|
26
25
|
//# sourceMappingURL=keysetPage.d.ts.map
|
package/dist/keysetPage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keysetPage.d.ts","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,
|
|
1
|
+
{"version":3,"file":"keysetPage.d.ts","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAElD,MAAM,WAAW,WAAW,CAAC,OAAO;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CACxE;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC;CAClC;AAED,MAAM,WAAW,UAAU,CAAC,IAAI,EAAE,OAAO;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,IAAI,EAAE,CAAC;CAC/B;AAID,eAAO,MAAM,UAAU,GAAgC,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;oBACzD,MAAM;qBACL,OAAO,GAAG,SAAS;sBAClB,SAAS,WAAW,CAAC,OAAO,CAAC,EAAE;yBAC5B,WAAW,CAAC,OAAO,CAAC;wBACrB,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO;mBAC3B,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;yEAiC9E,CAAC"}
|
package/dist/keysetPage.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import
|
|
2
|
+
import * as Statement from "effect/sql/Statement";
|
|
3
3
|
const PageSize = Schema.Number.check(Schema.isInt(), Schema.isGreaterThan(0), Schema.isLessThan(Number.MAX_SAFE_INTEGER));
|
|
4
4
|
export const keysetPage = Effect.fnUntraced(function* (options) {
|
|
5
5
|
const limit = yield* Schema.decodeUnknownEffect(PageSize)(options.limit);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const order = [...options.orderBy, options.tieBreaker].map((part) => ({
|
|
7
|
+
...part,
|
|
8
|
+
column: typeof part.column === "string" ? Statement.fragment([Statement.identifier(part.column)]) : part.column,
|
|
9
|
+
}));
|
|
9
10
|
const after = options.after;
|
|
10
11
|
const values = after === undefined ? undefined : order.map(({ value }) => value(after));
|
|
11
|
-
const
|
|
12
|
-
?
|
|
13
|
-
:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const conditions = values === undefined
|
|
13
|
+
? []
|
|
14
|
+
: order.map(({ column, direction }, index) => {
|
|
15
|
+
const prefix = order
|
|
16
|
+
.slice(0, index)
|
|
17
|
+
.map(({ column: previousColumn }, previous) => Statement.fragment([...previousColumn.segments, Statement.literal(" = "), Statement.parameter(values[previous])]));
|
|
18
|
+
const comparison = Statement.fragment([
|
|
19
|
+
...column.segments,
|
|
20
|
+
Statement.literal(direction === "asc" ? " > " : " < "),
|
|
21
|
+
Statement.parameter(values[index]),
|
|
22
|
+
]);
|
|
23
|
+
return Statement.and([...prefix, comparison]);
|
|
24
|
+
});
|
|
25
|
+
const seek = values === undefined ? Statement.fragment([Statement.literal("true")]) : Statement.or(conditions);
|
|
26
|
+
const orderBy = Statement.csv(order.map(({ column, direction }) => Statement.fragment([...column.segments, Statement.literal(direction === "asc" ? " asc" : " desc")])));
|
|
18
27
|
const found = yield* options.read({ limit: limit + 1, orderBy, seek });
|
|
19
28
|
const rows = found.slice(0, limit);
|
|
20
29
|
const last = rows.at(-1);
|
package/dist/keysetPage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keysetPage.js","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,
|
|
1
|
+
{"version":3,"file":"keysetPage.js","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"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"}
|
package/dist/releaseWorkers.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Clock, Effect, Option, Random, Schema } from "effect";
|
|
2
2
|
import { SqlClient } from "effect/sql";
|
|
3
|
-
import {
|
|
3
|
+
import { WorkerReleaseClock } from "#WorkerReleaseClock.ts";
|
|
4
4
|
export const releaseWorkers = Effect.fn("EffectSql.releaseWorkers")(function* (release, staleMillis, table = "effect_sql_migrations") {
|
|
5
5
|
const sql = yield* SqlClient.SqlClient;
|
|
6
6
|
const processId = `${yield* Random.nextInt}:${yield* Random.nextInt}`;
|
|
7
7
|
const workers = `${table}_workers`;
|
|
8
|
-
const testing = yield* Effect.serviceOption(
|
|
8
|
+
const testing = yield* Effect.serviceOption(WorkerReleaseClock);
|
|
9
9
|
const now = Option.isSome(testing)
|
|
10
10
|
? Clock.currentTimeMillis.pipe(Effect.provideService(Clock.Clock, testing.value.clock), Effect.map((millis) => sql `${new Date(millis).toISOString()}::timestamptz`))
|
|
11
11
|
: Effect.succeed(sql `clock_timestamp()`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseWorkers.js","sourceRoot":"","sources":["../src/releaseWorkers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"releaseWorkers.js","sourceRoot":"","sources":["../src/releaseWorkers.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Schema-derived repositories over native Effect SQL",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
"@effect/platform-node": "4.0.7",
|
|
51
51
|
"@effect/sql-pg": "4.0.7",
|
|
52
52
|
"@effect/sql-sqlite-node": "4.0.7",
|
|
53
|
-
"@shivaedev/contract": "0.
|
|
54
|
-
"@shivaedev/jobs": "0.
|
|
55
|
-
"@shivaedev/quality": "0.
|
|
56
|
-
"@shivaedev/test-kits": "0.
|
|
57
|
-
"@shivaedev/test-sql": "0.
|
|
58
|
-
"@shivaedev/test-story": "0.
|
|
53
|
+
"@shivaedev/contract": "0.37.0",
|
|
54
|
+
"@shivaedev/jobs": "0.37.1",
|
|
55
|
+
"@shivaedev/quality": "0.63.0",
|
|
56
|
+
"@shivaedev/test-kits": "0.6.0",
|
|
57
|
+
"@shivaedev/test-sql": "0.22.0",
|
|
58
|
+
"@shivaedev/test-story": "0.40.0",
|
|
59
59
|
"@types/node": "24.19.2",
|
|
60
60
|
"effect": "4.0.7",
|
|
61
61
|
"typescript": "7.0.2",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type Clock, Context, type Effect, type Schema, type Scope } from "effect";
|
|
2
2
|
import type { SqlError } from "effect/sql/SqlError";
|
|
3
3
|
|
|
4
|
-
export class
|
|
5
|
-
|
|
4
|
+
export class WorkerReleaseClock extends Context.Service<
|
|
5
|
+
WorkerReleaseClock,
|
|
6
6
|
{
|
|
7
7
|
readonly clock: Clock.Clock;
|
|
8
8
|
readonly register: (tick: {
|
|
@@ -10,4 +10,4 @@ export class WorkerReleaseTesting extends Context.Service<
|
|
|
10
10
|
readonly poll: Effect.Effect<void, SqlError | Schema.SchemaError>;
|
|
11
11
|
}) => Effect.Effect<void, never, Scope.Scope>;
|
|
12
12
|
}
|
|
13
|
-
>()("@shivaedev/sql/
|
|
13
|
+
>()("@shivaedev/sql/WorkerReleaseClock") {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Duration, Effect, Exit, Option, Predicate, Schedule, Scope } from "effect";
|
|
2
2
|
import { isSqlError } from "effect/sql/SqlError";
|
|
3
|
-
import {
|
|
3
|
+
import { WorkerReleaseClock } from "#WorkerReleaseClock.ts";
|
|
4
4
|
import { makeWorkerRelease } from "./makeWorkerRelease.ts";
|
|
5
5
|
import { readPostgresRelease } from "./postgresRelease.ts";
|
|
6
6
|
import { releaseWorkers } from "./releaseWorkers.ts";
|
|
@@ -66,7 +66,7 @@ export const coordinateWorkerRelease = Effect.fn("EffectSql.coordinateWorkerRele
|
|
|
66
66
|
yield* loop("heartbeat", rows.heartbeat);
|
|
67
67
|
yield* tick("poll", poll).pipe(Scope.provide(scope));
|
|
68
68
|
yield* loop("poll", poll);
|
|
69
|
-
const testing = yield* Effect.serviceOption(
|
|
69
|
+
const testing = yield* Effect.serviceOption(WorkerReleaseClock);
|
|
70
70
|
if (Option.isSome(testing)) {
|
|
71
71
|
const context = yield* Effect.context<Effect.Services<typeof poll>>();
|
|
72
72
|
yield* testing.value.register({ heartbeat: rows.heartbeat, poll: poll.pipe(Effect.provideContext(context)) }).pipe(Scope.provide(scope));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context, type Effect, Layer } from "effect";
|
|
2
|
+
|
|
3
|
+
export function defineRepository<const TIdentifier extends string, TShape, TError, TRequirements>(definition: {
|
|
4
|
+
readonly id: TIdentifier;
|
|
5
|
+
readonly initialize: Effect.Effect<TShape, TError, TRequirements>;
|
|
6
|
+
}) {
|
|
7
|
+
const repository = Context.Service<TIdentifier, TShape>(definition.id);
|
|
8
|
+
return Object.assign(repository, { layer: Layer.effect(repository)(definition.initialize) });
|
|
9
|
+
}
|
package/src/keysetPage.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import
|
|
3
|
-
import type { Fragment } from "effect/sql/Statement";
|
|
2
|
+
import * as Statement from "effect/sql/Statement";
|
|
4
3
|
|
|
5
4
|
export interface KeysetOrder<TCursor> {
|
|
6
|
-
readonly column: string | Fragment;
|
|
5
|
+
readonly column: string | Statement.Fragment;
|
|
7
6
|
readonly direction: "asc" | "desc";
|
|
8
7
|
readonly value: (cursor: TCursor) => string | number | bigint | boolean;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
export interface KeysetPageQuery {
|
|
12
11
|
readonly limit: number;
|
|
13
|
-
readonly orderBy: Fragment;
|
|
14
|
-
readonly seek: Fragment;
|
|
12
|
+
readonly orderBy: Statement.Fragment;
|
|
13
|
+
readonly seek: Statement.Fragment;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export interface KeysetPage<TRow, TCursor> {
|
|
@@ -28,25 +27,34 @@ export const keysetPage = Effect.fnUntraced(function* <TRow, TCursor, E, R>(opti
|
|
|
28
27
|
readonly tieBreaker: KeysetOrder<TCursor>;
|
|
29
28
|
readonly cursorFor: (row: TRow) => TCursor;
|
|
30
29
|
readonly read: (query: KeysetPageQuery) => Effect.Effect<readonly TRow[], E, R>;
|
|
31
|
-
}): Effect.fn.Return<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R
|
|
30
|
+
}): Effect.fn.Return<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R> {
|
|
32
31
|
const limit = yield* Schema.decodeUnknownEffect(PageSize)(options.limit);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const order = [...options.orderBy, options.tieBreaker].map((part) => ({
|
|
33
|
+
...part,
|
|
34
|
+
column: typeof part.column === "string" ? Statement.fragment([Statement.identifier(part.column)]) : part.column,
|
|
35
|
+
}));
|
|
36
36
|
const after = options.after;
|
|
37
37
|
const values = after === undefined ? undefined : order.map(({ value }) => value(after));
|
|
38
|
-
const
|
|
38
|
+
const conditions =
|
|
39
39
|
values === undefined
|
|
40
|
-
?
|
|
41
|
-
:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
? []
|
|
41
|
+
: order.map(({ column, direction }, index) => {
|
|
42
|
+
const prefix = order
|
|
43
|
+
.slice(0, index)
|
|
44
|
+
.map(({ column: previousColumn }, previous) =>
|
|
45
|
+
Statement.fragment([...previousColumn.segments, Statement.literal(" = "), Statement.parameter(values[previous])]),
|
|
46
|
+
);
|
|
47
|
+
const comparison = Statement.fragment([
|
|
48
|
+
...column.segments,
|
|
49
|
+
Statement.literal(direction === "asc" ? " > " : " < "),
|
|
50
|
+
Statement.parameter(values[index]),
|
|
51
|
+
]);
|
|
52
|
+
return Statement.and([...prefix, comparison]);
|
|
53
|
+
});
|
|
54
|
+
const seek = values === undefined ? Statement.fragment([Statement.literal("true")]) : Statement.or(conditions);
|
|
55
|
+
const orderBy = Statement.csv(
|
|
56
|
+
order.map(({ column, direction }) => Statement.fragment([...column.segments, Statement.literal(direction === "asc" ? " asc" : " desc")])),
|
|
57
|
+
);
|
|
50
58
|
const found = yield* options.read({ limit: limit + 1, orderBy, seek });
|
|
51
59
|
const rows = found.slice(0, limit);
|
|
52
60
|
const last = rows.at(-1);
|
package/src/releaseWorkers.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Clock, Effect, Option, Random, Schema } from "effect";
|
|
2
2
|
import { SqlClient } from "effect/sql";
|
|
3
|
-
import {
|
|
3
|
+
import { WorkerReleaseClock } from "#WorkerReleaseClock.ts";
|
|
4
4
|
|
|
5
5
|
export const releaseWorkers = Effect.fn("EffectSql.releaseWorkers")(function* (
|
|
6
6
|
release: string,
|
|
@@ -10,7 +10,7 @@ export const releaseWorkers = Effect.fn("EffectSql.releaseWorkers")(function* (
|
|
|
10
10
|
const sql = yield* SqlClient.SqlClient;
|
|
11
11
|
const processId = `${yield* Random.nextInt}:${yield* Random.nextInt}`;
|
|
12
12
|
const workers = `${table}_workers`;
|
|
13
|
-
const testing = yield* Effect.serviceOption(
|
|
13
|
+
const testing = yield* Effect.serviceOption(WorkerReleaseClock);
|
|
14
14
|
const now = Option.isSome(testing)
|
|
15
15
|
? Clock.currentTimeMillis.pipe(
|
|
16
16
|
Effect.provideService(Clock.Clock, testing.value.clock),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WorkerRelease.d.ts","sourceRoot":"","sources":["../../src/testing/WorkerRelease.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AACnF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;;oBAKlC,KAAK,CAAC,KAAK;uBACR,CAAC,IAAI,EAAE;QACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;KAClE,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;;AAP/C,qBAAa,oBAAqB,SAAQ,yBASQ;CAAG"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WorkerRelease.js","sourceRoot":"","sources":["../../src/testing/WorkerRelease.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,OAAO,EAAwC,MAAM,QAAQ,CAAC;AAGnF,MAAM,OAAO,oBAAqB,SAAQ,OAAO,CAAC,OAAO,EAStD,CAAC,6CAA6C,CAAC;CAAG"}
|