@effect-app/infra 4.0.0-beta.327 → 4.0.0-beta.328

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.
@@ -1,10 +1,14 @@
1
+ import * as Option from "effect-app/Option";
1
2
  import * as S from "effect-app/Schema";
3
+ import * as Result from "effect/Result";
2
4
  import { toJsonQueryValue } from "./utils.js";
3
5
  const splitEtag = (doc) => {
4
6
  const { _etag, ...rest } = doc;
5
7
  return { rest: rest, _etag };
6
8
  };
7
9
  const joinEtag = (rest, _etag) => (_etag === undefined ? rest : { ...rest, _etag });
10
+ /** JSON document ↔ Encoded document codec for a store's domain schema. */
11
+ const jsonCodec = (schema) => S.toCodecJson(S.toEncoded(schema));
8
12
  /**
9
13
  * Encoded document ↔ JSON document. Prefer `Schema.toCodecJson(toEncoded(schema))`
10
14
  * when the store has a schema; otherwise lower Date/Map/Set structurally.
@@ -13,18 +17,17 @@ const joinEtag = (rest, _etag) => (_etag === undefined ? rest : { ...rest, _etag
13
17
  * document that does not match the Encoded shape fails loudly rather than being
14
18
  * read back half-decoded.
15
19
  *
16
- * Migrating a legacy-shaped document is therefore not this codec's job: the
17
- * store applies `StoreConfig.jitM` to the raw JSON document *before* calling
18
- * `decode` (see {@link makeStoredDecode}), which is the only place where a
19
- * document can be repaired. A document `jitM` does not repair is a genuine
20
- * failure and throws here.
20
+ * This is the **write** path codec: `encode` on the way to the database, and
21
+ * `decode` to hand the just-written document back. The read path uses
22
+ * {@link makeStoredDecode}, which applies `jitM` first and reports a document it
23
+ * does not repair as a typed `S.SchemaError` instead of throwing.
21
24
  *
22
25
  * `_etag` is infrastructure metadata rather than part of the domain document,
23
26
  * so it is split off before the codec runs and re-attached afterwards.
24
27
  */
