@effect-app/infra 4.0.0-beta.327 → 4.0.0-beta.329
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 +19 -0
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +7 -6
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +4 -4
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +11 -10
- package/dist/Store/SQL/Pg.js +17 -16
- package/dist/Store/SQL.d.ts +9 -2
- package/dist/Store/SQL.d.ts.map +1 -1
- package/dist/Store/SQL.js +28 -29
- package/dist/Store/jsonDocument.d.ts +34 -11
- package/dist/Store/jsonDocument.d.ts.map +1 -1
- package/dist/Store/jsonDocument.js +53 -19
- package/dist/arbs.d.ts.map +1 -1
- package/dist/arbs.js +5 -4
- package/package.json +2 -2
- package/src/Store/Cosmos.ts +11 -6
- package/src/Store/Disk.ts +4 -3
- package/src/Store/Memory.ts +14 -9
- package/src/Store/SQL/Pg.ts +21 -21
- package/src/Store/SQL.ts +37 -37
- package/src/Store/jsonDocument.ts +73 -18
- package/src/arbs.ts +4 -3
- package/test/arbs-magnitude.test.ts +12 -0
- package/test/arbs.test.ts +20 -0
- package/test/repository-legacy-document.test.ts +96 -31
|
@@ -1,6 +1,8 @@
|
|
|
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
|
import { toJsonQueryValue } from "./utils.ts"
|
|
5
7
|
|
|
6
8
|
export interface JsonDocumentCodec<E extends FieldValues> {
|
|
@@ -8,6 +10,18 @@ export interface JsonDocumentCodec<E extends FieldValues> {
|
|
|
8
10
|
readonly decode: (doc: PersistenceModelType<E>) => PersistenceModelType<E>
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The store *read* boundary decode: a stored JSON document in, either the
|
|
15
|
+
* Encoded document or the `S.SchemaError` saying why it is not one.
|
|
16
|
+
*
|
|
17
|
+
* Synchronous on purpose: it runs once per document over whole result sets, so
|
|
18
|
+
* it must not allocate an Effect per row. Adapters lift the `Result` into their
|
|
19
|
+
* Effect channel once per operation (see {@link decodeStoredMany}).
|
|
20
|
+
*/
|
|
21
|
+
export type StoredDecode<E extends FieldValues> = (
|
|
22
|
+
doc: PersistenceModelType<E>
|
|
23
|
+
) => Result.Result<PersistenceModelType<E>, S.SchemaError>
|
|
24
|
+
|
|
11
25
|
const splitEtag = <E extends FieldValues>(doc: PersistenceModelType<E>) => {
|
|
12
26
|
const { _etag, ...rest } = doc
|
|
13
27
|
return { rest: rest as E, _etag }
|
|
@@ -18,6 +32,9 @@ const joinEtag = <E extends FieldValues>(
|
|
|
18
32
|
_etag: string | undefined
|
|
19
33
|
): PersistenceModelType<E> => (_etag === undefined ? rest : { ...rest, _etag })
|
|
20
34
|
|
|
35
|
+
/** JSON document ↔ Encoded document codec for a store's domain schema. */
|
|
36
|
+
const jsonCodec = <E extends FieldValues>(schema: S.Top) => S.toCodecJson(S.toEncoded(schema)) as S.Codec<E, S.Json>
|
|
37
|
+
|
|
21
38
|
/**
|
|
22
39
|
* Encoded document ↔ JSON document. Prefer `Schema.toCodecJson(toEncoded(schema))`
|
|
23
40
|
* when the store has a schema; otherwise lower Date/Map/Set structurally.
|
|
@@ -26,18 +43,17 @@ const joinEtag = <E extends FieldValues>(
|
|
|
26
43
|
* document that does not match the Encoded shape fails loudly rather than being
|
|
27
44
|
* read back half-decoded.
|
|
28
45
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* failure and throws here.
|
|
46
|
+
* This is the **write** path codec: `encode` on the way to the database, and
|
|
47
|
+
* `decode` to hand the just-written document back. The read path uses
|
|
48
|
+
* {@link makeStoredDecode}, which applies `jitM` first and reports a document it
|
|
49
|
+
* does not repair as a typed `S.SchemaError` instead of throwing.
|
|
34
50
|
*
|
|
35
51
|
* `_etag` is infrastructure metadata rather than part of the domain document,
|
|
36
52
|
* so it is split off before the codec runs and re-attached afterwards.
|
|
37
53
|
*/
|
|
38
54
|
export const makeJsonDocumentCodec = <E extends FieldValues>(schema?: S.Top): JsonDocumentCodec<E> => {
|
|
39
55
|
if (schema) {
|
|
40
|
-
const codec =
|
|
56
|
+
const codec = jsonCodec<E>(schema)
|
|
41
57
|
return {
|
|
42
58
|
encode: (doc) => {
|
|
43
59
|
const { rest, _etag } = splitEtag(doc)
|
|
@@ -58,31 +74,70 @@ export const makeJsonDocumentCodec = <E extends FieldValues>(schema?: S.Top): Js
|
|
|
58
74
|
/**
|
|
59
75
|
* The store *read* boundary: apply `jitM` to the raw JSON document - after the
|
|
60
76
|
* adapter merged `defaultValues` into it - and only then decode JSON→Encoded
|
|
61
|
-
* with the strict
|
|
77
|
+
* with the strict `Schema.toCodecJson(Schema.toEncoded(schema))` codec.
|
|
62
78
|
*
|
|
63
79
|
* This is the one place a legacy-shaped document can be repaired, and why
|
|
64
80
|
* `jitM` can fix things no schema decode could - an explicit `null` where a
|
|
65
81
|
* `Date` is expected, a key added after the document was written. It runs
|
|
66
|
-
* before any schema decode sees the document
|
|
67
|
-
*
|
|
82
|
+
* before any schema decode sees the document.
|
|
83
|
+
*
|
|
84
|
+
* Nothing here is lenient: a document `jitM` does not repair still fails. It
|
|
85
|
+
* fails as a typed `S.SchemaError` in a `Result` (via `S.decodeUnknownResult`)
|
|
86
|
+
* rather than by throwing, so adapters can turn it into `Effect.fail` and
|
|
87
|
+
* callers - `Repository.validateSample` in particular - can inspect it per
|
|
88
|
+
* document instead of losing the whole run to a defect.
|
|
68
89
|
*
|
|
69
90
|
* `_etag` is infrastructure metadata rather than part of the domain document,
|
|
70
91
|
* so it is split off first and `jitM` never sees it.
|
|
71
|
-
*
|
|
72
|
-
* Returns `codec.decode` unchanged when there is no `jitM`; the write path,
|
|
73
|
-
* which must not run migrations, keeps using `codec.decode` directly.
|
|
74
92
|
*/
|
|
75
93
|
export const makeStoredDecode = <E extends FieldValues>(
|
|
76
|
-
|
|
94
|
+
schema: StoreConfig<E>["schema"],
|
|
77
95
|
jitM: StoreConfig<E>["jitM"]
|
|
78
|
-
):
|
|
79
|
-
if (
|
|
96
|
+
): StoredDecode<E> => {
|
|
97
|
+
if (schema === undefined) {
|
|
98
|
+
// schemaless store: the stored document is already the Encoded document,
|
|
99
|
+
// so there is nothing to decode - only `jitM` to apply.
|
|
100
|
+
if (jitM === undefined) return Result.succeed
|
|
101
|
+
return (doc) => {
|
|
102
|
+
const { rest, _etag } = splitEtag(doc)
|
|
103
|
+
return Result.succeed(joinEtag(jitM(rest) as unknown as E, _etag))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const decode = S.decodeUnknownResult(jsonCodec<E>(schema))
|
|
80
107
|
return (doc) => {
|
|
81
108
|
const { rest, _etag } = splitEtag(doc)
|
|
82
109
|
// adapters type the stored document as the Encoded persistence model, but
|
|
83
110
|
// at this point it is still the raw JSON document that `jitM` is written
|
|
84
|
-
// against - and `jitM` returns JSON, which is what
|
|
85
|
-
const
|
|
86
|
-
return
|
|
111
|
+
// against - and `jitM` returns JSON, which is what the codec consumes.
|
|
112
|
+
const json = jitM === undefined ? rest : jitM(rest) as unknown as E
|
|
113
|
+
return Result.map(decode(json), (decoded) => joinEtag(decoded, _etag))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Decode a whole result set, failing fast on the first bad document.
|
|
119
|
+
*
|
|
120
|
+
* One synchronous loop and one `Result` for the batch: no Effect per document,
|
|
121
|
+
* and no allocation beyond the decoded array (plus a `Result` wrapper) on the
|
|
122
|
+
* happy path. Adapters lift the returned `Result` with `Effect.fromResult`
|
|
123
|
+
* once, at the end.
|
|
124
|
+
*/
|
|
125
|
+
export const decodeStoredMany = <A, B>(
|
|
126
|
+
docs: readonly A[],
|
|
127
|
+
decode: (doc: A) => Result.Result<B, S.SchemaError>
|
|
128
|
+
): Result.Result<B[], S.SchemaError> => {
|
|
129
|
+
const decoded: B[] = []
|
|
130
|
+
for (const doc of docs) {
|
|
131
|
+
const result = decode(doc)
|
|
132
|
+
if (Result.isFailure(result)) return Result.fail(result.failure)
|
|
133
|
+
decoded.push(result.success)
|
|
87
134
|
}
|
|
135
|
+
return Result.succeed(decoded)
|
|
88
136
|
}
|
|
137
|
+
|
|
138
|
+
/** Decode an optional stored document; `None` short-circuits without decoding. */
|
|
139
|
+
export const decodeStoredOption = <A, B>(
|
|
140
|
+
doc: Option.Option<A>,
|
|
141
|
+
decode: (doc: A) => Result.Result<B, S.SchemaError>
|
|
142
|
+
): Result.Result<Option.Option<B>, S.SchemaError> =>
|
|
143
|
+
Option.isNone(doc) ? Result.succeed(Option.none()) : Result.map(decode(doc.value), Option.some)
|
package/src/arbs.ts
CHANGED
|
@@ -23,13 +23,14 @@ export function generateFromArbitrary<T>(arb: S.Arbitrary<T>) {
|
|
|
23
23
|
return generate(arb(FastCheck))
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
26
|
+
// Native `sampleEffect({ count: 1 })` always uses attempt 0 of the master seed (edge-biased).
|
|
27
|
+
// Integer `seed + n` stayed in that bucket and collided for StringId. Mix a per-call suffix
|
|
28
|
+
// so each call is a jump-before-toss, like successive `generate(rnd)` draws.
|
|
28
29
|
let schemaSamples = 0
|
|
29
30
|
|
|
30
31
|
export function generateFromSchema<S extends S.Constraint>(schema: S) {
|
|
31
32
|
const samples = Effect.runSync(
|
|
32
|
-
Arbitrary.sampleEffect(Arbitrary.schema(schema), { count: 1, seed: seed
|
|
33
|
+
Arbitrary.sampleEffect(Arbitrary.schema(schema), { count: 1, seed: `${seed}:${schemaSamples++}` })
|
|
33
34
|
)
|
|
34
35
|
const value = samples[0]
|
|
35
36
|
if (value === undefined) {
|
|
@@ -14,8 +14,20 @@ describe("generateFromSchema number magnitudes", () => {
|
|
|
14
14
|
}
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
it("keeps Finite samples realistic", () => {
|
|
18
|
+
const schema = S.Struct({ amount: S.Finite, weight: S.Finite })
|
|
19
|
+
for (let i = 0; i < 200; i++) {
|
|
20
|
+
const { amount, weight } = generateFromSchema(schema).value
|
|
21
|
+
expect(Number.isFinite(amount)).toBe(true)
|
|
22
|
+
expect(Number.isFinite(weight)).toBe(true)
|
|
23
|
+
expect(Math.max(Math.abs(amount), Math.abs(weight))).toBeLessThanOrEqual(1_000_000)
|
|
24
|
+
expect(Number.isFinite(amount * weight)).toBe(true)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
17
28
|
it("does not change the JSON Schema of the number schemas", () => {
|
|
18
29
|
expect(S.toJsonSchemaDocument(S.PositiveNumber).schema).not.toHaveProperty("maximum")
|
|
19
30
|
expect(S.toJsonSchemaDocument(S.NonNegativeNumber).schema).not.toHaveProperty("maximum")
|
|
31
|
+
expect(S.toJsonSchemaDocument(S.Finite).schema).not.toHaveProperty("maximum")
|
|
20
32
|
})
|
|
21
33
|
})
|
package/test/arbs.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
+
import { RequestId } from "effect-app/ids"
|
|
2
3
|
import * as S from "effect-app/Schema"
|
|
3
4
|
import { generateFromSchema } from "../src/arbs.js"
|
|
4
5
|
|
|
@@ -18,4 +19,23 @@ describe("generateFromSchema", () => {
|
|
|
18
19
|
expect(S.is(schema)(generateFromSchema(schema).value)).toBe(true)
|
|
19
20
|
}
|
|
20
21
|
})
|
|
22
|
+
|
|
23
|
+
it("successive StringId samples are unique (count:1 jump, not attempt-0 edge strings)", () => {
|
|
24
|
+
const Item = S.Struct({ id: S.StringId, amount: S.Finite })
|
|
25
|
+
const ids = Array.from({ length: 20 }, () => generateFromSchema(Item).value.id)
|
|
26
|
+
expect(new Set(ids).size).toBe(20)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("successive RequestId samples are unique", () => {
|
|
30
|
+
const ids = Array.from({ length: 20 }, () => generateFromSchema(RequestId).value)
|
|
31
|
+
expect(new Set(ids).size).toBe(20)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("samples Url as https URLs", () => {
|
|
35
|
+
for (let i = 0; i < 10; i++) {
|
|
36
|
+
const url = generateFromSchema(S.Url).value
|
|
37
|
+
expect(url.startsWith("https://")).toBe(true)
|
|
38
|
+
expect(S.is(S.Url)(url)).toBe(true)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
21
41
|
})
|
|
@@ -9,8 +9,11 @@ import * as S from "effect-app/Schema"
|
|
|
9
9
|
import { setupRequestContextFromCurrent } from "effect-app/setupRequest"
|
|
10
10
|
import type { JsonRecord } from "effect-app/Store"
|
|
11
11
|
import { StoreMaker } from "effect-app/Store"
|
|
12
|
+
import * as Cause from "effect/Cause"
|
|
13
|
+
import * as Exit from "effect/Exit"
|
|
14
|
+
import * as Result from "effect/Result"
|
|
12
15
|
import { describe, expect, it } from "vitest"
|
|
13
|
-
import {
|
|
16
|
+
import { decodeStoredMany, decodeStoredOption, makeStoredDecode } from "../src/Store/jsonDocument.js"
|
|
14
17
|
import { makeMemoryStoreInt } from "../src/Store/Memory.js"
|
|
15
18
|
import { makeJsonLower } from "../src/Store/utils.js"
|
|
16
19
|
|
|
@@ -39,12 +42,15 @@ const LegacyDocStoreLive = (docs: readonly Record<string, unknown>[]) =>
|
|
|
39
42
|
makeJsonLower(config)
|
|
40
43
|
),
|
|
41
44
|
(store) => {
|
|
42
|
-
const decode = makeStoredDecode<any>(
|
|
45
|
+
const decode = makeStoredDecode<any>(config?.schema, config?.jitM)
|
|
43
46
|
return {
|
|
44
47
|
...store,
|
|
45
|
-
all: Effect.
|
|
46
|
-
find: (id: any) =>
|
|
47
|
-
|
|
48
|
+
all: Effect.flatMap(store.all, (rows) => Effect.fromResult(decodeStoredMany(rows, decode))),
|
|
49
|
+
find: (id: any) =>
|
|
50
|
+
Effect.flatMap(store.find(id), (doc) => Effect.fromResult(decodeStoredOption(doc, decode))),
|
|
51
|
+
filter: (f: any) =>
|
|
52
|
+
Effect.flatMap(store.filter(f), (rows: any[]) =>
|
|
53
|
+
f.select ? Effect.succeed(rows) : Effect.fromResult(decodeStoredMany(rows, decode)))
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
)) as any
|
|
@@ -63,6 +69,12 @@ class Shop extends S.Class<Shop>("LegacyDocShop")({
|
|
|
63
69
|
// `vatRate` was added later; stored documents from before that do not have it.
|
|
64
70
|
const jitM = (json: JsonRecord): JsonRecord => "vatRate" in json ? json : { ...json, vatRate: 19 }
|
|
65
71
|
|
|
72
|
+
/** does *not* repair `vatRate`, so `legacyDoc` cannot decode at the store boundary */
|
|
73
|
+
const jitMThatDoesNotRepair = (json: JsonRecord): JsonRecord => ({
|
|
74
|
+
...json,
|
|
75
|
+
updatedAt: json["updatedAt"] ?? "2024-06-03T00:00:00.000Z"
|
|
76
|
+
})
|
|
77
|
+
|
|
66
78
|
const legacyDoc = {
|
|
67
79
|
id: "shop-legacy",
|
|
68
80
|
name: "Legacy Shop",
|
|
@@ -270,53 +282,106 @@ describe("repository reads of legacy documents", () => {
|
|
|
270
282
|
/**
|
|
271
283
|
* The store boundary decode is deliberately **strict**: `jitM` is the one and
|
|
272
284
|
* only place a legacy document can be repaired, and a document it does not
|
|
273
|
-
* repair must fail
|
|
274
|
-
*
|
|
275
|
-
*
|
|
285
|
+
* repair must fail rather than be read back half-decoded. This test pins that
|
|
286
|
+
* behaviour down on purpose - do not "fix" it by making the decode lenient.
|
|
287
|
+
*
|
|
288
|
+
* It fails as a *typed* `S.SchemaError` in the store's error channel (it used
|
|
289
|
+
* to throw out of `decodeSync` as a defect), which is what lets
|
|
290
|
+
* `validateSample` report the document instead of losing the whole run.
|
|
276
291
|
*/
|
|
277
|
-
it("a document jitM does not repair fails
|
|
278
|
-
|
|
292
|
+
it("a document jitM does not repair fails as a typed SchemaError at the store boundary", () =>
|
|
293
|
+
Effect
|
|
279
294
|
.gen(function*() {
|
|
295
|
+
const { make } = yield* StoreMaker
|
|
280
296
|
// `vatRate` is still missing after this jitM ran: it only touches `updatedAt`
|
|
281
|
-
const
|
|
282
|
-
|
|
297
|
+
const store = yield* make<"id", any>("LegacyDocShop", "id", undefined, {
|
|
298
|
+
partitionValue: () => "primary",
|
|
299
|
+
schema: Shop,
|
|
300
|
+
jitM: jitMThatDoesNotRepair
|
|
283
301
|
})
|
|
284
302
|
|
|
285
|
-
|
|
303
|
+
// `Effect.result` only catches typed failures - a defect would reject here
|
|
304
|
+
const result = yield* store.all.pipe(Effect.result)
|
|
305
|
+
|
|
306
|
+
expect(Result.isFailure(result)).toBe(true)
|
|
307
|
+
if (!Result.isFailure(result)) return
|
|
308
|
+
const failure = result.failure
|
|
309
|
+
expect(S.isSchemaError(failure)).toBe(true)
|
|
310
|
+
expect((failure as S.SchemaError)._tag).toBe("SchemaError")
|
|
311
|
+
// the strict `toCodecJson(toEncoded(schema))` decode still reports the
|
|
312
|
+
// key jitM failed to add, with the path to it
|
|
313
|
+
expect((failure as S.SchemaError).message).toContain("Missing key")
|
|
314
|
+
expect((failure as S.SchemaError).message).toContain("vatRate")
|
|
286
315
|
})
|
|
287
316
|
.pipe(
|
|
288
317
|
Effect.provide(TestLive),
|
|
289
318
|
setupRequestContextFromCurrent(),
|
|
290
319
|
Effect.scoped,
|
|
291
320
|
Effect.runPromise
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
321
|
+
))
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* ...and the *public* repository surface is unchanged: `Repository.find`/`all`
|
|
325
|
+
* declare only `DatabaseError`, so the store's `SchemaError` becomes a defect
|
|
326
|
+
* when the public members are produced.
|
|
327
|
+
*/
|
|
328
|
+
it("the same document still dies on a public repository read", () =>
|
|
329
|
+
Effect
|
|
330
|
+
.gen(function*() {
|
|
331
|
+
const repo = yield* makeRepo("LegacyDocShop", Shop, { jitM: jitMThatDoesNotRepair })
|
|
332
|
+
|
|
333
|
+
const exit = yield* repo.all.pipe(Effect.exit)
|
|
334
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
335
|
+
if (Exit.isFailure(exit)) {
|
|
336
|
+
expect(Cause.hasDies(exit.cause)).toBe(true)
|
|
337
|
+
expect(Cause.hasFails(exit.cause)).toBe(false)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// a defect is not catchable as a typed failure
|
|
341
|
+
const notCaught = yield* repo.find("shop-legacy").pipe(Effect.result, Effect.exit)
|
|
342
|
+
expect(Exit.isFailure(notCaught)).toBe(true)
|
|
343
|
+
})
|
|
344
|
+
.pipe(
|
|
345
|
+
Effect.provide(TestLive),
|
|
346
|
+
setupRequestContextFromCurrent(),
|
|
347
|
+
Effect.scoped,
|
|
348
|
+
Effect.runPromise
|
|
349
|
+
))
|
|
350
|
+
|
|
351
|
+
it("validateSample reports a document that fails at the store boundary and keeps going", () =>
|
|
352
|
+
Effect
|
|
353
|
+
.gen(function*() {
|
|
354
|
+
const repo = yield* makeRepo("LegacyDocShop", Shop, { jitM: jitMThatDoesNotRepair })
|
|
355
|
+
|
|
356
|
+
const result = yield* repo.validateSample({ percentage: 1.0 })
|
|
357
|
+
|
|
358
|
+
expect(result.total).toBe(2)
|
|
359
|
+
expect(result.sampled).toBe(2)
|
|
360
|
+
// `shop-current` decodes, `shop-legacy` does not
|
|
361
|
+
expect(result.valid).toBe(1)
|
|
362
|
+
expect(result.errors).toHaveLength(1)
|
|
363
|
+
const error = result.errors[0]!
|
|
364
|
+
expect(error.id).toBe("shop-legacy")
|
|
365
|
+
expect(S.isSchemaError(error.error)).toBe(true)
|
|
366
|
+
})
|
|
367
|
+
.pipe(
|
|
368
|
+
Effect.provide(TestLive),
|
|
369
|
+
setupRequestContextFromCurrent(),
|
|
370
|
+
Effect.scoped,
|
|
371
|
+
Effect.runPromise
|
|
372
|
+
))
|
|
308
373
|
|
|
309
374
|
it("_etag is not visible to jitM and survives the round trip", () => {
|
|
310
375
|
const seen: JsonRecord[] = []
|
|
311
376
|
const decode = makeStoredDecode<any>(
|
|
312
|
-
|
|
377
|
+
Shop,
|
|
313
378
|
(json) => {
|
|
314
379
|
seen.push(json)
|
|
315
380
|
return { ...json, vatRate: 19 }
|
|
316
381
|
}
|
|
317
382
|
)
|
|
318
383
|
|
|
319
|
-
const decoded = decode({ ...legacyDoc, _etag: "etag-1" } as any)
|
|
384
|
+
const decoded = Result.getOrThrow(decode({ ...legacyDoc, _etag: "etag-1" } as any))
|
|
320
385
|
|
|
321
386
|
// jitM sees the document without the infra metadata
|
|
322
387
|
expect(seen).toHaveLength(1)
|