@shivaedev/sql 0.22.0 → 0.23.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.23.0 - 2026-10-27
4
+
5
+ ### Added
6
+
7
+ - SQL stories can declare domain read keys that cover written tables with `withChangeCoverage(body, { covers })` and assert committed change keys with `expectPublished(keys)`.
8
+ - Contract stories can observe a viewer's live stream with `watchHints` and use `heard(viewer, keys)` to await allowed hints or report denied keys as unheard on the story clock.
9
+
3
10
  ## 0.22.0 - 2026-10-27
4
11
 
5
12
  ### Breaking
package/README.md CHANGED
@@ -612,4 +612,18 @@ JSON and JSONB columns are not supported. A repository write of a plain object t
612
612
 
613
613
  The package's PostgreSQL tests use a disposable database through `PLATFORM_SQL_TEST_DATABASE_URL`. Without the URL they skip locally; under `CI`, collection fails at the shared gate.
614
614
 
615
- The `withChangeCoverage(body)` story verb from `testing.ts` checks PostgreSQL transaction write counters before commit and fails with `UnannouncedWrites` for tables without a recorded table key. See [automatic change delivery](https://github.com/ShivaeDev/platform/blob/main/docs/framework/changes.md) for host defaults and coverage limits.
615
+ The sql story kit exposes `withChangeCoverage` and `expectPublished`, also exported from `@shivaedev/sql/testing.ts`. `withChangeCoverage(body, { covers })` checks PostgreSQL transaction write counters before commit. Each written table must have its table key or at least one declared covering key recorded; otherwise `UnannouncedWrites` fails the transaction. Declare a module's domain read keys by table, including for raw SQL writes:
616
+
617
+ ```ts
618
+ const coverage = { covers: { orders: ["dashboard:orders"] } };
619
+
620
+ yield* withChangeCoverage(
621
+ Effect.gen(function* () {
622
+ yield* sql`update orders set status = 'closed' where id = ${id}`;
623
+ yield* invalidateOnCommit(["dashboard:orders"]);
624
+ }),
625
+ coverage,
626
+ ).pipe(expectPublished(["dashboard:orders"]));
627
+ ```
628
+
629
+ `body.pipe(expectPublished(keys))` compares the distinct published keys, regardless of order, and preserves the body's result. Put it outside the committing transaction. It observes the existing change channel and leaves delivery intact; recorded keys from rolled-back writes do not count. A mismatch fails with `PublishedKeysMismatch`, whose `expected`, `actual` and message show both key sets. The assertion happens after the body, so a mismatch does not roll back a committed write. See [automatic change delivery](https://github.com/ShivaeDev/platform/blob/main/docs/framework/changes.md) for host defaults and coverage limits.
package/dist/testing.d.ts CHANGED
@@ -7,6 +7,19 @@ export declare class UnannouncedWrites extends UnannouncedWrites_base<{
7
7
  readonly tables: readonly string[];
8
8
  }> {
9
9
  }
10
- export declare const withChangeCoverage: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, import("effect/sql/SqlError").SqlError | UnannouncedWrites | Exclude<E, import("effect/sql/SqlError").SqlError>, import("effect/reactivity/Reactivity").Reactivity | SqlClient.SqlClient | Exclude<R, never>>;
10
+ export interface ChangeCoverage {
11
+ readonly covers: Readonly<Record<string, readonly unknown[]>>;
12
+ }
13
+ export declare const withChangeCoverage: <A, E, R>(body: Effect.Effect<A, E, R>, options?: ChangeCoverage | undefined) => Effect.Effect<A, import("effect/sql/SqlError").SqlError | UnannouncedWrites | Exclude<E, import("effect/sql/SqlError").SqlError>, import("effect/reactivity/Reactivity").Reactivity | SqlClient.SqlClient | Exclude<R, never>>;
14
+ declare const PublishedKeysMismatch_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
15
+ readonly _tag: "PublishedKeysMismatch";
16
+ } & Readonly<A>;
17
+ export declare class PublishedKeysMismatch extends PublishedKeysMismatch_base<{
18
+ readonly expected: readonly unknown[];
19
+ readonly actual: readonly unknown[];
20
+ }> {
21
+ get message(): string;
22
+ }
23
+ export declare function expectPublished(keys: readonly unknown[]): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | PublishedKeysMismatch, Exclude<R, never>>;
11
24
  export {};
