@effect-app/infra 4.0.0-beta.326 → 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,21 @@
1
1
  import type { FieldValues } from "effect-app/Model/filter/types";
2
+ import * as Option from "effect-app/Option";
2
3
  import * as S from "effect-app/Schema";
3
4
  import type { PersistenceModelType, StoreConfig } from "effect-app/Store";
5
+ import * as Result from "effect/Result";
4
6
  export interface JsonDocumentCodec<E extends FieldValues> {
5
7
  readonly encode: (doc: PersistenceModelType<E>) => PersistenceModelType<E>;
6
8
  readonly decode: (doc: PersistenceModelType<E>) => PersistenceModelType<E>;
7
9
  }
10
+ /**
11
+ * The store *read* boundary decode: a stored JSON document in, either the
12
+ * Encoded document or the `S.SchemaError` saying why it is not one.
13
+ *
14
+ * Synchronous on purpose: it runs once per document over whole result sets, so
15
+ * it must not allocate an Effect per row. Adapters lift the `Result` into their
16
+ * Effect channel once per operation (see {@link decodeStoredMany}).
17
+ */
18
+ export type StoredDecode<E extends FieldValues> = (doc: PersistenceModelType<E>) => Result.Result<PersistenceModelType<E>, S.SchemaError>;
8
19
  /**
9
20
  * Encoded document ↔ JSON document. Prefer `Schema.toCodecJson(toEncoded(schema))`
10
21
  * when the store has a schema; otherwise lower Date/Map/Set structurally.
@@ -13,11 +24,10 @@ export interface JsonDocumentCodec<E extends FieldValues> {
13
24
  * document that does not match the Encoded shape fails loudly rather than being
14
25
  * read back half-decoded.
15
26
  *
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.
27
+ * This is the **write** path codec: `encode` on the way to the database, and
28
+ * `decode` to hand the just-written document back. The read path uses
29
+ * {@link makeStoredDecode}, which applies `jitM` first and reports a document it
30
+ * does not repair as a typed `S.SchemaError` instead of throwing.
21
31
  *
22
32
  * `_etag` is infrastructure metadata rather than part of the domain document,
23
33
  * so it is split off before the codec runs and re-attached afterwards.
@@ -26,19 +36,32 @@ export declare const makeJsonDocumentCodec: <E extends FieldValues>(schema?: S.T
26
36
  /**
27
37
  * The store *read* boundary: apply `jitM` to the raw JSON document - after the
28
38
  * adapter merged `defaultValues` into it - and only then decode JSON→Encoded
29
- * with the strict {@link makeJsonDocumentCodec} codec.
39
+ * with the strict `Schema.toCodecJson(Schema.toEncoded(schema))` codec.
30
40
  *
31
41
  * This is the one place a legacy-shaped document can be repaired, and why
32
42
  * `jitM` can fix things no schema decode could - an explicit `null` where a
33
43
  * `Date` is expected, a key added after the document was written. It runs
34
- * before any schema decode sees the document; a document it does not repair
35
- * still fails loudly in `codec.decode`.
44
+ * before any schema decode sees the document.
45
+ *
46
+ * Nothing here is lenient: a document `jitM` does not repair still fails. It
47
+ * fails as a typed `S.SchemaError` in a `Result` (via `S.decodeUnknownResult`)
48
+ * rather than by throwing, so adapters can turn it into `Effect.fail` and
49
+ * callers - `Repository.validateSample` in particular - can inspect it per
50
+ * document instead of losing the whole run to a defect.
36
51
  *
37
52
  * `_etag` is infrastructure metadata rather than part of the domain document,
38
53
  * so it is split off first and `jitM` never sees it.
54
+ */
55
+ export declare const makeStoredDecode: <E extends FieldValues>(schema: StoreConfig<E>["schema"], jitM: StoreConfig<E>["jitM"]) => StoredDecode<E>;
56
+ /**
57
+ * Decode a whole result set, failing fast on the first bad document.
39
58
  *
40
- * Returns `codec.decode` unchanged when there is no `jitM`; the write path,
41
- * which must not run migrations, keeps using `codec.decode` directly.
59
+ * One synchronous loop and one `Result` for the batch: no Effect per document,
60
+ * and no allocation beyond the decoded array (plus a `Result` wrapper) on the
61
+ * happy path. Adapters lift the returned `Result` with `Effect.fromResult`
62
+ * once, at the end.
42
63
  */
43
- export declare const makeStoredDecode: <E extends FieldValues>(codec: JsonDocumentCodec<E>, jitM: StoreConfig<E>["jitM"]) => JsonDocumentCodec<E>["decode"];
64
+ export declare const decodeStoredMany: <A, B>(docs: readonly A[], decode: (doc: A) => Result.Result<B, S.SchemaError>) => Result.Result<B[], S.SchemaError>;
65
+ /** Decode an optional stored document; `None` short-circuits without decoding. */
66
+ export declare const decodeStoredOption: <A, B>(doc: Option.Option<A>, decode: (doc: A) => Result.Result<B, S.SchemaError>) => Result.Result<Option.Option<B>, S.SchemaError>;
44
67
  //# sourceMappingURL=jsonDocument.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jsonDocument.d.ts","sourceRoot":"","sources":["../../src/Store/jsonDocument.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAChE,OAAO,KAAK,CAAC,MAAM,mBAAmB,CAAA;AACtC,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGzE,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,WAAW;IACtD,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,oBAAoB,CAAC,CAAC,CAAC,CAAA;IAC1E,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,oBAAoB,CAAC,CAAC,CAAC,CAAA;CAC3E;AAYD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,SAAS,WAAW,WAAW,CAAC,CAAC,GAAG,KAAG,iBAAiB,CAAC,CAAC,CAkBhG,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,WAAW,SAC7C,iBAAiB,CAAC,CAAC,CAAC,QACrB,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAC3B,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAU/B,CAAA"}