25
28
  export const makeJsonDocumentCodec = (schema) => {
26
29
  if (schema) {
27
- const codec = S.toCodecJson(S.toEncoded(schema));
30
+ const codec = jsonCodec(schema);
28
31
  return {
29
32
  encode: (doc) => {
30
33
  const { rest, _etag } = splitEtag(doc);
@@ -44,30 +47,61 @@ export const makeJsonDocumentCodec = (schema) => {
44
47
  /**
45
48
  * The store *read* boundary: apply `jitM` to the raw JSON document - after the
46
49
  * adapter merged `defaultValues` into it - and only then decode JSON→Encoded
47
- * with the strict {@link makeJsonDocumentCodec} codec.
50
+ * with the strict `Schema.toCodecJson(Schema.toEncoded(schema))` codec.
48
51
  *
49
52
  * This is the one place a legacy-shaped document can be repaired, and why
50
53
  * `jitM` can fix things no schema decode could - an explicit `null` where a
51
54
  * `Date` is expected, a key added after the document was written. It runs
52
- * before any schema decode sees the document; a document it does not repair
53
- * still fails loudly in `codec.decode`.
55
+ * before any schema decode sees the document.
56
+ *
57
+ * Nothing here is lenient: a document `jitM` does not repair still fails. It
58
+ * fails as a typed `S.SchemaError` in a `Result` (via `S.decodeUnknownResult`)
59
+ * rather than by throwing, so adapters can turn it into `Effect.fail` and
60
+ * callers - `Repository.validateSample` in particular - can inspect it per
61
+ * document instead of losing the whole run to a defect.
54
62
  *
55
63
  * `_etag` is infrastructure metadata rather than part of the domain document,
56
64
  * so it is split off first and `jitM` never sees it.
57
- *
58
- * Returns `codec.decode` unchanged when there is no `jitM`; the write path,
59
- * which must not run migrations, keeps using `codec.decode` directly.
60
65
  */
61
- export const makeStoredDecode = (codec, jitM) => {
62
- if (jitM === undefined)
63
- return codec.decode;
66
+ export const makeStoredDecode = (schema, jitM) => {
67
+ if (schema === undefined) {
68
+ // schemaless store: the stored document is already the Encoded document,
69
+ // so there is nothing to decode - only `jitM` to apply.
70
+ if (jitM === undefined)
71
+ return Result.succeed;
72
+ return (doc) => {
73
+ const { rest, _etag } = splitEtag(doc);
74
+ return Result.succeed(joinEtag(jitM(rest), _etag));
75
+ };
76
+ }
77
+ const decode = S.decodeUnknownResult(jsonCodec(schema));
64
78
  return (doc) => {
65
79
  const { rest, _etag } = splitEtag(doc);
66
80
  // adapters type the stored document as the Encoded persistence model, but
67
81
  // at this point it is still the raw JSON document that `jitM` is written
68
- // against - and `jitM` returns JSON, which is what `codec.decode` consumes.
69
- const migrated = jitM(rest);
70
- return codec.decode(joinEtag(migrated, _etag));
82
+ // against - and `jitM` returns JSON, which is what the codec consumes.
83
+ const json = jitM === undefined ? rest : jitM(rest);
84
+ return Result.map(decode(json), (decoded) => joinEtag(decoded, _etag));
71
85
  };
72
86
  };
73
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoianNvbkRvY3VtZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL1N0b3JlL2pzb25Eb2N1bWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEtBQUssQ0FBQyxNQUFNLG1CQUFtQixDQUFBO0FBRXRDLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLFlBQVksQ0FBQTtBQU83QyxNQUFNLFNBQVMsR0FBRyxDQUF3QixHQUE0QixFQUFFLEVBQUU7SUFDeEUsTUFBTSxFQUFFLEtBQUssRUFBRSxHQUFHLElBQUksRUFBRSxHQUFHLEdBQUcsQ0FBQTtJQUM5QixPQUFPLEVBQUUsSUFBSSxFQUFFLElBQVMsRUFBRSxLQUFLLEVBQUUsQ0FBQTtBQUNuQyxDQUFDLENBQUE7QUFFRCxNQUFNLFFBQVEsR0FBRyxDQUNmLElBQU8sRUFDUCxLQUF5QixFQUNBLEVBQUUsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFBO0FBRS9FOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQUcsQ0FBd0IsTUFBYyxFQUF3QixFQUFFO0lBQ25HLElBQUksTUFBTSxFQUFFLENBQUM7UUFDWCxNQUFNLEtBQUssR0FBRyxDQUFDLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLENBQXVCLENBQUE7UUFDdEUsT0FBTztZQUNMLE1BQU0sRUFBRSxDQUFDLEdBQUcsRUFBRSxFQUFFO2dCQUNkLE1BQU0sRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsU0FBUyxDQUFDLEdBQUcsQ0FBQyxDQUFBO2dCQUN0QyxPQUFPLFFBQVEsQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBTSxFQUFFLEtBQUssQ0FBQyxDQUFBO1lBQ3hELENBQUM7WUFDRCxNQUFNLEVBQUUsQ0FBQyxHQUFHLEVBQUUsRUFBRTtnQkFDZCxNQUFNLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxHQUFHLFNBQVMsQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDdEMsT0FBTyxRQUFRLENBQUMsQ0FBQyxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxJQUFjLENBQUMsRUFBRSxLQUFLLENBQUMsQ0FBQTtZQUM3RCxDQUFDO1NBQ0YsQ0FBQTtJQUNILENBQUM7SUFDRCxPQUFPO1FBQ0wsTUFBTSxFQUFFLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxnQkFBZ0IsQ0FBQyxHQUFHLENBQTRCO1FBQ2pFLE1BQU0sRUFBRSxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsR0FBRztLQUNyQixDQUFBO0FBQ0gsQ0FBQyxDQUFBO0FBRUQ7Ozs7Ozs7Ozs7Ozs7Ozs7R0FnQkc7QUFDSCxNQUFNLENBQUMsTUFBTSxnQkFBZ0IsR0FBRyxDQUM5QixLQUEyQixFQUMzQixJQUE0QixFQUNJLEVBQUU7SUFDbEMsSUFBSSxJQUFJLEtBQUssU0FBUztRQUFFLE9BQU8sS0FBSyxDQUFDLE1BQU0sQ0FBQTtJQUMzQyxPQUFPLENBQUMsR0FBRyxFQUFFLEVBQUU7UUFDYixNQUFNLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxHQUFHLFNBQVMsQ0FBQyxHQUFHLENBQUMsQ0FBQTtRQUN0QywwRUFBMEU7UUFDMUUseUVBQXlFO1FBQ3pFLDRFQUE0RTtRQUM1RSxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFpQixDQUFBO1FBQzNDLE9BQU8sS0FBSyxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsUUFBUSxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUE7SUFDaEQsQ0FBQyxDQUFBO0FBQ0gsQ0FBQyxDQUFBIn0=
87
+ /**
88
+ * Decode a whole result set, failing fast on the first bad document.
89
+ *
90
+ * One synchronous loop and one `Result` for the batch: no Effect per document,
91
+ * and no allocation beyond the decoded array (plus a `Result` wrapper) on the
92
+ * happy path. Adapters lift the returned `Result` with `Effect.fromResult`
93
+ * once, at the end.
94
+ */
95
+ export const decodeStoredMany = (docs, decode) => {
96
+ const decoded = [];
97
+ for (const doc of docs) {
98
+ const result = decode(doc);
99
+ if (Result.isFailure(result))
100
+ return Result.fail(result.failure);
101
+ decoded.push(result.success);
102
+ }
103
+ return Result.succeed(decoded);
104
+ };
105
+ /** Decode an optional stored document; `None` short-circuits without decoding. */
106
+ export const decodeStoredOption = (doc, decode) => Option.isNone(doc) ? Result.succeed(Option.none()) : Result.map(decode(doc.value), Option.some);
107
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoianNvbkRvY3VtZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL1N0b3JlL2pzb25Eb2N1bWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEtBQUssTUFBTSxNQUFNLG1CQUFtQixDQUFBO0FBQzNDLE9BQU8sS0FBSyxDQUFDLE1BQU0sbUJBQW1CLENBQUE7QUFFdEMsT0FBTyxLQUFLLE1BQU0sTUFBTSxlQUFlLENBQUE7QUFDdkMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sWUFBWSxDQUFBO0FBbUI3QyxNQUFNLFNBQVMsR0FBRyxDQUF3QixHQUE0QixFQUFFLEVBQUU7SUFDeEUsTUFBTSxFQUFFLEtBQUssRUFBRSxHQUFHLElBQUksRUFBRSxHQUFHLEdBQUcsQ0FBQTtJQUM5QixPQUFPLEVBQUUsSUFBSSxFQUFFLElBQVMsRUFBRSxLQUFLLEVBQUUsQ0FBQTtBQUNuQyxDQUFDLENBQUE7QUFFRCxNQUFNLFFBQVEsR0FBRyxDQUNmLElBQU8sRUFDUCxLQUF5QixFQUNBLEVBQUUsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFBO0FBRS9FLDBFQUEwRTtBQUMxRSxNQUFNLFNBQVMsR0FBRyxDQUF3QixNQUFhLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsQ0FBdUIsQ0FBQTtBQUVwSDs7Ozs7Ozs7Ozs7Ozs7O0dBZUc7QUFDSCxNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBRyxDQUF3QixNQUFjLEVBQXdCLEVBQUU7SUFDbkcsSUFBSSxNQUFNLEVBQUUsQ0FBQztRQUNYLE1BQU0sS0FBSyxHQUFHLFNBQVMsQ0FBSSxNQUFNLENBQUMsQ0FBQTtRQUNsQyxPQUFPO1lBQ0wsTUFBTSxFQUFFLENBQUMsR0FBRyxFQUFFLEVBQUU7Z0JBQ2QsTUFBTSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsR0FBRyxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUE7Z0JBQ3RDLE9BQU8sUUFBUSxDQUFDLENBQUMsQ0FBQyxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUMsSUFBSSxDQUFNLEVBQUUsS0FBSyxDQUFDLENBQUE7WUFDeEQsQ0FBQztZQUNELE1BQU0sRUFBRSxDQUFDLEdBQUcsRUFBRSxFQUFFO2dCQUNkLE1BQU0sRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsU0FBUyxDQUFDLEdBQUcsQ0FBQyxDQUFBO2dCQUN0QyxPQUFPLFFBQVEsQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQWMsQ0FBQyxFQUFFLEtBQUssQ0FBQyxDQUFBO1lBQzdELENBQUM7U0FDRixDQUFBO0lBQ0gsQ0FBQztJQUNELE9BQU87UUFDTCxNQUFNLEVBQUUsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLGdCQUFnQixDQUFDLEdBQUcsQ0FBNEI7UUFDakUsTUFBTSxFQUFFLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxHQUFHO0tBQ3JCLENBQUE7QUFDSCxDQUFDLENBQUE7QUFFRDs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBa0JHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sZ0JBQWdCLEdBQUcsQ0FDOUIsTUFBZ0MsRUFDaEMsSUFBNEIsRUFDWCxFQUFFO0lBQ25CLElBQUksTUFBTSxLQUFLLFNBQVMsRUFBRSxDQUFDO1FBQ3pCLHlFQUF5RTtRQUN6RSx3REFBd0Q7UUFDeEQsSUFBSSxJQUFJLEtBQUssU0FBUztZQUFFLE9BQU8sTUFBTSxDQUFDLE9BQU8sQ0FBQTtRQUM3QyxPQUFPLENBQUMsR0FBRyxFQUFFLEVBQUU7WUFDYixNQUFNLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxHQUFHLFNBQVMsQ0FBQyxHQUFHLENBQUMsQ0FBQTtZQUN0QyxPQUFPLE1BQU0sQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQWlCLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQTtRQUNwRSxDQUFDLENBQUE7SUFDSCxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQUcsQ0FBQyxDQUFDLG1CQUFtQixDQUFDLFNBQVMsQ0FBSSxNQUFNLENBQUMsQ0FBQyxDQUFBO0lBQzFELE9BQU8sQ0FBQyxHQUFHLEVBQUUsRUFBRTtRQUNiLE1BQU0sRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsU0FBUyxDQUFDLEdBQUcsQ0FBQyxDQUFBO1FBQ3RDLDBFQUEwRTtRQUMxRSx5RUFBeUU7UUFDekUsdUVBQXVFO1FBQ3ZFLE1BQU0sSUFBSSxHQUFHLElBQUksS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBaUIsQ0FBQTtRQUNuRSxPQUFPLE1BQU0sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxRQUFRLENBQUMsT0FBTyxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUE7SUFDeEUsQ0FBQyxDQUFBO0FBQ0gsQ0FBQyxDQUFBO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILE1BQU0sQ0FBQyxNQUFNLGdCQUFnQixHQUFHLENBQzlCLElBQWtCLEVBQ2xCLE1BQW1ELEVBQ2hCLEVBQUU7SUFDckMsTUFBTSxPQUFPLEdBQVEsRUFBRSxDQUFBO0lBQ3ZCLEtBQUssTUFBTSxHQUFHLElBQUksSUFBSSxFQUFFLENBQUM7UUFDdkIsTUFBTSxNQUFNLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO1FBQzFCLElBQUksTUFBTSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUM7WUFBRSxPQUFPLE1BQU0sQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxDQUFBO1FBQ2hFLE9BQU8sQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxDQUFBO0lBQzlCLENBQUM7SUFDRCxPQUFPLE1BQU0sQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUE7QUFDaEMsQ0FBQyxDQUFBO0FBRUQsa0ZBQWtGO0FBQ2xGLE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixHQUFHLENBQ2hDLEdBQXFCLEVBQ3JCLE1BQW1ELEVBQ0gsRUFBRSxDQUNsRCxNQUFNLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFBIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-app/infra",
3
- "version": "4.0.0-beta.327",
3
+ "version": "4.0.0-beta.328",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -16,7 +16,7 @@
16
16
  "proper-lockfile": "^4.1.2",
17
17
  "pure-rand": "8.4.0",
18
18
  "query-string": "^9.4.0",
19
- "effect-app": "4.0.0-beta.327"
19
+ "effect-app": "4.0.0-beta.328"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@azure/cosmos": "^4.9.3",
@@ -19,7 +19,7 @@ import { DatabaseError, OptimisticConcurrencyException } from "../errors.ts"
19
19
  import { InfraLogger } from "../logger.ts"
20
20
  import { annotateCosmosResponse, annotateDb } from "../otel.ts"
21
21
  import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.ts"
22
- import { makeJsonDocumentCodec, makeStoredDecode } from "./jsonDocument.ts"
22
+ import { decodeStoredMany, decodeStoredOption, makeJsonDocumentCodec, makeStoredDecode } from "./jsonDocument.ts"
23
23
  import { makeJsonLower } from "./utils.ts"
24
24
 
25
25
  const makeMapId =
@@ -99,7 +99,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ autoscaleMaxThroughput, pr
99
99
  const mapId = makeMapId<IdKey, Encoded>(idKey)
100
100
  const mapReverseId = makeReverseMapId<IdKey, Encoded>(idKey)
101
101
  const codec = makeJsonDocumentCodec<Encoded>(config?.schema)
102
- const decodeStored = makeStoredDecode<Encoded>(codec, config?.jitM)
102
+ const decodeStored = makeStoredDecode<Encoded>(config?.schema, config?.jitM)
103
103
  const json = makeJsonLower(config)
104
104
  const defaultValues = json.toJson(config?.defaultValues ?? {}) as Partial<Encoded>
105
105
  // stored JSON -> defaultValues -> jitM -> JSON→Encoded decode
@@ -458,7 +458,9 @@ const makeCosmosStore = Effect.fnUntraced(function*({ autoscaleMaxThroughput, pr
458
458
  container.items.query<PMCosmos>(q, { partitionKey: nsBasePartitionKey(ns) }).fetchAll()
459
459
  )
460
460
  yield* annotateFeed(response)
461
- return response.resources.map((_) => fromStored(_ as unknown as Encoded))
461
+ return yield* Effect.fromResult(
462
+ decodeStoredMany(response.resources, (_) => fromStored(_ as unknown as Encoded))
463
+ )
462
464
  })
463
465
  .pipe(
464
466
  annotateDb({
@@ -532,7 +534,10 @@ const makeCosmosStore = Effect.fnUntraced(function*({ autoscaleMaxThroughput, pr
532
534
  container.items.query<{ f: M }>(q, { partitionKey: nsBasePartitionKey(ns) }).fetchAll()
533
535
  )
534
536
  yield* annotateFeed(response)
535
- return response.resources.map(({ f }) => fromStored(f as Encoded) as any)
537
+ const decoded = yield* Effect.fromResult(
538
+ decodeStoredMany(response.resources, ({ f }) => fromStored(f as Encoded))
539
+ )
540
+ return decoded as any
536
541
  })
537
542
  .pipe(
538
543
  annotateDb({
@@ -557,8 +562,8 @@ const makeCosmosStore = Effect.fnUntraced(function*({ autoscaleMaxThroughput, pr
557
562
  .read<Encoded>()
558
563
  )
559
564
  yield* annotateItem(response)
560
- return Option.fromNullishOr(response.resource).pipe(
561
- Option.map((_) => fromStored(_))
565
+ return yield* Effect.fromResult(
566
+ decodeStoredOption(Option.fromNullishOr(response.resource), fromStored)
562
567
  )
563
568
  })
564
569
  .pipe(annotateDb({
package/src/Store/Disk.ts CHANGED
@@ -10,7 +10,7 @@ import * as Console from "effect/Console"
10
10
  import { flow } from "effect/Function"
11
11
  import * as Semaphore from "effect/Semaphore"
12
12
  import { annotateDb } from "../otel.ts"
13
- import { makeJsonDocumentCodec, makeStoredDecode } from "./jsonDocument.ts"
13
+ import { decodeStoredMany, makeJsonDocumentCodec, makeStoredDecode } from "./jsonDocument.ts"
14
14
  import { makeMemoryStoreInt } from "./Memory.ts"
15
15
  import { type JsonLower, makeJsonLower } from "./utils.ts"
16
16
 
@@ -30,7 +30,7 @@ function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValu
30
30
  const codec = makeJsonDocumentCodec<Encoded>(schema)
31
31
  // the file is the raw JSON boundary for this adapter:
32
32
  // stored JSON -> jitM -> JSON→Encoded decode
33
- const decodeStored = makeStoredDecode<Encoded>(codec, jitM)
33
+ const decodeStored = makeStoredDecode<Encoded>(schema, jitM)
34
34
  return Effect.gen(function*() {
35
35
  if (namespace !== "primary") {
36
36
  dir = dir + "/" + namespace
@@ -53,7 +53,8 @@ function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValu
53
53
  extra: fileExtra
54
54
  }),
55
55
  Effect.flatMap((x) =>
56
- Effect.sync(() => (JSON.parse(x) as PM[]).map((row) => decodeStored(row))).pipe(
56
+ Effect.sync(() => JSON.parse(x) as PM[]).pipe(
57
+ Effect.flatMap((docs) => Effect.fromResult(decodeStoredMany(docs, decodeStored))),
57
58
  annotateDb({
58
59
  operation: "read.parse",
59
60
  system: "disk",
@@ -18,7 +18,7 @@ import * as Struct from "effect/Struct"
18
18
  import { InfraLogger } from "../logger.ts"
19
19
  import { annotateDb } from "../otel.ts"
20
20
  import { codeFilter, codeFilter3_ } from "./codeFilter.ts"
21
- import { makeJsonDocumentCodec, makeStoredDecode } from "./jsonDocument.ts"
21
+ import { decodeStoredMany, decodeStoredOption, makeJsonDocumentCodec, makeStoredDecode } from "./jsonDocument.ts"
22
22
  import { get, jsonifyFilter, type JsonLower, makeJsonLower, makeUpdateETag, toJsonQueryValue } from "./utils.ts"
23
23
 
24
24
  export { get } from "./utils.ts"
@@ -344,7 +344,7 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
344
344
  const codec = makeJsonDocumentCodec<Encoded>(schema)
345
345
  const encodeDoc = (e: Encoded | PM): PM => codec.encode({ _etag: undefined, ...e })
346
346
  // read path: stored JSON -> jitM -> JSON→Encoded decode
347
- const decodeDoc = makeStoredDecode<Encoded>(codec, jitM)
347
+ const decodeDoc = makeStoredDecode<Encoded>(schema, jitM)
348
348
  const items_ = yield* seed ?? Effect.sync(() => [])
349
349
  const toJson = json?.toJson ?? toJsonQueryValue
350
350
  const lowerFilter = json?.jsonifyFilter ?? jsonifyFilter
@@ -362,7 +362,7 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
362
362
  const values = Effect.map(Ref.get(store), (s) => s.values())
363
363
 
364
364
  const allStored = Effect.map(values, Array.fromIterable)
365
- const all = Effect.map(allStored, (rows) => rows.map(decodeDoc))
365
+ const all = Effect.flatMap(allStored, (rows) => Effect.fromResult(decodeStoredMany(rows, decodeDoc)))
366
366
 
367
367
  const batchSet = (items: NonEmptyReadonlyArray<PM>) =>
368
368
  Effect
@@ -384,7 +384,9 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
384
384
  )
385
385
  ),
386
386
  Effect
387
- .map((items) => items.map(decodeDoc) as unknown as NonEmptyReadonlyArray<PM>),
387
+ .flatMap((items) => Effect.fromResult(decodeStoredMany(items, decodeDoc))),
388
+ Effect
389
+ .map((items) => items as unknown as NonEmptyReadonlyArray<PM>),
388
390
  withPermit
389
391
  )
390
392
 
@@ -430,7 +432,7 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
430
432
  Ref
431
433
  .get(store)
432
434
  .pipe(
433
- Effect.map((_) => Option.fromNullishOr(_.get(id)).pipe(Option.map(decodeDoc))),
435
+ Effect.flatMap((_) => Effect.fromResult(decodeStoredOption(Option.fromNullishOr(_.get(id)), decodeDoc))),
434
436
  annotateDb({
435
437
  operation: "find",
436
438
  system: "memory",
@@ -445,10 +447,13 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
445
447
  .pipe(
446
448
  Effect.tap(() => logQuery(f, encodedDefaults)),
447
449
  Effect.map(memFilter({ ...f, filter: f.filter ? lowerFilter(f.filter) : f.filter })),
448
- Effect.map((rows): (U extends undefined ? Encoded : Pick<Encoded, U>)[] =>
450
+ Effect.flatMap((rows) =>
449
451
  f.select
450
- ? rows as (U extends undefined ? Encoded : Pick<Encoded, U>)[]
451
- : rows.map(decodeDoc) as (U extends undefined ? Encoded : Pick<Encoded, U>)[]
452
+ ? Effect.succeed(rows as (U extends undefined ? Encoded : Pick<Encoded, U>)[])
453
+ : Effect.map(
454
+ Effect.fromResult(decodeStoredMany(rows, decodeDoc)),
455
+ (decoded) => decoded as (U extends undefined ? Encoded : Pick<Encoded, U>)[]
456
+ )
452
457
  ),
453
458
  annotateDb({
454
459
  operation: "filter",
@@ -470,7 +475,7 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
470
475
  Effect.flatMap((_) => Ref.set(store, _))
471
476
  )
472
477
  ),
473
- Effect.map(decodeDoc),
478
+ Effect.flatMap((stored) => Effect.fromResult(decodeDoc(stored))),
474
479
  withPermit,
475
480
  annotateDb({
476
481
  operation: "set",
@@ -12,7 +12,7 @@ import { SqlClient } from "effect/unstable/sql"
12
12
  import { DatabaseError, OptimisticConcurrencyException } from "../../errors.ts"
13
13
  import { InfraLogger } from "../../logger.ts"
14
14
  import { annotateDb } from "../../otel.ts"
15
- import { makeJsonDocumentCodec, makeStoredDecode } from "../jsonDocument.ts"
15
+ import { decodeStoredMany, decodeStoredOption, makeJsonDocumentCodec, makeStoredDecode } from "../jsonDocument.ts"
16
16
  import { makeETag, makeJsonLower, toJsonQueryValue } from "../utils.ts"
17
17
  import { buildWhereSQLQuery, logQuery, pgDialect } from "./query.ts"
18
18
 
@@ -34,17 +34,19 @@ const preserveStoreError = (e: unknown): DatabaseError | OptimisticConcurrencyEx
34
34
  return toDatabaseError(e)
35
35
  }
36
36
 
37
+ /**
38
+ * Row -> raw stored JSON document. Decoding it is a separate step
39
+ * (`makeStoredDecode`), so a document `jitM` does not repair fails as a typed
40
+ * `S.SchemaError` rather than throwing out of here.
41
+ */
37
42
  const parseRow = <Encoded extends FieldValues>(
38
43
  row: { id: string; _etag: string | null; data: unknown },
39
44
  idKey: PropertyKey,
40
- defaultValues: Partial<Encoded>,
41
- decode: (doc: PersistenceModelType<Encoded>) => PersistenceModelType<Encoded> = (doc) => doc
45
+ defaultValues: Partial<Encoded>
42
46
  ): PersistenceModelType<Encoded> => {
43
47
  const data = (typeof row.data === "string" ? JSON.parse(row.data) : row.data) as object
44
48
  const jsonDefaults = toJsonQueryValue(defaultValues) as Partial<Encoded>
45
- return decode(
46
- { ...jsonDefaults, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
47
- )
49
+ return { ...jsonDefaults, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
48
50
  }
49
51
 
50
52
  const parseSelectRow = (
@@ -79,7 +81,9 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
79
81
  const defaultValues = json.toJson(config?.defaultValues ?? {}) as Partial<Encoded>
80
82
  const codec = makeJsonDocumentCodec<Encoded>(config?.schema)
81
83
  // read path: stored JSON -> defaultValues -> jitM -> JSON→Encoded decode
82
- const decodeStored = makeStoredDecode<Encoded>(codec, config?.jitM)
84
+ const decodeStored = makeStoredDecode<Encoded>(config?.schema, config?.jitM)
85
+ const decodeRow = (row: { id: string; _etag: string | null; data: unknown }) =>
86
+ decodeStored(parseRow<Encoded>(row, idKey, defaultValues))
83
87
 
84
88
  const resolveNamespace = !config?.allowNamespace
85
89
  ? Effect.succeed("primary")
@@ -202,9 +206,7 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
202
206
  const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = $1`
203
207
  return exec(sqlText, [ns])
204
208
  .pipe(
205
- Effect.map((rows) =>
206
- (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues, decodeStored))
207
- ),
209
+ Effect.flatMap((rows) => Effect.fromResult(decodeStoredMany(rows as any[], decodeRow))),
208
210
  annotateDb({
209
211
  operation: "all",
210
212
  system: "postgresql",
@@ -223,12 +225,9 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
223
225
  const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE id = $1 AND _namespace = $2`
224
226
  return exec(sqlText, [id, ns])
225
227
  .pipe(
226
- Effect.map((rows) => {
227
- const row = (rows as any[])[0]
228
- return row
229
- ? Option.some(parseRow<Encoded>(row, idKey, defaultValues, decodeStored))
230
- : Option.none()
231
- }),
228
+ Effect.flatMap((rows) =>
229
+ Effect.fromResult(decodeStoredOption(Option.fromNullishOr((rows as any[])[0]), decodeRow))
230
+ ),
232
231
  annotateDb({
233
232
  operation: "find",
234
233
  system: "postgresql",
@@ -286,9 +285,9 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
286
285
  Effect.tap((q) => Effect.annotateCurrentSpan({ "db.query.text": q.sql })),
287
286
  Effect.flatMap((q) =>
288
287
  exec(q.sql, q.params).pipe(
289
- Effect.map((rows) => {
288
+ Effect.flatMap((rows) => {
290
289
  if (f.select) {
291
- return (rows as any[]).map((r) => {
290
+ return Effect.succeed((rows as any[]).map((r) => {
292
291
  const selected = parseSelectRow(r, idKey, {})
293
292
  return {
294
293
  ...Struct.pick(
@@ -297,10 +296,11 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
297
296
  ),
298
297
  ...selected
299
298
  } as M
300
- })
299
+ }))
301
300
  }
302
- return (rows as any[]).map((r) =>
303
- parseRow<Encoded>(r, idKey, defaultValues, decodeStored) as any as M
301
+ return Effect.map(
302
+ Effect.fromResult(decodeStoredMany(rows as any[], decodeRow)),
303
+ (decoded) => decoded as any as M[]
304
304
  )
305
305
  })
306
306
  )
package/src/Store/SQL.ts CHANGED
@@ -15,7 +15,7 @@ import { SqlClient } from "effect/unstable/sql"
15
15
  import { DatabaseError, OptimisticConcurrencyException } from "../errors.ts"
16
16
  import { InfraLogger } from "../logger.ts"
17
17
  import { annotateDb, type DbSystem } from "../otel.ts"
18
- import { makeJsonDocumentCodec, makeStoredDecode } from "./jsonDocument.ts"
18
+ import { decodeStoredMany, decodeStoredOption, makeJsonDocumentCodec, makeStoredDecode } from "./jsonDocument.ts"
19
19
  import { buildWhereSQLQuery, logQuery, type SQLDialect, sqliteDialect } from "./SQL/query.ts"
20
20
  import { makeETag, makeJsonLower, toJsonQueryValue } from "./utils.ts"
21
21
 
@@ -43,18 +43,22 @@ export class WithNsTransaction
43
43
  extends Context.Service<WithNsTransaction, WithNsTransactionFn>()("effect-app/WithNsTransaction")
44
44
  {}
45
45
 
46
- /** @internal */
46
+ /**
47
+ * Row -> raw stored JSON document: `data` parsed, `defaultValues` merged, `id`
48
+ * and `_etag` re-injected from their columns. Decoding it is a separate step
49
+ * (`makeStoredDecode`), so a document `jitM` does not repair fails as a typed
50
+ * `S.SchemaError` rather than throwing out of here.
51
+ *
52
+ * @internal
53
+ */
47
54
  export const parseRow = <Encoded extends FieldValues>(
48
55
  row: { id: string; _etag: string | null; data: string },
49
56
  idKey: PropertyKey,
50
- defaultValues: Partial<Encoded>,
51
- decode: (doc: PersistenceModelType<Encoded>) => PersistenceModelType<Encoded> = (doc) => doc
57
+ defaultValues: Partial<Encoded>
52
58
  ): PersistenceModelType<Encoded> => {
53
59
  const data = (typeof row.data === "string" ? JSON.parse(row.data) : row.data) as object
54
60
  const jsonDefaults = toJsonQueryValue(defaultValues) as Partial<Encoded>
55
- return decode(
56
- { ...jsonDefaults, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
57
- )
61
+ return { ...jsonDefaults, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
58
62
  }
59
63
 
60
64
  const parseSelectRow = (
@@ -95,7 +99,9 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
95
99
  const defaultValues = json.toJson(config?.defaultValues ?? {}) as Partial<Encoded>
96
100
  const codec = makeJsonDocumentCodec<Encoded>(config?.schema)
97
101
  // read path: stored JSON -> defaultValues -> jitM -> JSON→Encoded decode
98
- const decodeStored = makeStoredDecode<Encoded>(codec, config?.jitM)
102
+ const decodeStored = makeStoredDecode<Encoded>(config?.schema, config?.jitM)
103
+ const decodeRow = (row: { id: string; _etag: string | null; data: string }) =>
104
+ decodeStored(parseRow<Encoded>(row, idKey, defaultValues))
99
105
 
100
106
  const resolveNamespace = !config?.allowNamespace
101
107
  ? Effect.succeed("primary")
@@ -218,9 +224,7 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
218
224
  const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = ?`
219
225
  return exec(sqlText, [ns])
220
226
  .pipe(
221
- Effect.map((rows) =>
222
- (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues, decodeStored))
223
- ),
227
+ Effect.flatMap((rows) => Effect.fromResult(decodeStoredMany(rows as any[], decodeRow))),
224
228
  annotateDb({
225
229
  operation: "all",
226
230
  system,
@@ -239,12 +243,9 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
239
243
  const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE id = ? AND _namespace = ?`
240
244
  return exec(sqlText, [id, ns])
241
245
  .pipe(
242
- Effect.map((rows) => {
243
- const row = (rows as any[])[0]
244
- return row
245
- ? Option.some(parseRow<Encoded>(row, idKey, defaultValues, decodeStored))
246
- : Option.none()
247
- }),
246
+ Effect.flatMap((rows) =>
247
+ Effect.fromResult(decodeStoredOption(Option.fromNullishOr((rows as any[])[0]), decodeRow))
248
+ ),
248
249
  annotateDb({
249
250
  operation: "find",
250
251
  system,
@@ -302,9 +303,9 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
302
303
  Effect.tap((q) => Effect.annotateCurrentSpan({ "db.query.text": q.sql })),
303
304
  Effect.flatMap((q) =>
304
305
  exec(q.sql, q.params).pipe(
305
- Effect.map((rows) => {
306
+ Effect.flatMap((rows) => {
306
307
  if (f.select) {
307
- return (rows as any[]).map((r) => {
308
+ return Effect.succeed((rows as any[]).map((r) => {
308
309
  const selected = parseSelectRow(r, idKey)
309
310
  return {
310
311
  ...Struct.pick(
@@ -313,10 +314,11 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
313
314
  ),
314
315
  ...selected
315
316
  } as M
316
- })
317
+ }))
317
318
  }
318
- return (rows as any[]).map((r) =>
319
- parseRow<Encoded>(r, idKey, defaultValues, decodeStored) as any as M
319
+ return Effect.map(
320
+ Effect.fromResult(decodeStoredMany(rows as any[], decodeRow)),
321
+ (decoded) => decoded as any as M[]
320
322
  )
321
323
  })
322
324
  )
@@ -436,7 +438,9 @@ function makeSQLiteStorePerNs(
436
438
  const defaultValues = json.toJson(config?.defaultValues ?? {}) as Partial<Encoded>
437
439
  const codec = makeJsonDocumentCodec<Encoded>(config?.schema)
438
440
  // read path: stored JSON -> defaultValues -> jitM -> JSON→Encoded decode
439
- const decodeStored = makeStoredDecode<Encoded>(codec, config?.jitM)
441
+ const decodeStored = makeStoredDecode<Encoded>(config?.schema, config?.jitM)
442
+ const decodeRow = (row: { id: string; _etag: string | null; data: string }) =>
443
+ decodeStored(parseRow<Encoded>(row, idKey, defaultValues))
440
444
 
441
445
  const resolveNamespace = !config?.allowNamespace
442
446
  ? Effect.succeed("primary")
@@ -567,9 +571,7 @@ function makeSQLiteStorePerNs(
567
571
  const sqlText = `SELECT id, _etag, data FROM "${tableName}"`
568
572
  return exec(ns, sqlText)
569
573
  .pipe(
570
- Effect.map((rows) =>
571
- (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues, decodeStored))
572
- ),
574
+ Effect.flatMap((rows) => Effect.fromResult(decodeStoredMany(rows as any[], decodeRow))),
573
575
  annotateDb({
574
576
  operation: "all",
575
577
  system: "sqlite",
@@ -587,12 +589,9 @@ function makeSQLiteStorePerNs(
587
589
  const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE id = ?`
588
590
  return exec(ns, sqlText, [id])
589
591
  .pipe(
590
- Effect.map((rows) => {
591
- const row = (rows as any[])[0]
592
- return row
593
- ? Option.some(parseRow<Encoded>(row, idKey, defaultValues, decodeStored))
594
- : Option.none()
595
- }),
592
+ Effect.flatMap((rows) =>
593
+ Effect.fromResult(decodeStoredOption(Option.fromNullishOr((rows as any[])[0]), decodeRow))
594
+ ),
596
595
  annotateDb({
597
596
  operation: "find",
598
597
  system: "sqlite",
@@ -650,9 +649,9 @@ function makeSQLiteStorePerNs(
650
649
  Effect.tap((q) => Effect.annotateCurrentSpan({ "db.query.text": q.sql })),
651
650
  Effect.flatMap((q) =>
652
651
  exec(ns, q.sql, q.params).pipe(
653
- Effect.map((rows) => {
652
+ Effect.flatMap((rows) => {
654
653
  if (f.select) {
655
- return (rows as any[]).map((r) => {
654
+ return Effect.succeed((rows as any[]).map((r) => {
656
655
  const selected = parseSelectRow(r, idKey)
657
656
  return {
658
657
  ...Struct.pick(
@@ -661,10 +660,11 @@ function makeSQLiteStorePerNs(
661
660
  ),
662
661
  ...selected
663
662
  } as M
664
- })
663
+ }))
665
664
  }
666
- return (rows as any[]).map((r) =>
667
- parseRow<Encoded>(r, idKey, defaultValues, decodeStored) as any as M
665
+ return Effect.map(
666
+ Effect.fromResult(decodeStoredMany(rows as any[], decodeRow)),
667
+ (decoded) => decoded as any as M[]
668
668
  )
669
669
  })
670
670
  )