12
25
  //# sourceMappingURL=testing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;;;;AAGvC,qBAAa,iBAAkB,SAAQ,uBAAsC;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;CAAG;AAQvH,eAAO,MAAM,kBAAkB,GAAsD,CAAC,EAAE,CAAC,EAAE,CAAC,iQAyB1F,CAAC"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;;;;AAGvC,qBAAa,iBAAkB,SAAQ,uBAAsC;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;CAAG;AAEvH,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC,CAAC;CAC9D;AAQD,eAAO,MAAM,kBAAkB,GAAsD,CAAC,EAAE,CAAC,EAAE,CAAC,uSA8B1F,CAAC;;;;AAEH,qBAAa,qBAAsB,SAAQ,2BAA0C;IACpF,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CACpC,CAAC;IACD,IAAa,OAAO,WAEnB;CACD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,IACA,CAAC,EAAE,CAAC,EAAE,CAAC,kGAmB9D"}
package/dist/testing.js CHANGED
@@ -3,7 +3,7 @@ import { SqlClient } from "effect/sql";
3
3
  import { ChangeObserver, transact } from "./transact.js";
4
4
  export class UnannouncedWrites extends Data.TaggedError("UnannouncedWrites") {
5
5
  }
6
- export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(function* (body) {
6
+ export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(function* (body, options) {
7
7
  const observer = yield* ChangeObserver;
8
8
  const sql = yield* SqlClient.SqlClient;
9
9
  const named = new Set();
@@ -11,7 +11,7 @@ export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(functi
11
11
  const checked = Effect.gen(function* () {
12
12
  const before = new Map((yield* written).map((row) => [row.relid, row.writes]));
13
13
  const result = yield* body;
14
- const missing = (yield* written).filter((row) => row.writes > (before.get(row.relid) ?? 0) && !named.has(row.relname));
14
+ const missing = (yield* written).filter((row) => row.writes > (before.get(row.relid) ?? 0) && !named.has(row.relname) && !options?.covers[row.relname]?.some((key) => named.has(key)));
15
15
  if (missing.length > 0) {
16
16
  return yield* Effect.fail(new UnannouncedWrites({ tables: missing.map((row) => row.relname).sort() }));
17
17
  }
@@ -26,4 +26,28 @@ export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(functi
26
26
  return observer(event);
27
27
  }));
28
28
  });
29
+ export class PublishedKeysMismatch extends Data.TaggedError("PublishedKeysMismatch") {
30
+ get message() {
31
+ return `Expected published keys ${JSON.stringify(this.expected)}; actual published keys ${JSON.stringify(this.actual)}`;
32
+ }
33
+ }
34
+ export function expectPublished(keys) {
35
+ return Effect.fn("Changes.expectPublished")(function* (body) {
36
+ const observer = yield* ChangeObserver;
37
+ const actual = new Set();
38
+ const result = yield* body.pipe(Effect.provideService(ChangeObserver, (event) => {
39
+ if (event._tag === "Published") {
40
+ for (const key of event.changes) {
41
+ actual.add(key);
42
+ }
43
+ }
44
+ return observer(event);
45
+ }));
46
+ const expected = new Set(keys);
47
+ if (actual.size !== expected.size || [...expected].some((key) => !actual.has(key))) {
48
+ return yield* Effect.fail(new PublishedKeysMismatch({ actual: [...actual], expected: [...expected] }));
49
+ }
50
+ return result;
51
+ });
52
+ }
29
53
  //# sourceMappingURL=testing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzD,MAAM,OAAO,iBAAkB,SAAQ,IAAI,CAAC,WAAW,CAAC,mBAAmB,CAAyC;CAAG;AAQvH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,EAAE,CAAC,4BAA4B,CAAC,CAAC,QAAQ,CAAC,EAAW,IAA4B;IACzH,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC;IACvC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAW,CAAC;IACjC,MAAM,OAAO,GAAG,GAAG,CAAa,yGAAyG,CAAC;IAC1I,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/E,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC;QAC3B,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QACvH,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;QACxG,CAAC;QACD,OAAO,MAAM,CAAC;IACf,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CACzB,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAC1C,MAAM,CAAC,cAAc,CAAC,cAAc,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/C,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBACjC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAChB,CAAC;QACF,CAAC;QACD,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC,CAAC,CACF,CAAC;AACH,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.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.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Schema-derived repositories over native Effect SQL",
5
5
  "keywords": [
6
6
  "effect"
@@ -52,7 +52,7 @@
52
52
  "@shivaedev/quality": "0.42.0",
53
53
  "@shivaedev/service": "0.5.0",
54
54
  "@shivaedev/test": "0.10.0",
55
- "@shivaedev/test-story": "0.21.0",
55
+ "@shivaedev/test-story": "0.21.1",
56
56
  "@types/node": "24.19.2",
57
57
  "effect": "4.0.5",
58
58
  "typescript": "7.0.2",
package/src/testing.ts CHANGED
@@ -4,13 +4,20 @@ import { ChangeObserver, transact } from "./transact.ts";
4
4
 
5
5
  export class UnannouncedWrites extends Data.TaggedError("UnannouncedWrites")<{ readonly tables: readonly string[] }> {}
6
6
 
7
+ export interface ChangeCoverage {
8
+ readonly covers: Readonly<Record<string, readonly unknown[]>>;
9
+ }
10
+
7
11
  interface TableWrites {
8
12
  readonly relid: number;
9
13
  readonly relname: string;
10
14
  readonly writes: number;
11
15
  }
12
16
 
13
- export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(function* <A, E, R>(body: Effect.Effect<A, E, R>) {
17
+ export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(function* <A, E, R>(
18
+ body: Effect.Effect<A, E, R>,
19
+ options?: ChangeCoverage,
20
+ ) {
14
21
  const observer = yield* ChangeObserver;
15
22
  const sql = yield* SqlClient.SqlClient;
16
23
  const named = new Set<unknown>();
@@ -18,7 +25,9 @@ export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(functi
18
25
  const checked = Effect.gen(function* () {
19
26
  const before = new Map((yield* written).map((row) => [row.relid, row.writes]));
20
27
  const result = yield* body;
21
- const missing = (yield* written).filter((row) => row.writes > (before.get(row.relid) ?? 0) && !named.has(row.relname));
28
+ const missing = (yield* written).filter(
29
+ (row) => row.writes > (before.get(row.relid) ?? 0) && !named.has(row.relname) && !options?.covers[row.relname]?.some((key) => named.has(key)),
30
+ );
22
31
  if (missing.length > 0) {
23
32
  return yield* Effect.fail(new UnannouncedWrites({ tables: missing.map((row) => row.relname).sort() }));
24
33
  }
@@ -36,3 +45,34 @@ export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(functi
36
45
  }),
37
46
  );
38
47
  });
48
+
49
+ export class PublishedKeysMismatch extends Data.TaggedError("PublishedKeysMismatch")<{
50
+ readonly expected: readonly unknown[];
51
+ readonly actual: readonly unknown[];
52
+ }> {
53
+ override get message() {
54
+ return `Expected published keys ${JSON.stringify(this.expected)}; actual published keys ${JSON.stringify(this.actual)}`;
55
+ }
56
+ }
57
+
58
+ export function expectPublished(keys: readonly unknown[]) {
59
+ return Effect.fn("Changes.expectPublished")(function* <A, E, R>(body: Effect.Effect<A, E, R>) {
60
+ const observer = yield* ChangeObserver;
61
+ const actual = new Set<unknown>();
62
+ const result = yield* body.pipe(
63
+ Effect.provideService(ChangeObserver, (event) => {
64
+ if (event._tag === "Published") {
65
+ for (const key of event.changes) {
66
+ actual.add(key);
67
+ }
68
+ }
69
+ return observer(event);
70
+ }),
71
+ );
72
+ const expected = new Set(keys);
73
+ if (actual.size !== expected.size || [...expected].some((key) => !actual.has(key))) {
74
+ return yield* Effect.fail(new PublishedKeysMismatch({ actual: [...actual], expected: [...expected] }));
75
+ }
76
+ return result;
77
+ });
78
+ }