1
+ {"version":3,"file":"jsonDocument.d.ts","sourceRoot":"","sources":["../../src/Store/jsonDocument.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAChE,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,CAAC,MAAM,mBAAmB,CAAA;AACtC,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,WAAW;IACtD,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,oBAAoB,CAAC,CAAC,CAAC,CAAA;IAC1E,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,oBAAoB,CAAC,CAAC,CAAC,CAAA;CAC3E;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,WAAW,IAAI,CAChD,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC,KACzB,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,CAAA;AAe1D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,SAAS,WAAW,WAAW,CAAC,CAAC,GAAG,KAAG,iBAAiB,CAAC,CAAC,CAkBhG,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,WAAW,UAC5C,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAC1B,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAC3B,YAAY,CAAC,CAAC,CAmBhB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAAE,CAAC,QAC7B,SAAS,CAAC,EAAE,UACV,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,KAClD,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAQlC,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAAE,CAAC,OAChC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,UACb,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,KAClD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,CACiD,CAAA"}
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoianNvbkRvY3VtZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL1N0b3JlL2pzb25Eb2N1bWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEtBQUssTUFBTSxNQUFNLG1CQUFtQixDQUFBO0FBQzNDLE9BQU8sS0FBSyxDQUFDLE1BQU0sbUJBQW1CLENBQUE7QUFFdEMsT0FBTyxLQUFLLE1BQU0sTUFBTSxlQUFlLENBQUE7QUFDdkMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sWUFBWSxDQUFBO0FBbUI3QyxNQUFNLFNBQVMsR0FBRyxDQUF3QixHQUE0QixFQUFFLEVBQUU7SUFDeEUsTUFBTSxFQUFFLEtBQUssRUFBRSxHQUFHLElBQUksRUFBRSxHQUFHLEdBQUcsQ0FBQTtJQUM5QixPQUFPLEVBQUUsSUFBSSxFQUFFLElBQVMsRUFBRSxLQUFLLEVBQUUsQ0FBQTtBQUNuQyxDQUFDLENBQUE7QUFFRCxNQUFNLFFBQVEsR0FBRyxDQUNmLElBQU8sRUFDUCxLQUF5QixFQUNBLEVBQUUsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFBO0FBRS9FLDBFQUEwRTtBQUMxRSxNQUFNLFNBQVMsR0FBRyxDQUF3QixNQUFhLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsQ0FBdUIsQ0FBQTtBQUVwSDs7Ozs7Ozs7Ozs7Ozs7O0dBZUc7QUFDSCxNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBRyxDQUF3QixNQUFjLEVBQXdCLEVBQUU7SUFDbkcsSUFBSSxNQUFNLEVBQUUsQ0FBQztRQUNYLE1BQU0sS0FBSyxHQUFHLFNBQVMsQ0FBSSxNQUFNLENBQUMsQ0FBQTtRQUNsQyxPQUFPO1lBQ0wsTUFBTSxFQUFFLENBQUMsR0FB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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-app/infra",
3
- "version": "4.0.0-beta.326",
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.326"
19
+ "effect-app": "4.0.0-beta.328"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@azure/cosmos": "^4.9.3",
@@ -9,7 +9,7 @@ import type { SupportedValues } from "effect-app/Store"
9
9
  import { assertUnreachable } from "effect-app/utils"
10
10
  import { InfraLogger } from "../../logger.ts"
11
11
  import { isRelationCheck } from "../codeFilter.ts"
12
- import { jsonifyFilter, type JsonLower, toJsonQueryValue } from "../utils.ts"
12
+ import { jsonifyFilter, type JsonLower } from "../utils.ts"
13
13
 
14
14
  export function logQuery(q: {
15
15
  query: string
@@ -64,9 +64,9 @@ export function buildWhereCosmosQuery3(
64
64
  limit?: number,
65
65
  json?: JsonLower
66
66
  ) {
67
- const toJson = json?.toJson ?? toJsonQueryValue
67
+ // defaultValues are already JSON-lowered at store construction; a second
68
+ // schema encode of an ISO Date string throws "Expected a valid Date".
68
69
  filter = (json?.jsonifyFilter ?? jsonifyFilter)(filter)
69
- defaultValues = toJson(defaultValues) as Record<string, unknown>
70
70
  const statement = (x: FilterR, i: number) => {
71
71
  if (x.path === idKey) {
72
72
  x = { ...x, path: "id" }
@@ -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
  )
@@ -6,7 +6,7 @@ import type { AggregateIrExpression, ComputedProjectionIrExpression, ComputedPro
6
6
  import { assertUnreachable } from "effect-app/utils"
7
7
  import { InfraLogger } from "../../logger.ts"
8
8
  import { isRelationCheck } from "../codeFilter.ts"
9
- import { jsonifyFilter, type JsonLower, toJsonQueryValue } from "../utils.ts"
9
+ import { jsonifyFilter, type JsonLower } from "../utils.ts"
10
10
 
11
11
  export interface SQLDialect {
12
12
  readonly jsonExtract: (path: string) => string
@@ -216,9 +216,9 @@ export function buildWhereSQLQuery(
216
216
  namespace?: string,
217
217
  json?: JsonLower
218
218
  ) {
219
- const toJson = json?.toJson ?? toJsonQueryValue
219
+ // defaultValues are already JSON-lowered at store construction; a second
220
+ // schema encode of an ISO Date string throws "Expected a valid Date".
220
221
  filter = (json?.jsonifyFilter ?? jsonifyFilter)(filter)
221
- defaultValues = toJson(defaultValues) as Record<string, unknown>
222
222
  const params: unknown[] = []
223
223
  let paramIndex = 1
224
224