@entifix/testing-unit 0.1.3 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-ports.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/transaction-ports.contract.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,eAAe,EAEf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAO/B;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACtC,MAAM,MAAM,EACZ,iBAAiB,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,KACxD,IAqDF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,GAC1C,MAAM,MAAM,EACZ,qBAAqB,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,KACpE,IA6BF,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,QAAQ,CAAC;IACd,oEAAoE;IACpE,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8DAA8D;IAC9D,SAAS,IAAI,WAAW,EAAE,CAAC;CAC5B;AAwCD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GACnC,MAAM,MAAM,EACZ,aAAa,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,KAC5E,IAkDF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,MAAM,EACZ,WAAW,CAAC,QAAQ,EAAE,MAAM,KAAK,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAC5E,IAyCF,CAAC"}
1
+ {"version":3,"file":"transaction-ports.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/transaction-ports.contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,eAAe,EAEf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAqB/B;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACtC,MAAM,MAAM,EACZ,iBAAiB,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,KACxD,IAqDF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,GAC1C,MAAM,MAAM,EACZ,qBAAqB,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,KACpE,IA6BF,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,QAAQ,CAAC;IACd,oEAAoE;IACpE,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8DAA8D;IAC9D,SAAS,IAAI,WAAW,EAAE,CAAC;CAC5B;AAwCD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GACnC,MAAM,MAAM,EACZ,aAAa,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,KAC5E,IAkDF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,MAAM,EACZ,WAAW,CAAC,QAAQ,EAAE,MAAM,KAAK,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAC5E,IAyCF,CAAC"}
@@ -1,6 +1,12 @@
1
- import { it as effectIt } from '@effect/vitest';
2
- import { Duration, Effect, Exit, Fiber, TestClock } from 'effect';
1
+ import { Duration, Effect, Exit, Fiber } from 'effect';
2
+ import { TestClock } from 'effect/testing';
3
3
  import { describe, expect, it } from 'vitest';
4
+ /**
5
+ * Runs an Effect test on a `TestClock`, so a retry budget measured in seconds
6
+ * costs none of the wall clock.
7
+ */ const effectIt = {
8
+ effect: (name, body)=>it(name, ()=>Effect.runPromise(body().pipe(Effect.provide(TestClock.layer()), Effect.scoped)))
9
+ };
4
10
  const run = (effect)=>Effect.runPromise(effect);
5
11
  /**
6
12
  * What every {@link LockService} must guarantee: exclusivity while held,
@@ -22,7 +28,7 @@ const run = (effect)=>Effect.runPromise(effect);
22
28
  effectIt.effect('refuses a key that is already held', ()=>Effect.gen(function*() {
23
29
  const locks = yield* Effect.promise(async ()=>makeLockService());
24
30
  yield* locks.acquire('contract:taken');
25
- const fiber = yield* Effect.fork(locks.acquire('contract:taken'));
31
+ const fiber = yield* Effect.forkChild(locks.acquire('contract:taken'));
26
32
  yield* TestClock.adjust(Duration.minutes(1));
27
33
  const exit = yield* Fiber.await(fiber);
28
34
  expect(Exit.isFailure(exit)).toBe(true);
@@ -42,7 +48,7 @@ const run = (effect)=>Effect.runPromise(effect);
42
48
  token: 'not-mine'
43
49
  });
44
50
  // Still held, so a fresh acquisition must fail.
45
- const fiber = yield* Effect.fork(locks.acquire('contract:stolen'));
51
+ const fiber = yield* Effect.forkChild(locks.acquire('contract:stolen'));
46
52
  yield* TestClock.adjust(Duration.minutes(1));
47
53
  const exit = yield* Fiber.await(fiber);
48
54
  expect(Exit.isFailure(exit)).toBe(true);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/contracts/transaction-ports.contract.ts"],"sourcesContent":["import { it as effectIt } from '@effect/vitest';\nimport type { DomainEvent } from '@entifix/core';\nimport type {\n EventBus,\n LockService,\n SequenceService,\n Subscription,\n TransactionInbox,\n} from '@entifix/transactions';\nimport { Duration, Effect, Exit, Fiber, TestClock } from 'effect';\nimport { describe, expect, it } from 'vitest';\n\nconst run = <TValue, TError>(effect: Effect.Effect<TValue, TError>) =>\n Effect.runPromise(effect);\n\n/**\n * What every {@link LockService} must guarantee: exclusivity while held,\n * availability after release, and a release that only frees the caller's own\n * lock. The last one is why `release` takes a handle rather than a key — a lock\n * that lapsed and was re-acquired by someone else must not be freed by the\n * previous holder.\n */\nexport const describeLockServiceContract = (\n name: string,\n makeLockService: () => LockService | Promise<LockService>,\n): void => {\n describe(`LockService contract: ${name}`, () => {\n it('acquires an uncontended key', async () => {\n const locks = await makeLockService();\n\n const handle = await run(locks.acquire('contract:free'));\n\n expect(handle.key).toBe('contract:free');\n expect(handle.token).toBeTruthy();\n });\n\n // An implementation may retry before giving up (the Redis adapter waits out\n // a bounded budget), so this runs on a TestClock: the assertion is about\n // exclusivity, and it should not cost seconds of wall clock to make.\n effectIt.effect('refuses a key that is already held', () =>\n Effect.gen(function* () {\n const locks = yield* Effect.promise(async () => makeLockService());\n yield* locks.acquire('contract:taken');\n\n const fiber = yield* Effect.fork(locks.acquire('contract:taken'));\n yield* TestClock.adjust(Duration.minutes(1));\n const exit = yield* Fiber.await(fiber);\n\n expect(Exit.isFailure(exit)).toBe(true);\n }),\n );\n\n it('makes the key available again after release', async () => {\n const locks = await makeLockService();\n const handle = await run(locks.acquire('contract:cycle'));\n\n await run(locks.release(handle));\n const reacquired = await run(locks.acquire('contract:cycle'));\n\n expect(reacquired.key).toBe('contract:cycle');\n });\n\n effectIt.effect('ignores a release whose token does not match', () =>\n Effect.gen(function* () {\n const locks = yield* Effect.promise(async () => makeLockService());\n const handle = yield* locks.acquire('contract:stolen');\n\n yield* locks.release({ key: handle.key, token: 'not-mine' });\n\n // Still held, so a fresh acquisition must fail.\n const fiber = yield* Effect.fork(locks.acquire('contract:stolen'));\n yield* TestClock.adjust(Duration.minutes(1));\n const exit = yield* Fiber.await(fiber);\n\n expect(Exit.isFailure(exit)).toBe(true);\n }),\n );\n });\n};\n\n/**\n * What every {@link SequenceService} must guarantee: strictly increasing values\n * per name, never repeating, with names independent of one another. This is the\n * property incremental codes rest on.\n */\nexport const describeSequenceServiceContract = (\n name: string,\n makeSequenceService: () => SequenceService | Promise<SequenceService>,\n): void => {\n describe(`SequenceService contract: ${name}`, () => {\n it('starts at 1', async () => {\n const sequences = await makeSequenceService();\n\n expect(await run(sequences.next('contract:fresh'))).toBe(1);\n });\n\n it('never returns the same value twice for a name', async () => {\n const sequences = await makeSequenceService();\n\n const drawn = [\n await run(sequences.next('contract:codes')),\n await run(sequences.next('contract:codes')),\n await run(sequences.next('contract:codes')),\n ];\n\n expect(drawn).toEqual([1, 2, 3]);\n expect(new Set(drawn).size).toBe(drawn.length);\n });\n\n it('keeps separate names independent', async () => {\n const sequences = await makeSequenceService();\n await run(sequences.next('contract:a'));\n await run(sequences.next('contract:a'));\n\n expect(await run(sequences.next('contract:b'))).toBe(1);\n });\n });\n};\n\nexport interface EventBusContractHarness {\n bus: EventBus;\n /** Delivers `event` to whatever subscribed, as the broker would. */\n deliver(event: DomainEvent): Promise<void>;\n /** Everything the bus published, decoded back into events. */\n published(): DomainEvent[];\n}\n\n/** Every subscription in this contract, unless a test says otherwise. */\nconst ANY_WIDGET_EVENT = 'contract.widget.*';\n\n/**\n * A `work` subscription for `pattern`. The mode and the ceiling are broker\n * facts with no bearing on routing, so a test about routing states them once\n * here rather than repeating them at every call.\n */\nconst on = (pattern: string): Subscription => ({\n slice: 'contract',\n pattern,\n mode: 'work',\n maxAttempts: 5,\n});\n\n/**\n * A handler that records what it is given. Shared by both routing tests so the\n * \"nothing was delivered\" case asserts on the *same* handler the positive case\n * proves works — otherwise it could pass against a handler that was simply\n * never wired up.\n */\nconst collectInto = (sink: DomainEvent[]) => (event: DomainEvent) =>\n Effect.sync(() => {\n sink.push(event);\n });\n\nconst anEvent = (\n id: string,\n name = 'contract.widget.created',\n): DomainEvent => ({\n name,\n id,\n source: 'contract-slice',\n at: new Date('2026-01-01T00:00:00.000Z').toISOString(),\n correlationId: id,\n data: { entity: 'contract-widget' },\n});\n\n/**\n * What every {@link EventBus} must guarantee: what goes out comes back\n * unchanged, subscribers receive deliveries, and a subscriber receives only what\n * its pattern matches. The transports differ wildly (an array here, a topic\n * exchange in RabbitMQ) — the observable contract does not.\n */\nexport const describeEventBusContract = (\n name: string,\n makeHarness: () => EventBusContractHarness | Promise<EventBusContractHarness>,\n): void => {\n describe(`EventBus contract: ${name}`, () => {\n it('publishes an event verbatim', async () => {\n const harness = await makeHarness();\n const event = anEvent('tx-1');\n\n await run(harness.bus.publish(event));\n\n expect(harness.published()).toEqual([event]);\n });\n\n it('delivers published events to a subscriber', async () => {\n const harness = await makeHarness();\n const received: DomainEvent[] = [];\n await run(\n harness.bus.subscribe(on(ANY_WIDGET_EVENT), collectInto(received)),\n );\n\n await harness.deliver(anEvent('tx-2'));\n\n expect(received.map(event => event.id)).toEqual(['tx-2']);\n });\n\n // Routing is the transport's job. A bus that hands every subscriber every\n // event lets a consumer pass while receiving traffic it never bound to —\n // which is precisely the fault the fanout exchange had.\n it('delivers nothing to a subscriber whose pattern does not match', async () => {\n const harness = await makeHarness();\n const received: DomainEvent[] = [];\n await run(\n harness.bus.subscribe(on('contract.other.*'), collectInto(received)),\n );\n\n await harness.deliver(anEvent('tx-5'));\n\n expect(received).toEqual([]);\n });\n\n it('preserves publication order', async () => {\n const harness = await makeHarness();\n\n await run(harness.bus.publish(anEvent('tx-3')));\n await run(harness.bus.publish(anEvent('tx-4')));\n\n expect(harness.published().map(event => event.id)).toEqual([\n 'tx-3',\n 'tx-4',\n ]);\n });\n });\n};\n\n/**\n * What every {@link TransactionInbox} must guarantee: a first claim is granted,\n * every repeat of it is refused, and one consumer's claim says nothing about\n * another's.\n *\n * That last one is the property with teeth. Two consumers legitimately process\n * the same event — the saga tracker's fold and the SSE hub both bind\n * `transaction.*` — so an implementation keyed on `eventId` alone passes the\n * first two cases and silently starves every consumer but the first. It looks\n * like exactly-once from inside one consumer, which is why it is asserted here\n * rather than left to each adapter's own spec.\n */\nexport const describeTransactionInboxContract = (\n name: string,\n makeInbox: (consumer: string) => TransactionInbox | Promise<TransactionInbox>,\n): void => {\n describe(`TransactionInbox contract: ${name}`, () => {\n it('grants a first claim', async () => {\n const inbox = await makeInbox('contract.consumer');\n\n expect(await run(inbox.claim('tx-1:completed'))).toBe('claimed');\n });\n\n it('refuses a redelivery of the same event', async () => {\n const inbox = await makeInbox('contract.consumer');\n await run(inbox.claim('tx-2:completed'));\n\n expect(await run(inbox.claim('tx-2:completed'))).toBe('duplicate');\n });\n\n it('refuses every repeat, not only the second', async () => {\n const inbox = await makeInbox('contract.consumer');\n await run(inbox.claim('tx-3:completed'));\n\n expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');\n expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');\n });\n\n it('claims each step of one transaction separately', async () => {\n // `<transactionId>:<step>`, never the transaction id alone: one\n // transaction emits up to three messages, and keying on the correlation\n // id would make `completed` read as a redelivery of `accepted`.\n const inbox = await makeInbox('contract.consumer');\n\n expect(await run(inbox.claim('tx-4:accepted'))).toBe('claimed');\n expect(await run(inbox.claim('tx-4:completed'))).toBe('claimed');\n });\n\n it('does not let one consumer consume another consumer’s claim', async () => {\n const first = await makeInbox('contract.first');\n const second = await makeInbox('contract.second');\n await run(first.claim('tx-5:completed'));\n\n expect(await run(second.claim('tx-5:completed'))).toBe('claimed');\n });\n });\n};\n"],"names":["it","effectIt","Duration","Effect","Exit","Fiber","TestClock","describe","expect","run","effect","runPromise","describeLockServiceContract","name","makeLockService","locks","handle","acquire","key","toBe","token","toBeTruthy","gen","promise","fiber","fork","adjust","minutes","exit","await","isFailure","release","reacquired","describeSequenceServiceContract","makeSequenceService","sequences","next","drawn","toEqual","Set","size","length","ANY_WIDGET_EVENT","on","pattern","slice","mode","maxAttempts","collectInto","sink","event","sync","push","anEvent","id","source","at","Date","toISOString","correlationId","data","entity","describeEventBusContract","makeHarness","harness","bus","publish","published","received","subscribe","deliver","map","describeTransactionInboxContract","makeInbox","inbox","claim","first","second"],"mappings":"AAAA,SAASA,MAAMC,QAAQ,QAAQ,iBAAiB;AAShD,SAASC,QAAQ,EAAEC,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEC,SAAS,QAAQ,SAAS;AAClE,SAASC,QAAQ,EAAEC,MAAM,EAAER,EAAE,QAAQ,SAAS;AAE9C,MAAMS,MAAM,CAAiBC,SAC3BP,OAAOQ,UAAU,CAACD;AAEpB;;;;;;CAMC,GACD,OAAO,MAAME,8BAA8B,CACzCC,MACAC;IAEAP,SAAS,CAAC,sBAAsB,EAAEM,MAAM,EAAE;QACxCb,GAAG,+BAA+B;YAChC,MAAMe,QAAQ,MAAMD;YAEpB,MAAME,SAAS,MAAMP,IAAIM,MAAME,OAAO,CAAC;YAEvCT,OAAOQ,OAAOE,GAAG,EAAEC,IAAI,CAAC;YACxBX,OAAOQ,OAAOI,KAAK,EAAEC,UAAU;QACjC;QAEA,4EAA4E;QAC5E,yEAAyE;QACzE,qEAAqE;QACrEpB,SAASS,MAAM,CAAC,sCAAsC,IACpDP,OAAOmB,GAAG,CAAC;gBACT,MAAMP,QAAQ,OAAOZ,OAAOoB,OAAO,CAAC,UAAYT;gBAChD,OAAOC,MAAME,OAAO,CAAC;gBAErB,MAAMO,QAAQ,OAAOrB,OAAOsB,IAAI,CAACV,MAAME,OAAO,CAAC;gBAC/C,OAAOX,UAAUoB,MAAM,CAACxB,SAASyB,OAAO,CAAC;gBACzC,MAAMC,OAAO,OAAOvB,MAAMwB,KAAK,CAACL;gBAEhChB,OAAOJ,KAAK0B,SAAS,CAACF,OAAOT,IAAI,CAAC;YACpC;QAGFnB,GAAG,+CAA+C;YAChD,MAAMe,QAAQ,MAAMD;YACpB,MAAME,SAAS,MAAMP,IAAIM,MAAME,OAAO,CAAC;YAEvC,MAAMR,IAAIM,MAAMgB,OAAO,CAACf;YACxB,MAAMgB,aAAa,MAAMvB,IAAIM,MAAME,OAAO,CAAC;YAE3CT,OAAOwB,WAAWd,GAAG,EAAEC,IAAI,CAAC;QAC9B;QAEAlB,SAASS,MAAM,CAAC,gDAAgD,IAC9DP,OAAOmB,GAAG,CAAC;gBACT,MAAMP,QAAQ,OAAOZ,OAAOoB,OAAO,CAAC,UAAYT;gBAChD,MAAME,SAAS,OAAOD,MAAME,OAAO,CAAC;gBAEpC,OAAOF,MAAMgB,OAAO,CAAC;oBAAEb,KAAKF,OAAOE,GAAG;oBAAEE,OAAO;gBAAW;gBAE1D,gDAAgD;gBAChD,MAAMI,QAAQ,OAAOrB,OAAOsB,IAAI,CAACV,MAAME,OAAO,CAAC;gBAC/C,OAAOX,UAAUoB,MAAM,CAACxB,SAASyB,OAAO,CAAC;gBACzC,MAAMC,OAAO,OAAOvB,MAAMwB,KAAK,CAACL;gBAEhChB,OAAOJ,KAAK0B,SAAS,CAACF,OAAOT,IAAI,CAAC;YACpC;IAEJ;AACF,EAAE;AAEF;;;;CAIC,GACD,OAAO,MAAMc,kCAAkC,CAC7CpB,MACAqB;IAEA3B,SAAS,CAAC,0BAA0B,EAAEM,MAAM,EAAE;QAC5Cb,GAAG,eAAe;YAChB,MAAMmC,YAAY,MAAMD;YAExB1B,OAAO,MAAMC,IAAI0B,UAAUC,IAAI,CAAC,oBAAoBjB,IAAI,CAAC;QAC3D;QAEAnB,GAAG,iDAAiD;YAClD,MAAMmC,YAAY,MAAMD;YAExB,MAAMG,QAAQ;gBACZ,MAAM5B,IAAI0B,UAAUC,IAAI,CAAC;gBACzB,MAAM3B,IAAI0B,UAAUC,IAAI,CAAC;gBACzB,MAAM3B,IAAI0B,UAAUC,IAAI,CAAC;aAC1B;YAED5B,OAAO6B,OAAOC,OAAO,CAAC;gBAAC;gBAAG;gBAAG;aAAE;YAC/B9B,OAAO,IAAI+B,IAAIF,OAAOG,IAAI,EAAErB,IAAI,CAACkB,MAAMI,MAAM;QAC/C;QAEAzC,GAAG,oCAAoC;YACrC,MAAMmC,YAAY,MAAMD;YACxB,MAAMzB,IAAI0B,UAAUC,IAAI,CAAC;YACzB,MAAM3B,IAAI0B,UAAUC,IAAI,CAAC;YAEzB5B,OAAO,MAAMC,IAAI0B,UAAUC,IAAI,CAAC,gBAAgBjB,IAAI,CAAC;QACvD;IACF;AACF,EAAE;AAUF,uEAAuE,GACvE,MAAMuB,mBAAmB;AAEzB;;;;CAIC,GACD,MAAMC,KAAK,CAACC,UAAmC,CAAA;QAC7CC,OAAO;QACPD;QACAE,MAAM;QACNC,aAAa;IACf,CAAA;AAEA;;;;;CAKC,GACD,MAAMC,cAAc,CAACC,OAAwB,CAACC,QAC5C/C,OAAOgD,IAAI,CAAC;YACVF,KAAKG,IAAI,CAACF;QACZ;AAEF,MAAMG,UAAU,CACdC,IACAzC,OAAO,yBAAyB,GACf,CAAA;QACjBA;QACAyC;QACAC,QAAQ;QACRC,IAAI,IAAIC,KAAK,4BAA4BC,WAAW;QACpDC,eAAeL;QACfM,MAAM;YAAEC,QAAQ;QAAkB;IACpC,CAAA;AAEA;;;;;CAKC,GACD,OAAO,MAAMC,2BAA2B,CACtCjD,MACAkD;IAEAxD,SAAS,CAAC,mBAAmB,EAAEM,MAAM,EAAE;QACrCb,GAAG,+BAA+B;YAChC,MAAMgE,UAAU,MAAMD;YACtB,MAAMb,QAAQG,QAAQ;YAEtB,MAAM5C,IAAIuD,QAAQC,GAAG,CAACC,OAAO,CAAChB;YAE9B1C,OAAOwD,QAAQG,SAAS,IAAI7B,OAAO,CAAC;gBAACY;aAAM;QAC7C;QAEAlD,GAAG,6CAA6C;YAC9C,MAAMgE,UAAU,MAAMD;YACtB,MAAMK,WAA0B,EAAE;YAClC,MAAM3D,IACJuD,QAAQC,GAAG,CAACI,SAAS,CAAC1B,GAAGD,mBAAmBM,YAAYoB;YAG1D,MAAMJ,QAAQM,OAAO,CAACjB,QAAQ;YAE9B7C,OAAO4D,SAASG,GAAG,CAACrB,CAAAA,QAASA,MAAMI,EAAE,GAAGhB,OAAO,CAAC;gBAAC;aAAO;QAC1D;QAEA,0EAA0E;QAC1E,yEAAyE;QACzE,wDAAwD;QACxDtC,GAAG,iEAAiE;YAClE,MAAMgE,UAAU,MAAMD;YACtB,MAAMK,WAA0B,EAAE;YAClC,MAAM3D,IACJuD,QAAQC,GAAG,CAACI,SAAS,CAAC1B,GAAG,qBAAqBK,YAAYoB;YAG5D,MAAMJ,QAAQM,OAAO,CAACjB,QAAQ;YAE9B7C,OAAO4D,UAAU9B,OAAO,CAAC,EAAE;QAC7B;QAEAtC,GAAG,+BAA+B;YAChC,MAAMgE,UAAU,MAAMD;YAEtB,MAAMtD,IAAIuD,QAAQC,GAAG,CAACC,OAAO,CAACb,QAAQ;YACtC,MAAM5C,IAAIuD,QAAQC,GAAG,CAACC,OAAO,CAACb,QAAQ;YAEtC7C,OAAOwD,QAAQG,SAAS,GAAGI,GAAG,CAACrB,CAAAA,QAASA,MAAMI,EAAE,GAAGhB,OAAO,CAAC;gBACzD;gBACA;aACD;QACH;IACF;AACF,EAAE;AAEF;;;;;;;;;;;CAWC,GACD,OAAO,MAAMkC,mCAAmC,CAC9C3D,MACA4D;IAEAlE,SAAS,CAAC,2BAA2B,EAAEM,MAAM,EAAE;QAC7Cb,GAAG,wBAAwB;YACzB,MAAM0E,QAAQ,MAAMD,UAAU;YAE9BjE,OAAO,MAAMC,IAAIiE,MAAMC,KAAK,CAAC,oBAAoBxD,IAAI,CAAC;QACxD;QAEAnB,GAAG,0CAA0C;YAC3C,MAAM0E,QAAQ,MAAMD,UAAU;YAC9B,MAAMhE,IAAIiE,MAAMC,KAAK,CAAC;YAEtBnE,OAAO,MAAMC,IAAIiE,MAAMC,KAAK,CAAC,oBAAoBxD,IAAI,CAAC;QACxD;QAEAnB,GAAG,6CAA6C;YAC9C,MAAM0E,QAAQ,MAAMD,UAAU;YAC9B,MAAMhE,IAAIiE,MAAMC,KAAK,CAAC;YAEtBnE,OAAO,MAAMC,IAAIiE,MAAMC,KAAK,CAAC,oBAAoBxD,IAAI,CAAC;YACtDX,OAAO,MAAMC,IAAIiE,MAAMC,KAAK,CAAC,oBAAoBxD,IAAI,CAAC;QACxD;QAEAnB,GAAG,kDAAkD;YACnD,gEAAgE;YAChE,wEAAwE;YACxE,gEAAgE;YAChE,MAAM0E,QAAQ,MAAMD,UAAU;YAE9BjE,OAAO,MAAMC,IAAIiE,MAAMC,KAAK,CAAC,mBAAmBxD,IAAI,CAAC;YACrDX,OAAO,MAAMC,IAAIiE,MAAMC,KAAK,CAAC,oBAAoBxD,IAAI,CAAC;QACxD;QAEAnB,GAAG,8DAA8D;YAC/D,MAAM4E,QAAQ,MAAMH,UAAU;YAC9B,MAAMI,SAAS,MAAMJ,UAAU;YAC/B,MAAMhE,IAAImE,MAAMD,KAAK,CAAC;YAEtBnE,OAAO,MAAMC,IAAIoE,OAAOF,KAAK,CAAC,oBAAoBxD,IAAI,CAAC;QACzD;IACF;AACF,EAAE"}
1
+ {"version":3,"sources":["../../src/contracts/transaction-ports.contract.ts"],"sourcesContent":["import type { DomainEvent } from '@entifix/core';\nimport type {\n EventBus,\n LockService,\n SequenceService,\n Subscription,\n TransactionInbox,\n} from '@entifix/transactions';\nimport { Duration, Effect, Exit, Fiber } from 'effect';\nimport { TestClock } from 'effect/testing';\nimport { describe, expect, it } from 'vitest';\n\n/**\n * Runs an Effect test on a `TestClock`, so a retry budget measured in seconds\n * costs none of the wall clock.\n */\nconst effectIt = {\n effect: (name: string, body: () => Effect.Effect<unknown, unknown>) =>\n it(name, () =>\n Effect.runPromise(\n body().pipe(Effect.provide(TestClock.layer()), Effect.scoped),\n ),\n ),\n};\n\nconst run = <TValue, TError>(effect: Effect.Effect<TValue, TError>) =>\n Effect.runPromise(effect);\n\n/**\n * What every {@link LockService} must guarantee: exclusivity while held,\n * availability after release, and a release that only frees the caller's own\n * lock. The last one is why `release` takes a handle rather than a key — a lock\n * that lapsed and was re-acquired by someone else must not be freed by the\n * previous holder.\n */\nexport const describeLockServiceContract = (\n name: string,\n makeLockService: () => LockService | Promise<LockService>,\n): void => {\n describe(`LockService contract: ${name}`, () => {\n it('acquires an uncontended key', async () => {\n const locks = await makeLockService();\n\n const handle = await run(locks.acquire('contract:free'));\n\n expect(handle.key).toBe('contract:free');\n expect(handle.token).toBeTruthy();\n });\n\n // An implementation may retry before giving up (the Redis adapter waits out\n // a bounded budget), so this runs on a TestClock: the assertion is about\n // exclusivity, and it should not cost seconds of wall clock to make.\n effectIt.effect('refuses a key that is already held', () =>\n Effect.gen(function* () {\n const locks = yield* Effect.promise(async () => makeLockService());\n yield* locks.acquire('contract:taken');\n\n const fiber = yield* Effect.forkChild(locks.acquire('contract:taken'));\n yield* TestClock.adjust(Duration.minutes(1));\n const exit = yield* Fiber.await(fiber);\n\n expect(Exit.isFailure(exit)).toBe(true);\n }),\n );\n\n it('makes the key available again after release', async () => {\n const locks = await makeLockService();\n const handle = await run(locks.acquire('contract:cycle'));\n\n await run(locks.release(handle));\n const reacquired = await run(locks.acquire('contract:cycle'));\n\n expect(reacquired.key).toBe('contract:cycle');\n });\n\n effectIt.effect('ignores a release whose token does not match', () =>\n Effect.gen(function* () {\n const locks = yield* Effect.promise(async () => makeLockService());\n const handle = yield* locks.acquire('contract:stolen');\n\n yield* locks.release({ key: handle.key, token: 'not-mine' });\n\n // Still held, so a fresh acquisition must fail.\n const fiber = yield* Effect.forkChild(locks.acquire('contract:stolen'));\n yield* TestClock.adjust(Duration.minutes(1));\n const exit = yield* Fiber.await(fiber);\n\n expect(Exit.isFailure(exit)).toBe(true);\n }),\n );\n });\n};\n\n/**\n * What every {@link SequenceService} must guarantee: strictly increasing values\n * per name, never repeating, with names independent of one another. This is the\n * property incremental codes rest on.\n */\nexport const describeSequenceServiceContract = (\n name: string,\n makeSequenceService: () => SequenceService | Promise<SequenceService>,\n): void => {\n describe(`SequenceService contract: ${name}`, () => {\n it('starts at 1', async () => {\n const sequences = await makeSequenceService();\n\n expect(await run(sequences.next('contract:fresh'))).toBe(1);\n });\n\n it('never returns the same value twice for a name', async () => {\n const sequences = await makeSequenceService();\n\n const drawn = [\n await run(sequences.next('contract:codes')),\n await run(sequences.next('contract:codes')),\n await run(sequences.next('contract:codes')),\n ];\n\n expect(drawn).toEqual([1, 2, 3]);\n expect(new Set(drawn).size).toBe(drawn.length);\n });\n\n it('keeps separate names independent', async () => {\n const sequences = await makeSequenceService();\n await run(sequences.next('contract:a'));\n await run(sequences.next('contract:a'));\n\n expect(await run(sequences.next('contract:b'))).toBe(1);\n });\n });\n};\n\nexport interface EventBusContractHarness {\n bus: EventBus;\n /** Delivers `event` to whatever subscribed, as the broker would. */\n deliver(event: DomainEvent): Promise<void>;\n /** Everything the bus published, decoded back into events. */\n published(): DomainEvent[];\n}\n\n/** Every subscription in this contract, unless a test says otherwise. */\nconst ANY_WIDGET_EVENT = 'contract.widget.*';\n\n/**\n * A `work` subscription for `pattern`. The mode and the ceiling are broker\n * facts with no bearing on routing, so a test about routing states them once\n * here rather than repeating them at every call.\n */\nconst on = (pattern: string): Subscription => ({\n slice: 'contract',\n pattern,\n mode: 'work',\n maxAttempts: 5,\n});\n\n/**\n * A handler that records what it is given. Shared by both routing tests so the\n * \"nothing was delivered\" case asserts on the *same* handler the positive case\n * proves works — otherwise it could pass against a handler that was simply\n * never wired up.\n */\nconst collectInto = (sink: DomainEvent[]) => (event: DomainEvent) =>\n Effect.sync(() => {\n sink.push(event);\n });\n\nconst anEvent = (\n id: string,\n name = 'contract.widget.created',\n): DomainEvent => ({\n name,\n id,\n source: 'contract-slice',\n at: new Date('2026-01-01T00:00:00.000Z').toISOString(),\n correlationId: id,\n data: { entity: 'contract-widget' },\n});\n\n/**\n * What every {@link EventBus} must guarantee: what goes out comes back\n * unchanged, subscribers receive deliveries, and a subscriber receives only what\n * its pattern matches. The transports differ wildly (an array here, a topic\n * exchange in RabbitMQ) — the observable contract does not.\n */\nexport const describeEventBusContract = (\n name: string,\n makeHarness: () => EventBusContractHarness | Promise<EventBusContractHarness>,\n): void => {\n describe(`EventBus contract: ${name}`, () => {\n it('publishes an event verbatim', async () => {\n const harness = await makeHarness();\n const event = anEvent('tx-1');\n\n await run(harness.bus.publish(event));\n\n expect(harness.published()).toEqual([event]);\n });\n\n it('delivers published events to a subscriber', async () => {\n const harness = await makeHarness();\n const received: DomainEvent[] = [];\n await run(\n harness.bus.subscribe(on(ANY_WIDGET_EVENT), collectInto(received)),\n );\n\n await harness.deliver(anEvent('tx-2'));\n\n expect(received.map(event => event.id)).toEqual(['tx-2']);\n });\n\n // Routing is the transport's job. A bus that hands every subscriber every\n // event lets a consumer pass while receiving traffic it never bound to —\n // which is precisely the fault the fanout exchange had.\n it('delivers nothing to a subscriber whose pattern does not match', async () => {\n const harness = await makeHarness();\n const received: DomainEvent[] = [];\n await run(\n harness.bus.subscribe(on('contract.other.*'), collectInto(received)),\n );\n\n await harness.deliver(anEvent('tx-5'));\n\n expect(received).toEqual([]);\n });\n\n it('preserves publication order', async () => {\n const harness = await makeHarness();\n\n await run(harness.bus.publish(anEvent('tx-3')));\n await run(harness.bus.publish(anEvent('tx-4')));\n\n expect(harness.published().map(event => event.id)).toEqual([\n 'tx-3',\n 'tx-4',\n ]);\n });\n });\n};\n\n/**\n * What every {@link TransactionInbox} must guarantee: a first claim is granted,\n * every repeat of it is refused, and one consumer's claim says nothing about\n * another's.\n *\n * That last one is the property with teeth. Two consumers legitimately process\n * the same event — the saga tracker's fold and the SSE hub both bind\n * `transaction.*` — so an implementation keyed on `eventId` alone passes the\n * first two cases and silently starves every consumer but the first. It looks\n * like exactly-once from inside one consumer, which is why it is asserted here\n * rather than left to each adapter's own spec.\n */\nexport const describeTransactionInboxContract = (\n name: string,\n makeInbox: (consumer: string) => TransactionInbox | Promise<TransactionInbox>,\n): void => {\n describe(`TransactionInbox contract: ${name}`, () => {\n it('grants a first claim', async () => {\n const inbox = await makeInbox('contract.consumer');\n\n expect(await run(inbox.claim('tx-1:completed'))).toBe('claimed');\n });\n\n it('refuses a redelivery of the same event', async () => {\n const inbox = await makeInbox('contract.consumer');\n await run(inbox.claim('tx-2:completed'));\n\n expect(await run(inbox.claim('tx-2:completed'))).toBe('duplicate');\n });\n\n it('refuses every repeat, not only the second', async () => {\n const inbox = await makeInbox('contract.consumer');\n await run(inbox.claim('tx-3:completed'));\n\n expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');\n expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');\n });\n\n it('claims each step of one transaction separately', async () => {\n // `<transactionId>:<step>`, never the transaction id alone: one\n // transaction emits up to three messages, and keying on the correlation\n // id would make `completed` read as a redelivery of `accepted`.\n const inbox = await makeInbox('contract.consumer');\n\n expect(await run(inbox.claim('tx-4:accepted'))).toBe('claimed');\n expect(await run(inbox.claim('tx-4:completed'))).toBe('claimed');\n });\n\n it('does not let one consumer consume another consumer’s claim', async () => {\n const first = await makeInbox('contract.first');\n const second = await makeInbox('contract.second');\n await run(first.claim('tx-5:completed'));\n\n expect(await run(second.claim('tx-5:completed'))).toBe('claimed');\n });\n });\n};\n"],"names":["Duration","Effect","Exit","Fiber","TestClock","describe","expect","it","effectIt","effect","name","body","runPromise","pipe","provide","layer","scoped","run","describeLockServiceContract","makeLockService","locks","handle","acquire","key","toBe","token","toBeTruthy","gen","promise","fiber","forkChild","adjust","minutes","exit","await","isFailure","release","reacquired","describeSequenceServiceContract","makeSequenceService","sequences","next","drawn","toEqual","Set","size","length","ANY_WIDGET_EVENT","on","pattern","slice","mode","maxAttempts","collectInto","sink","event","sync","push","anEvent","id","source","at","Date","toISOString","correlationId","data","entity","describeEventBusContract","makeHarness","harness","bus","publish","published","received","subscribe","deliver","map","describeTransactionInboxContract","makeInbox","inbox","claim","first","second"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"entity-repository.d.ts","sourceRoot":"","sources":["../../src/doubles/entity-repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,MAAM,EAaZ,MAAM,eAAe,CAAC;AA0KvB,MAAM,WAAW,wBAAyB,SAAQ,gBAAgB;IAChE,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IACzB,4DAA4D;IAC5D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACvC,OAAM,MAAM,EAAO,KAClB,wBAqGF,CAAC"}
1
+ {"version":3,"file":"entity-repository.d.ts","sourceRoot":"","sources":["../../src/doubles/entity-repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,MAAM,EAaZ,MAAM,eAAe,CAAC;AA0KvB,MAAM,WAAW,wBAAyB,SAAQ,gBAAgB;IAChE,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IACzB,4DAA4D;IAC5D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACvC,OAAM,MAAM,EAAO,KAClB,wBAuGF,CAAC"}
@@ -157,14 +157,14 @@ const applySorting = (items, sorting)=>{
157
157
  // The store keeps its own copy and hands back another, so neither the
158
158
  // caller's instance nor the returned one can change the record without a
159
159
  // second save — which a real adapter guarantees by construction.
160
- return copyOf(entity).pipe(Effect.tap((stored)=>{
161
- const index = items.findIndex((item)=>item.id === stored.id);
162
- if (index === -1) {
163
- items.push(stored);
164
- } else {
165
- items[index] = stored;
166
- }
167
- }), Effect.flatMap(copyOf));
160
+ return copyOf(entity).pipe(Effect.tap((stored)=>Effect.sync(()=>{
161
+ const index = items.findIndex((item)=>item.id === stored.id);
162
+ if (index === -1) {
163
+ items.push(stored);
164
+ } else {
165
+ items[index] = stored;
166
+ }
167
+ })), Effect.flatMap(copyOf));
168
168
  });
169
169
  const remove = (entityOrId)=>guard(()=>{
170
170
  const id = entityOrId != null && typeof entityOrId === 'object' ? entityOrId.id : entityOrId;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/doubles/entity-repository.ts"],"sourcesContent":["import type { EntityRepository } from '@entifix/business';\nimport {\n deserializeSingleEntity,\n EntifixConnError,\n type EntifixError,\n type Entity,\n EntityCollectionLink,\n type EntityConstructor,\n type EntityFilter,\n type EntityFiltering,\n type EntityId,\n EntityLink,\n type EntityLoadRequest,\n type EntityPage,\n type EntitySorting,\n extractMetaAccessors,\n type FilterGroup,\n serializeEntity,\n} from '@entifix/core';\nimport { Effect } from 'effect';\n\n/** Escapes a string so `like`/`nlike` match literally, as the Mongo adapter does. */\nconst escapeRegex = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst isFilterGroup = <TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> =>\n !('property' in node) && (node.operator === 'and' || node.operator === 'or');\n\n/**\n * A member as the Mongo adapter stores it, and so as its queries see it: a\n * link is its target's id, a collection link the list of their ids. Without\n * this no operator can compare a link, since `eq` against an id is never\n * `true` for an object.\n */\nconst storedValue = (value: unknown): unknown => {\n if (value instanceof EntityLink) return value.id;\n if (value instanceof EntityCollectionLink) return value.ids;\n return value;\n};\n\n/** `$in` semantics: a scalar matches a value, an array when any element does. */\nconst matchesAny = (actual: unknown, values: readonly unknown[]): boolean =>\n Array.isArray(actual)\n ? actual.some(element => values.includes(element))\n : values.includes(actual);\n\nconst compareValues = (left: unknown, right: unknown): number => {\n if (left === right) return 0;\n // Mongo sorts missing values below present ones.\n if (left == null) return -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number') {\n return left - right;\n }\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n};\n\nconst matchesNode = <TEntity extends Entity>(\n item: TEntity,\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): boolean => {\n if (isFilterGroup(node)) {\n return node.operator === 'or'\n ? node.values.some(child => matchesNode(item, child))\n : node.values.every(child => matchesNode(item, child));\n }\n\n const actual = storedValue(\n (item as Record<string, unknown>)[String(node.property)],\n );\n\n switch (node.operator) {\n case 'eq':\n return actual === node.value;\n case 'ne':\n return actual !== node.value;\n case 'gt':\n return compareValues(actual, node.value) > 0;\n case 'gte':\n return compareValues(actual, node.value) >= 0;\n case 'lt':\n return compareValues(actual, node.value) < 0;\n case 'lte':\n return compareValues(actual, node.value) <= 0;\n case 'in':\n return matchesAny(actual, node.values);\n case 'nin':\n return !matchesAny(actual, node.values);\n case 'between':\n return (\n compareValues(actual, node.start) >= 0 &&\n compareValues(actual, node.end) <= 0\n );\n case 'nbetween':\n return !(\n compareValues(actual, node.start) >= 0 &&\n compareValues(actual, node.end) <= 0\n );\n case 'like':\n return new RegExp(escapeRegex(node.value), 'i').test(String(actual));\n case 'nlike':\n return !new RegExp(escapeRegex(node.value), 'i').test(String(actual));\n case 'isNull':\n return actual == null;\n case 'isNotNull':\n return actual != null;\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const never: never = node;\n return never;\n }\n }\n};\n\nconst applyFiltering = <TEntity extends Entity>(\n items: TEntity[],\n filtering?: EntityFiltering<TEntity>[],\n): TEntity[] => {\n if (!filtering || filtering.length === 0) return items;\n // Every top-level entry is combined with `and`, matching `translateFiltering`.\n const nodes = filtering.flatMap(entry =>\n Array.isArray(entry) ? entry : [entry],\n );\n return items.filter(item => nodes.every(node => matchesNode(item, node)));\n};\n\nconst applySorting = <TEntity extends Entity>(\n items: TEntity[],\n sorting?: EntitySorting<TEntity>[],\n): TEntity[] => {\n if (!sorting || sorting.length === 0) return items;\n\n const entries = sorting.flatMap(record =>\n Object.keys(record)\n .map(Number)\n .sort((left, right) => left - right)\n .map(priority => record[priority])\n .filter(entry => entry != null),\n );\n\n return [...items].sort((left, right) => {\n for (const entry of entries) {\n const property = String(entry.property);\n const result = compareValues(\n (left as Record<string, unknown>)[property],\n (right as Record<string, unknown>)[property],\n );\n if (result !== 0) return entry.type === 'desc' ? -result : result;\n }\n return 0;\n });\n};\n\n/**\n * A copy of `entity` that shares no state with it.\n *\n * Through the entity's own mapping — serialize, then deserialize — because that\n * is what a real adapter does to every record on the way in and out: a caller\n * can never hold the instance the store keeps, and a member the mapping drops\n * (`hidden`, `readonly`) does not survive a save here either. A class that\n * declares no accessors has no mapping to go through, so it is copied member by\n * member instead; plain spec fixtures are the case this keeps working.\n */\nconst copyOf = <TEntity extends Entity>(\n entity: TEntity,\n): Effect.Effect<TEntity, EntifixError> => {\n const entityConstructor = entity.constructor as EntityConstructor<TEntity>;\n if (extractMetaAccessors(entityConstructor).length === 0) {\n return Effect.succeed(\n Object.assign(\n Object.create(Object.getPrototypeOf(entity) as object) as TEntity,\n entity,\n ),\n );\n }\n // `undefined` only comes back for `null`/`undefined` input, and\n // `serializeEntity` always hands over an object.\n return deserializeSingleEntity(\n entityConstructor,\n serializeEntity(entityConstructor, entity),\n ) as Effect.Effect<TEntity, EntifixError>;\n};\n\nexport interface InMemoryEntityRepository extends EntityRepository {\n /** Everything currently stored, in insertion order. */\n readonly items: Entity[];\n /** Replaces the contents, discarding whatever was there. */\n seed(items: Entity[]): void;\n /**\n * Makes the next call to any method fail with `error`, so error branches are\n * reachable without reaching for a mocking library.\n */\n failNext(error: EntifixError): void;\n}\n\n/**\n * In-memory {@link EntityRepository}: the default double for anything that\n * depends on persistence.\n *\n * Its semantics deliberately mirror `makeMongoRepository` — same filtering and\n * sorting rules, same 1-based paging with a default page size of 10, same\n * \"entity not found\" failure, same id minting on create — and\n * `describeEntityRepositoryContract` holds both to that shared contract.\n */\nexport const makeInMemoryEntityRepository = (\n seed: Entity[] = [],\n): InMemoryEntityRepository => {\n let items = [...seed];\n let pendingFailure: EntifixError | undefined;\n\n /** Consumes a queued failure, if one was armed. */\n const guard = <TValue>(\n run: () => Effect.Effect<TValue, EntifixError>,\n ): Effect.Effect<TValue, EntifixError> => {\n if (pendingFailure !== undefined) {\n const failure = pendingFailure;\n pendingFailure = undefined;\n return Effect.fail(failure);\n }\n return run();\n };\n\n const load = <TEntity extends Entity>(request: EntityLoadRequest<TEntity>) =>\n guard(() => {\n const matched = applySorting(\n applyFiltering(items as TEntity[], request.filtering),\n request.sorting,\n );\n const page = request.page ?? 1;\n const pageSize = request.pageSize ?? 10;\n const start = (page - 1) * pageSize;\n\n return Effect.forEach(\n matched.slice(start, start + pageSize),\n copyOf,\n ).pipe(\n Effect.map(\n copies =>\n ({\n items: copies,\n total: matched.length,\n request,\n }) satisfies EntityPage<TEntity>,\n ),\n );\n });\n\n const get = <TEntity extends Entity>(id: EntityId) =>\n guard(() => {\n const found = items.find(item => item.id === id);\n return found === undefined\n ? Effect.fail(\n new EntifixConnError('Entity not found', undefined, { id }),\n )\n : copyOf(found as TEntity);\n });\n\n const save = <TEntity extends Entity>(entity: TEntity) =>\n guard(() => {\n // A create arrives without an id, so the store mints one — same rule the\n // Mongo adapter follows. The Web Crypto global rather than `node:crypto`,\n // because a browser bundle cannot resolve a `node:` import and an example\n // with no backend runs this repository in the page.\n entity.id = entity.id ?? globalThis.crypto.randomUUID();\n // The store keeps its own copy and hands back another, so neither the\n // caller's instance nor the returned one can change the record without a\n // second save — which a real adapter guarantees by construction.\n return copyOf(entity).pipe(\n Effect.tap(stored => {\n const index = items.findIndex(item => item.id === stored.id);\n if (index === -1) {\n items.push(stored);\n } else {\n items[index] = stored;\n }\n }),\n Effect.flatMap(copyOf),\n );\n });\n\n const remove = <TEntity extends Entity>(entityOrId: EntityId | TEntity) =>\n guard(() => {\n const id =\n entityOrId != null && typeof entityOrId === 'object'\n ? entityOrId.id\n : entityOrId;\n items = items.filter(item => item.id !== id);\n return Effect.succeed(undefined);\n });\n\n return {\n load,\n get,\n save,\n delete: remove,\n get items() {\n return items;\n },\n seed: (next: Entity[]) => {\n items = [...next];\n },\n failNext: (error: EntifixError) => {\n pendingFailure = error;\n },\n // Each method closes over its own state, so nothing is required from the\n // Effect environment; that is assignable to the interface's channel.\n } as unknown as InMemoryEntityRepository;\n};\n"],"names":["deserializeSingleEntity","EntifixConnError","EntityCollectionLink","EntityLink","extractMetaAccessors","serializeEntity","Effect","escapeRegex","value","replace","isFilterGroup","node","operator","storedValue","id","ids","matchesAny","actual","values","Array","isArray","some","element","includes","compareValues","left","right","Date","getTime","String","localeCompare","matchesNode","item","child","every","property","start","end","RegExp","test","never","applyFiltering","items","filtering","length","nodes","flatMap","entry","filter","applySorting","sorting","entries","record","Object","keys","map","Number","sort","priority","result","type","copyOf","entity","entityConstructor","succeed","assign","create","getPrototypeOf","makeInMemoryEntityRepository","seed","pendingFailure","guard","run","undefined","failure","fail","load","request","matched","page","pageSize","forEach","slice","pipe","copies","total","get","found","find","save","globalThis","crypto","randomUUID","tap","stored","index","findIndex","push","remove","entityOrId","delete","next","failNext","error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/doubles/entity-repository.ts"],"sourcesContent":["import type { EntityRepository } from '@entifix/business';\nimport {\n deserializeSingleEntity,\n EntifixConnError,\n type EntifixError,\n type Entity,\n EntityCollectionLink,\n type EntityConstructor,\n type EntityFilter,\n type EntityFiltering,\n type EntityId,\n EntityLink,\n type EntityLoadRequest,\n type EntityPage,\n type EntitySorting,\n extractMetaAccessors,\n type FilterGroup,\n serializeEntity,\n} from '@entifix/core';\nimport { Effect } from 'effect';\n\n/** Escapes a string so `like`/`nlike` match literally, as the Mongo adapter does. */\nconst escapeRegex = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst isFilterGroup = <TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> =>\n !('property' in node) && (node.operator === 'and' || node.operator === 'or');\n\n/**\n * A member as the Mongo adapter stores it, and so as its queries see it: a\n * link is its target's id, a collection link the list of their ids. Without\n * this no operator can compare a link, since `eq` against an id is never\n * `true` for an object.\n */\nconst storedValue = (value: unknown): unknown => {\n if (value instanceof EntityLink) return value.id;\n if (value instanceof EntityCollectionLink) return value.ids;\n return value;\n};\n\n/** `$in` semantics: a scalar matches a value, an array when any element does. */\nconst matchesAny = (actual: unknown, values: readonly unknown[]): boolean =>\n Array.isArray(actual)\n ? actual.some(element => values.includes(element))\n : values.includes(actual);\n\nconst compareValues = (left: unknown, right: unknown): number => {\n if (left === right) return 0;\n // Mongo sorts missing values below present ones.\n if (left == null) return -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number') {\n return left - right;\n }\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n};\n\nconst matchesNode = <TEntity extends Entity>(\n item: TEntity,\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): boolean => {\n if (isFilterGroup(node)) {\n return node.operator === 'or'\n ? node.values.some(child => matchesNode(item, child))\n : node.values.every(child => matchesNode(item, child));\n }\n\n const actual = storedValue(\n (item as Record<string, unknown>)[String(node.property)],\n );\n\n switch (node.operator) {\n case 'eq':\n return actual === node.value;\n case 'ne':\n return actual !== node.value;\n case 'gt':\n return compareValues(actual, node.value) > 0;\n case 'gte':\n return compareValues(actual, node.value) >= 0;\n case 'lt':\n return compareValues(actual, node.value) < 0;\n case 'lte':\n return compareValues(actual, node.value) <= 0;\n case 'in':\n return matchesAny(actual, node.values);\n case 'nin':\n return !matchesAny(actual, node.values);\n case 'between':\n return (\n compareValues(actual, node.start) >= 0 &&\n compareValues(actual, node.end) <= 0\n );\n case 'nbetween':\n return !(\n compareValues(actual, node.start) >= 0 &&\n compareValues(actual, node.end) <= 0\n );\n case 'like':\n return new RegExp(escapeRegex(node.value), 'i').test(String(actual));\n case 'nlike':\n return !new RegExp(escapeRegex(node.value), 'i').test(String(actual));\n case 'isNull':\n return actual == null;\n case 'isNotNull':\n return actual != null;\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const never: never = node;\n return never;\n }\n }\n};\n\nconst applyFiltering = <TEntity extends Entity>(\n items: TEntity[],\n filtering?: EntityFiltering<TEntity>[],\n): TEntity[] => {\n if (!filtering || filtering.length === 0) return items;\n // Every top-level entry is combined with `and`, matching `translateFiltering`.\n const nodes = filtering.flatMap(entry =>\n Array.isArray(entry) ? entry : [entry],\n );\n return items.filter(item => nodes.every(node => matchesNode(item, node)));\n};\n\nconst applySorting = <TEntity extends Entity>(\n items: TEntity[],\n sorting?: EntitySorting<TEntity>[],\n): TEntity[] => {\n if (!sorting || sorting.length === 0) return items;\n\n const entries = sorting.flatMap(record =>\n Object.keys(record)\n .map(Number)\n .sort((left, right) => left - right)\n .map(priority => record[priority])\n .filter(entry => entry != null),\n );\n\n return [...items].sort((left, right) => {\n for (const entry of entries) {\n const property = String(entry.property);\n const result = compareValues(\n (left as Record<string, unknown>)[property],\n (right as Record<string, unknown>)[property],\n );\n if (result !== 0) return entry.type === 'desc' ? -result : result;\n }\n return 0;\n });\n};\n\n/**\n * A copy of `entity` that shares no state with it.\n *\n * Through the entity's own mapping — serialize, then deserialize — because that\n * is what a real adapter does to every record on the way in and out: a caller\n * can never hold the instance the store keeps, and a member the mapping drops\n * (`hidden`, `readonly`) does not survive a save here either. A class that\n * declares no accessors has no mapping to go through, so it is copied member by\n * member instead; plain spec fixtures are the case this keeps working.\n */\nconst copyOf = <TEntity extends Entity>(\n entity: TEntity,\n): Effect.Effect<TEntity, EntifixError> => {\n const entityConstructor = entity.constructor as EntityConstructor<TEntity>;\n if (extractMetaAccessors(entityConstructor).length === 0) {\n return Effect.succeed(\n Object.assign(\n Object.create(Object.getPrototypeOf(entity) as object) as TEntity,\n entity,\n ),\n );\n }\n // `undefined` only comes back for `null`/`undefined` input, and\n // `serializeEntity` always hands over an object.\n return deserializeSingleEntity(\n entityConstructor,\n serializeEntity(entityConstructor, entity),\n ) as Effect.Effect<TEntity, EntifixError>;\n};\n\nexport interface InMemoryEntityRepository extends EntityRepository {\n /** Everything currently stored, in insertion order. */\n readonly items: Entity[];\n /** Replaces the contents, discarding whatever was there. */\n seed(items: Entity[]): void;\n /**\n * Makes the next call to any method fail with `error`, so error branches are\n * reachable without reaching for a mocking library.\n */\n failNext(error: EntifixError): void;\n}\n\n/**\n * In-memory {@link EntityRepository}: the default double for anything that\n * depends on persistence.\n *\n * Its semantics deliberately mirror `makeMongoRepository` — same filtering and\n * sorting rules, same 1-based paging with a default page size of 10, same\n * \"entity not found\" failure, same id minting on create — and\n * `describeEntityRepositoryContract` holds both to that shared contract.\n */\nexport const makeInMemoryEntityRepository = (\n seed: Entity[] = [],\n): InMemoryEntityRepository => {\n let items = [...seed];\n let pendingFailure: EntifixError | undefined;\n\n /** Consumes a queued failure, if one was armed. */\n const guard = <TValue>(\n run: () => Effect.Effect<TValue, EntifixError>,\n ): Effect.Effect<TValue, EntifixError> => {\n if (pendingFailure !== undefined) {\n const failure = pendingFailure;\n pendingFailure = undefined;\n return Effect.fail(failure);\n }\n return run();\n };\n\n const load = <TEntity extends Entity>(request: EntityLoadRequest<TEntity>) =>\n guard(() => {\n const matched = applySorting(\n applyFiltering(items as TEntity[], request.filtering),\n request.sorting,\n );\n const page = request.page ?? 1;\n const pageSize = request.pageSize ?? 10;\n const start = (page - 1) * pageSize;\n\n return Effect.forEach(\n matched.slice(start, start + pageSize),\n copyOf,\n ).pipe(\n Effect.map(\n copies =>\n ({\n items: copies,\n total: matched.length,\n request,\n }) satisfies EntityPage<TEntity>,\n ),\n );\n });\n\n const get = <TEntity extends Entity>(id: EntityId) =>\n guard(() => {\n const found = items.find(item => item.id === id);\n return found === undefined\n ? Effect.fail(\n new EntifixConnError('Entity not found', undefined, { id }),\n )\n : copyOf(found as TEntity);\n });\n\n const save = <TEntity extends Entity>(entity: TEntity) =>\n guard(() => {\n // A create arrives without an id, so the store mints one — same rule the\n // Mongo adapter follows. The Web Crypto global rather than `node:crypto`,\n // because a browser bundle cannot resolve a `node:` import and an example\n // with no backend runs this repository in the page.\n entity.id = entity.id ?? globalThis.crypto.randomUUID();\n // The store keeps its own copy and hands back another, so neither the\n // caller's instance nor the returned one can change the record without a\n // second save — which a real adapter guarantees by construction.\n return copyOf(entity).pipe(\n Effect.tap(stored =>\n Effect.sync(() => {\n const index = items.findIndex(item => item.id === stored.id);\n if (index === -1) {\n items.push(stored);\n } else {\n items[index] = stored;\n }\n }),\n ),\n Effect.flatMap(copyOf),\n );\n });\n\n const remove = <TEntity extends Entity>(entityOrId: EntityId | TEntity) =>\n guard(() => {\n const id =\n entityOrId != null && typeof entityOrId === 'object'\n ? entityOrId.id\n : entityOrId;\n items = items.filter(item => item.id !== id);\n return Effect.succeed(undefined);\n });\n\n return {\n load,\n get,\n save,\n delete: remove,\n get items() {\n return items;\n },\n seed: (next: Entity[]) => {\n items = [...next];\n },\n failNext: (error: EntifixError) => {\n pendingFailure = error;\n },\n // Each method closes over its own state, so nothing is required from the\n // Effect environment; that is assignable to the interface's channel.\n } as unknown as InMemoryEntityRepository;\n};\n"],"names":["deserializeSingleEntity","EntifixConnError","EntityCollectionLink","EntityLink","extractMetaAccessors","serializeEntity","Effect","escapeRegex","value","replace","isFilterGroup","node","operator","storedValue","id","ids","matchesAny","actual","values","Array","isArray","some","element","includes","compareValues","left","right","Date","getTime","String","localeCompare","matchesNode","item","child","every","property","start","end","RegExp","test","never","applyFiltering","items","filtering","length","nodes","flatMap","entry","filter","applySorting","sorting","entries","record","Object","keys","map","Number","sort","priority","result","type","copyOf","entity","entityConstructor","succeed","assign","create","getPrototypeOf","makeInMemoryEntityRepository","seed","pendingFailure","guard","run","undefined","failure","fail","load","request","matched","page","pageSize","forEach","slice","pipe","copies","total","get","found","find","save","globalThis","crypto","randomUUID","tap","stored","sync","index","findIndex","push","remove","entityOrId","delete","next","failNext","error"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fake-sql.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACzC;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAC7E;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,aAAa,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrD;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAuBD,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,GAC5B,OAAM,aAAa,CAAC,eAAe,CAAM,KACxC,aAiEF,CAAC"}
1
+ {"version":3,"file":"fake-sql.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACzC;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAC7E;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,aAAa,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrD;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAuBD,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,GAC5B,OAAM,aAAa,CAAC,eAAe,CAAM,KACxC,aAsEF,CAAC"}
@@ -11,9 +11,9 @@
11
11
  * parameters the adapter produced. Running the shared
12
12
  * `describeEntityRepositoryContract` needs a real engine and belongs against real
13
13
  * Postgres, the way the driver connection Layers are covered.
14
- */ import { SqlError } from '@effect/sql';
15
- import * as Statement from '@effect/sql/Statement';
16
- import { Effect } from 'effect';
14
+ */ import { Effect } from 'effect';
15
+ import { SqlError } from 'effect/sql';
16
+ import { Statement } from 'effect/sql';
17
17
  const unsupported = (name)=>{
18
18
  throw new TypeError(`fake sql: ${name} is not implemented`);
19
19
  };
@@ -60,8 +60,10 @@ const unsupported = (name)=>{
60
60
  const message = (_ref = scoped == null ? void 0 : scoped.message) != null ? _ref : failure;
61
61
  if (message !== undefined) {
62
62
  return Effect.fail(new SqlError.SqlError({
63
- cause: new Error(message),
64
- message
63
+ reason: new SqlError.UnknownError({
64
+ cause: new Error(message),
65
+ message
66
+ })
65
67
  }));
66
68
  }
67
69
  return Effect.succeed(answer(sql));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/drivers/fake-sql.ts"],"sourcesContent":["/**\n * A fake of the `@effect/sql` driver, not of the repository port.\n *\n * It sits one level *below* `makeSqlRepository`, so the adapter's own code — the\n * filter translation, the clause assembly, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would skip\n * all of it. Same reasoning as {@link makeFakeMongoDb}.\n *\n * What it does **not** do is execute SQL: it hands back canned rows and records\n * the statement each call compiled to, so a spec asserts on the SQL and\n * parameters the adapter produced. Running the shared\n * `describeEntityRepositoryContract` needs a real engine and belongs against real\n * Postgres, the way the driver connection Layers are covered.\n */\n\nimport { SqlError } from '@effect/sql';\nimport * as Statement from '@effect/sql/Statement';\nimport { Effect } from 'effect';\n\n/** One statement the adapter executed, already compiled. */\nexport interface FakeSqlExecution {\n /** The compiled SQL text, parameters replaced by `$1`, `$2`, … */\n readonly sql: string;\n readonly params: ReadonlyArray<unknown>;\n}\n\n/** Rows to answer with when a statement's SQL contains `match`. */\nexport interface FakeSqlResponse {\n readonly match: string;\n readonly rows: ReadonlyArray<Record<string, unknown>>;\n}\n\nexport interface FakeSqlClient {\n /**\n * Answers `rows` for any statement whose compiled SQL contains `match`. Later\n * registrations win, so a spec can override a default set up by a helper.\n */\n respondTo(match: string, rows: ReadonlyArray<Record<string, unknown>>): void;\n /**\n * Makes every subsequent execution fail, so the adapter's `EntifixConnError`\n * mapping is reachable.\n */\n failWith(message: string): void;\n /**\n * Makes only the statements containing `match` fail, leaving the rest working\n * — the way a select that succeeds and a count that then fails reaches a\n * distinct error branch.\n */\n failOn(match: string, message: string): void;\n /** Every statement executed, in order, for assertions about what ran. */\n readonly executions: ReadonlyArray<FakeSqlExecution>;\n /** The most recently executed statement. */\n readonly lastExecution: FakeSqlExecution | undefined;\n /**\n * The object to hand to `makeSqlRepository` in place of a real `SqlClient`.\n *\n * Typed `unknown` on purpose — casting at the call site keeps this fake honest\n * that it implements only the constructor surface the adapters use, not the\n * whole `SqlClient` (no transactions, no reactivity, no streaming).\n */\n readonly client: unknown;\n}\n\nconst unsupported = (name: string): never => {\n throw new TypeError(`fake sql: ${name} is not implemented`);\n};\n\n/**\n * The Postgres compiler's configuration, mirrored from `@effect/sql-pg` so the\n * fake emits exactly the dialect production does — `$n` placeholders and\n * double-quoted identifiers — without this package depending on the `pg` driver.\n *\n * `sql.updateValues` and the `PgJson` custom segment are deliberate `TypeError`s\n * rather than approximations: no adapter uses either, and a compiler that\n * half-emitted them would let a spec pass on SQL that real Postgres rejects.\n */\nconst pgCompiler = Statement.makeCompiler({\n dialect: 'pg',\n placeholder: index => `$${index}`,\n onIdentifier: Statement.defaultEscape('\"'),\n onRecordUpdate: () => unsupported('updateValues'),\n onCustom: () => unsupported('custom segments'),\n});\n\n/** Builds an in-memory stand-in for an `@effect/sql` client. */\nexport const makeFakeSqlClient = (\n seed: ReadonlyArray<FakeSqlResponse> = [],\n): FakeSqlClient => {\n const responses: FakeSqlResponse[] = [...seed];\n const executions: FakeSqlExecution[] = [];\n const failuresByMatch: Array<{ match: string; message: string }> = [];\n let failure: string | undefined;\n\n const answer = (sql: string) => {\n // Last registration wins, so an override does not have to remove the\n // default it replaces.\n for (let index = responses.length - 1; index >= 0; index -= 1) {\n const response = responses[index];\n if (response !== undefined && sql.includes(response.match)) {\n return response.rows;\n }\n }\n return [];\n };\n\n const execute = (sql: string, params: ReadonlyArray<unknown>) => {\n executions.push({ sql, params });\n\n const scoped = failuresByMatch.find(entry => sql.includes(entry.match));\n const message = scoped?.message ?? failure;\n if (message !== undefined) {\n return Effect.fail(\n new SqlError.SqlError({ cause: new Error(message), message }),\n );\n }\n return Effect.succeed(answer(sql));\n };\n\n const connection = {\n execute: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeUnprepared: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeRaw: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeValues: () => unsupported('executeValues'),\n executeStream: () => unsupported('executeStream'),\n };\n\n return {\n respondTo: (match, rows) => {\n responses.push({ match, rows });\n },\n failWith: message => {\n failure = message;\n },\n failOn: (match, message) => {\n failuresByMatch.push({ match, message });\n },\n get executions() {\n return executions;\n },\n get lastExecution() {\n return executions[executions.length - 1];\n },\n client: Statement.make(\n Effect.succeed(connection) as never,\n pgCompiler,\n [],\n undefined,\n ),\n };\n};\n"],"names":["SqlError","Statement","Effect","unsupported","name","TypeError","pgCompiler","makeCompiler","dialect","placeholder","index","onIdentifier","defaultEscape","onRecordUpdate","onCustom","makeFakeSqlClient","seed","responses","executions","failuresByMatch","failure","answer","sql","length","response","undefined","includes","match","rows","execute","params","push","scoped","find","entry","message","fail","cause","Error","succeed","connection","executeUnprepared","executeRaw","executeValues","executeStream","respondTo","failWith","failOn","lastExecution","client","make"],"mappings":"AAAA;;;;;;;;;;;;;CAaC,GAED,SAASA,QAAQ,QAAQ,cAAc;AACvC,YAAYC,eAAe,wBAAwB;AACnD,SAASC,MAAM,QAAQ,SAAS;AA8ChC,MAAMC,cAAc,CAACC;IACnB,MAAM,IAAIC,UAAU,CAAC,UAAU,EAAED,KAAK,mBAAmB,CAAC;AAC5D;AAEA;;;;;;;;CAQC,GACD,MAAME,aAAaL,UAAUM,YAAY,CAAC;IACxCC,SAAS;IACTC,aAAaC,CAAAA,QAAS,CAAC,CAAC,EAAEA,OAAO;IACjCC,cAAcV,UAAUW,aAAa,CAAC;IACtCC,gBAAgB,IAAMV,YAAY;IAClCW,UAAU,IAAMX,YAAY;AAC9B;AAEA,8DAA8D,GAC9D,OAAO,MAAMY,oBAAoB,CAC/BC,OAAuC,EAAE;IAEzC,MAAMC,YAA+B;WAAID;KAAK;IAC9C,MAAME,aAAiC,EAAE;IACzC,MAAMC,kBAA6D,EAAE;IACrE,IAAIC;IAEJ,MAAMC,SAAS,CAACC;QACd,qEAAqE;QACrE,uBAAuB;QACvB,IAAK,IAAIZ,QAAQO,UAAUM,MAAM,GAAG,GAAGb,SAAS,GAAGA,SAAS,EAAG;YAC7D,MAAMc,WAAWP,SAAS,CAACP,MAAM;YACjC,IAAIc,aAAaC,aAAaH,IAAII,QAAQ,CAACF,SAASG,KAAK,GAAG;gBAC1D,OAAOH,SAASI,IAAI;YACtB;QACF;QACA,OAAO,EAAE;IACX;IAEA,MAAMC,UAAU,CAACP,KAAaQ;;QAC5BZ,WAAWa,IAAI,CAAC;YAAET;YAAKQ;QAAO;QAE9B,MAAME,SAASb,gBAAgBc,IAAI,CAACC,CAAAA,QAASZ,IAAII,QAAQ,CAACQ,MAAMP,KAAK;QACrE,MAAMQ,kBAAUH,0BAAAA,OAAQG,OAAO,mBAAIf;QACnC,IAAIe,YAAYV,WAAW;YACzB,OAAOvB,OAAOkC,IAAI,CAChB,IAAIpC,SAASA,QAAQ,CAAC;gBAAEqC,OAAO,IAAIC,MAAMH;gBAAUA;YAAQ;QAE/D;QACA,OAAOjC,OAAOqC,OAAO,CAAClB,OAAOC;IAC/B;IAEA,MAAMkB,aAAa;QACjBX,SAAS,CAACP,KAAaQ,SACrBD,QAAQP,KAAKQ;QACfW,mBAAmB,CAACnB,KAAaQ,SAC/BD,QAAQP,KAAKQ;QACfY,YAAY,CAACpB,KAAaQ,SACxBD,QAAQP,KAAKQ;QACfa,eAAe,IAAMxC,YAAY;QACjCyC,eAAe,IAAMzC,YAAY;IACnC;IAEA,OAAO;QACL0C,WAAW,CAAClB,OAAOC;YACjBX,UAAUc,IAAI,CAAC;gBAAEJ;gBAAOC;YAAK;QAC/B;QACAkB,UAAUX,CAAAA;YACRf,UAAUe;QACZ;QACAY,QAAQ,CAACpB,OAAOQ;YACdhB,gBAAgBY,IAAI,CAAC;gBAAEJ;gBAAOQ;YAAQ;QACxC;QACA,IAAIjB,cAAa;YACf,OAAOA;QACT;QACA,IAAI8B,iBAAgB;YAClB,OAAO9B,UAAU,CAACA,WAAWK,MAAM,GAAG,EAAE;QAC1C;QACA0B,QAAQhD,UAAUiD,IAAI,CACpBhD,OAAOqC,OAAO,CAACC,aACflC,YACA,EAAE,EACFmB;IAEJ;AACF,EAAE"}
1
+ {"version":3,"sources":["../../src/drivers/fake-sql.ts"],"sourcesContent":["/**\n * A fake of the `@effect/sql` driver, not of the repository port.\n *\n * It sits one level *below* `makeSqlRepository`, so the adapter's own code — the\n * filter translation, the clause assembly, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would skip\n * all of it. Same reasoning as {@link makeFakeMongoDb}.\n *\n * What it does **not** do is execute SQL: it hands back canned rows and records\n * the statement each call compiled to, so a spec asserts on the SQL and\n * parameters the adapter produced. Running the shared\n * `describeEntityRepositoryContract` needs a real engine and belongs against real\n * Postgres, the way the driver connection Layers are covered.\n */\n\nimport { Effect } from 'effect';\nimport { SqlError } from 'effect/sql';\nimport { Statement } from 'effect/sql';\n\n/** One statement the adapter executed, already compiled. */\nexport interface FakeSqlExecution {\n /** The compiled SQL text, parameters replaced by `$1`, `$2`, … */\n readonly sql: string;\n readonly params: ReadonlyArray<unknown>;\n}\n\n/** Rows to answer with when a statement's SQL contains `match`. */\nexport interface FakeSqlResponse {\n readonly match: string;\n readonly rows: ReadonlyArray<Record<string, unknown>>;\n}\n\nexport interface FakeSqlClient {\n /**\n * Answers `rows` for any statement whose compiled SQL contains `match`. Later\n * registrations win, so a spec can override a default set up by a helper.\n */\n respondTo(match: string, rows: ReadonlyArray<Record<string, unknown>>): void;\n /**\n * Makes every subsequent execution fail, so the adapter's `EntifixConnError`\n * mapping is reachable.\n */\n failWith(message: string): void;\n /**\n * Makes only the statements containing `match` fail, leaving the rest working\n * — the way a select that succeeds and a count that then fails reaches a\n * distinct error branch.\n */\n failOn(match: string, message: string): void;\n /** Every statement executed, in order, for assertions about what ran. */\n readonly executions: ReadonlyArray<FakeSqlExecution>;\n /** The most recently executed statement. */\n readonly lastExecution: FakeSqlExecution | undefined;\n /**\n * The object to hand to `makeSqlRepository` in place of a real `SqlClient`.\n *\n * Typed `unknown` on purpose — casting at the call site keeps this fake honest\n * that it implements only the constructor surface the adapters use, not the\n * whole `SqlClient` (no transactions, no reactivity, no streaming).\n */\n readonly client: unknown;\n}\n\nconst unsupported = (name: string): never => {\n throw new TypeError(`fake sql: ${name} is not implemented`);\n};\n\n/**\n * The Postgres compiler's configuration, mirrored from `@effect/sql-pg` so the\n * fake emits exactly the dialect production does — `$n` placeholders and\n * double-quoted identifiers — without this package depending on the `pg` driver.\n *\n * `sql.updateValues` and the `PgJson` custom segment are deliberate `TypeError`s\n * rather than approximations: no adapter uses either, and a compiler that\n * half-emitted them would let a spec pass on SQL that real Postgres rejects.\n */\nconst pgCompiler = Statement.makeCompiler({\n dialect: 'pg',\n placeholder: index => `$${index}`,\n onIdentifier: Statement.defaultEscape('\"'),\n onRecordUpdate: () => unsupported('updateValues'),\n onCustom: () => unsupported('custom segments'),\n});\n\n/** Builds an in-memory stand-in for an `@effect/sql` client. */\nexport const makeFakeSqlClient = (\n seed: ReadonlyArray<FakeSqlResponse> = [],\n): FakeSqlClient => {\n const responses: FakeSqlResponse[] = [...seed];\n const executions: FakeSqlExecution[] = [];\n const failuresByMatch: Array<{ match: string; message: string }> = [];\n let failure: string | undefined;\n\n const answer = (sql: string) => {\n // Last registration wins, so an override does not have to remove the\n // default it replaces.\n for (let index = responses.length - 1; index >= 0; index -= 1) {\n const response = responses[index];\n if (response !== undefined && sql.includes(response.match)) {\n return response.rows;\n }\n }\n return [];\n };\n\n const execute = (sql: string, params: ReadonlyArray<unknown>) => {\n executions.push({ sql, params });\n\n const scoped = failuresByMatch.find(entry => sql.includes(entry.match));\n const message = scoped?.message ?? failure;\n if (message !== undefined) {\n return Effect.fail(\n new SqlError.SqlError({\n reason: new SqlError.UnknownError({\n cause: new Error(message),\n message,\n }),\n }),\n );\n }\n return Effect.succeed(answer(sql));\n };\n\n const connection = {\n execute: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeUnprepared: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeRaw: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeValues: () => unsupported('executeValues'),\n executeStream: () => unsupported('executeStream'),\n };\n\n return {\n respondTo: (match, rows) => {\n responses.push({ match, rows });\n },\n failWith: message => {\n failure = message;\n },\n failOn: (match, message) => {\n failuresByMatch.push({ match, message });\n },\n get executions() {\n return executions;\n },\n get lastExecution() {\n return executions[executions.length - 1];\n },\n client: Statement.make(\n Effect.succeed(connection) as never,\n pgCompiler,\n [],\n undefined,\n ),\n };\n};\n"],"names":["Effect","SqlError","Statement","unsupported","name","TypeError","pgCompiler","makeCompiler","dialect","placeholder","index","onIdentifier","defaultEscape","onRecordUpdate","onCustom","makeFakeSqlClient","seed","responses","executions","failuresByMatch","failure","answer","sql","length","response","undefined","includes","match","rows","execute","params","push","scoped","find","entry","message","fail","reason","UnknownError","cause","Error","succeed","connection","executeUnprepared","executeRaw","executeValues","executeStream","respondTo","failWith","failOn","lastExecution","client","make"],"mappings":"AAAA;;;;;;;;;;;;;CAaC,GAED,SAASA,MAAM,QAAQ,SAAS;AAChC,SAASC,QAAQ,QAAQ,aAAa;AACtC,SAASC,SAAS,QAAQ,aAAa;AA8CvC,MAAMC,cAAc,CAACC;IACnB,MAAM,IAAIC,UAAU,CAAC,UAAU,EAAED,KAAK,mBAAmB,CAAC;AAC5D;AAEA;;;;;;;;CAQC,GACD,MAAME,aAAaJ,UAAUK,YAAY,CAAC;IACxCC,SAAS;IACTC,aAAaC,CAAAA,QAAS,CAAC,CAAC,EAAEA,OAAO;IACjCC,cAAcT,UAAUU,aAAa,CAAC;IACtCC,gBAAgB,IAAMV,YAAY;IAClCW,UAAU,IAAMX,YAAY;AAC9B;AAEA,8DAA8D,GAC9D,OAAO,MAAMY,oBAAoB,CAC/BC,OAAuC,EAAE;IAEzC,MAAMC,YAA+B;WAAID;KAAK;IAC9C,MAAME,aAAiC,EAAE;IACzC,MAAMC,kBAA6D,EAAE;IACrE,IAAIC;IAEJ,MAAMC,SAAS,CAACC;QACd,qEAAqE;QACrE,uBAAuB;QACvB,IAAK,IAAIZ,QAAQO,UAAUM,MAAM,GAAG,GAAGb,SAAS,GAAGA,SAAS,EAAG;YAC7D,MAAMc,WAAWP,SAAS,CAACP,MAAM;YACjC,IAAIc,aAAaC,aAAaH,IAAII,QAAQ,CAACF,SAASG,KAAK,GAAG;gBAC1D,OAAOH,SAASI,IAAI;YACtB;QACF;QACA,OAAO,EAAE;IACX;IAEA,MAAMC,UAAU,CAACP,KAAaQ;;QAC5BZ,WAAWa,IAAI,CAAC;YAAET;YAAKQ;QAAO;QAE9B,MAAME,SAASb,gBAAgBc,IAAI,CAACC,CAAAA,QAASZ,IAAII,QAAQ,CAACQ,MAAMP,KAAK;QACrE,MAAMQ,kBAAUH,0BAAAA,OAAQG,OAAO,mBAAIf;QACnC,IAAIe,YAAYV,WAAW;YACzB,OAAOzB,OAAOoC,IAAI,CAChB,IAAInC,SAASA,QAAQ,CAAC;gBACpBoC,QAAQ,IAAIpC,SAASqC,YAAY,CAAC;oBAChCC,OAAO,IAAIC,MAAML;oBACjBA;gBACF;YACF;QAEJ;QACA,OAAOnC,OAAOyC,OAAO,CAACpB,OAAOC;IAC/B;IAEA,MAAMoB,aAAa;QACjBb,SAAS,CAACP,KAAaQ,SACrBD,QAAQP,KAAKQ;QACfa,mBAAmB,CAACrB,KAAaQ,SAC/BD,QAAQP,KAAKQ;QACfc,YAAY,CAACtB,KAAaQ,SACxBD,QAAQP,KAAKQ;QACfe,eAAe,IAAM1C,YAAY;QACjC2C,eAAe,IAAM3C,YAAY;IACnC;IAEA,OAAO;QACL4C,WAAW,CAACpB,OAAOC;YACjBX,UAAUc,IAAI,CAAC;gBAAEJ;gBAAOC;YAAK;QAC/B;QACAoB,UAAUb,CAAAA;YACRf,UAAUe;QACZ;QACAc,QAAQ,CAACtB,OAAOQ;YACdhB,gBAAgBY,IAAI,CAAC;gBAAEJ;gBAAOQ;YAAQ;QACxC;QACA,IAAIjB,cAAa;YACf,OAAOA;QACT;QACA,IAAIgC,iBAAgB;YAClB,OAAOhC,UAAU,CAACA,WAAWK,MAAM,GAAG,EAAE;QAC1C;QACA4B,QAAQjD,UAAUkD,IAAI,CACpBpD,OAAOyC,OAAO,CAACC,aACfpC,YACA,EAAE,EACFmB;IAEJ;AACF,EAAE"}
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/effect/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAIlD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,EAAE,MAAM,EAAE,QAAQ,EAC5C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAC3B,OAAO,CAAC,MAAM,CAAqD,CAAC;AAEvE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAU,MAAM,EAAE,MAAM,EAAE,QAAQ,EAC1D,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAC3B,OAAO,CAAC,MAAM,CAkBhB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,EAAE,MAAM,EAC1C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,0BAA0B,CAAC,EACjE,gBAAe,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAA4B,KAChF,OAAO,CAAC,MAAM,CAA6D,CAAC;AAE/E,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,GAAI,MAAM,EAAE,MAAM,EAC9C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,0BAA0B,CAAC,EACjE,gBAAe,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAA4B,KAChF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAC0B,CAAC;AAE/D,iEAAiE;AACjE,eAAO,MAAM,GAAG,GAAI,MAAM,EAAE,MAAM,EAChC,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,OAAO,CAAC,MAAM,CAA8B,CAAC;AAEhD,wEAAwE;AACxE,eAAO,MAAM,UAAU,GAAU,MAAM,EAAE,MAAM,EAC7C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,OAAO,CAAC,MAAM,CAgBhB,CAAC"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/effect/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAS,MAAM,EAAE,IAAI,EAAE,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAC;AAIjE;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,EAAE,MAAM,EAAE,QAAQ,EAC5C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAC3B,OAAO,CAAC,MAAM,CAAqD,CAAC;AAEvE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAU,MAAM,EAAE,MAAM,EAAE,QAAQ,EAC1D,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAC3B,OAAO,CAAC,MAAM,CAkBhB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,EAAE,MAAM,EAC1C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,0BAA0B,CAAC,EACjE,gBAAe,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAA4B,KAChF,OAAO,CAAC,MAAM,CAA6D,CAAC;AAE/E,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,GAAI,MAAM,EAAE,MAAM,EAC9C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,0BAA0B,CAAC,EACjE,gBAAe,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAA4B,KAChF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAC0B,CAAC;AAE/D,iEAAiE;AACjE,eAAO,MAAM,GAAG,GAAI,MAAM,EAAE,MAAM,EAChC,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,OAAO,CAAC,MAAM,CAA8B,CAAC;AAEhD,wEAAwE;AACxE,eAAO,MAAM,UAAU,GAAU,MAAM,EAAE,MAAM,EAC7C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,OAAO,CAAC,MAAM,CAiBhB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { Effect, Exit } from 'effect';
1
+ import { Cause, Effect, Exit, Option } from 'effect';
2
2
  import { stubConfigurationLayer } from '../doubles/configuration.js';
3
3
  /**
4
4
  * Runs an Effect that still requires `TContext`, providing it from `layer`.
@@ -18,11 +18,11 @@ import { stubConfigurationLayer } from '../doubles/configuration.js';
18
18
  if (Exit.isSuccess(exit)) {
19
19
  throw new Error(`Expected the effect to fail, but it succeeded with: ${JSON.stringify(exit.value)}`);
20
20
  }
21
- const failure = exit.cause;
22
- if (failure._tag === 'Fail') {
23
- return failure.error;
21
+ const failure = Cause.findErrorOption(exit.cause);
22
+ if (Option.isSome(failure)) {
23
+ return failure.value;
24
24
  }
25
- throw new Error(`Expected a typed failure, but the effect died with: ${String(failure)}`);
25
+ throw new Error(`Expected a typed failure, but the effect died with: ${String(exit.cause)}`);
26
26
  };
27
27
  /**
28
28
  * Runs a repository Effect.
@@ -39,8 +39,9 @@ import { stubConfigurationLayer } from '../doubles/configuration.js';
39
39
  if (Exit.isSuccess(exit)) {
40
40
  throw new Error(`Expected the effect to fail, but it succeeded with: ${JSON.stringify(exit.value)}`);
41
41
  }
42
- if (exit.cause._tag === 'Fail') {
43
- return exit.cause.error;
42
+ const failure = Cause.findErrorOption(exit.cause);
43
+ if (Option.isSome(failure)) {
44
+ return failure.value;
44
45
  }
45
46
  throw new Error(`Expected a typed failure, but the effect died with: ${String(exit.cause)}`);
46
47
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/effect/run.ts"],"sourcesContent":["import type { ConfigurationRepositoryTag } from '@entifix/business';\nimport { Effect, Exit, type Layer } from 'effect';\n\nimport { stubConfigurationLayer } from '../doubles/configuration.js';\n\n/**\n * Runs an Effect that still requires `TContext`, providing it from `layer`.\n *\n * Use-cases are `Effect.gen` blocks yielding Context tags; every spec would\n * otherwise repeat the same `Effect.runPromise(uc.pipe(Effect.provide(…)))`\n * incantation.\n */\nexport const runUC = <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, layer));\n\n/**\n * Runs an Effect expected to fail and returns its typed error.\n *\n * Asserting on the error value beats asserting that a promise rejected: it is\n * the difference between \"something went wrong\" and \"it failed with the\n * `EntifixLockError` that the facade turns into a 409\".\n */\nexport const expectFailure = async <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(Effect.provide(effect, layer));\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n\n const failure = exit.cause;\n if (failure._tag === 'Fail') {\n return failure.error;\n }\n throw new Error(\n `Expected a typed failure, but the effect died with: ${String(failure)}`,\n );\n};\n\n/**\n * Runs a repository Effect.\n *\n * Every `EntityRepository` method declares `ConfigurationRepositoryTag` in its\n * requirement channel — the REST-backed adapters resolve their endpoints\n * through it — so even a repository that needs no configuration has to be given\n * one. `configuration` supplies it for the adapters that do.\n */\nexport const runRepository = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>,\n configuration: Layer.Layer<ConfigurationRepositoryTag> = stubConfigurationLayer(),\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, configuration));\n\n/** {@link runRepository}, for an Effect expected to fail. */\nexport const runRepositoryExit = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>,\n configuration: Layer.Layer<ConfigurationRepositoryTag> = stubConfigurationLayer(),\n): Promise<Exit.Exit<TValue, TError>> =>\n Effect.runPromiseExit(Effect.provide(effect, configuration));\n\n/** Runs an Effect that requires nothing, returning its value. */\nexport const run = <TValue, TError>(\n effect: Effect.Effect<TValue, TError>,\n): Promise<TValue> => Effect.runPromise(effect);\n\n/** Runs an Effect that requires nothing and returns its typed error. */\nexport const runFailure = async <TValue, TError>(\n effect: Effect.Effect<TValue, TError>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(effect);\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n if (exit.cause._tag === 'Fail') {\n return exit.cause.error;\n }\n throw new Error(\n `Expected a typed failure, but the effect died with: ${String(exit.cause)}`,\n );\n};\n"],"names":["Effect","Exit","stubConfigurationLayer","runUC","effect","layer","runPromise","provide","expectFailure","exit","runPromiseExit","isSuccess","Error","JSON","stringify","value","failure","cause","_tag","error","String","runRepository","configuration","runRepositoryExit","run","runFailure"],"mappings":"AACA,SAASA,MAAM,EAAEC,IAAI,QAAoB,SAAS;AAElD,SAASC,sBAAsB,QAAQ,8BAA8B;AAErE;;;;;;CAMC,GACD,OAAO,MAAMC,QAAQ,CACnBC,QACAC,QACoBL,OAAOM,UAAU,CAACN,OAAOO,OAAO,CAACH,QAAQC,QAAQ;AAEvE;;;;;;CAMC,GACD,OAAO,MAAMG,gBAAgB,OAC3BJ,QACAC;IAEA,MAAMI,OAAO,MAAMT,OAAOU,cAAc,CAACV,OAAOO,OAAO,CAACH,QAAQC;IAEhE,IAAIJ,KAAKU,SAAS,CAACF,OAAO;QACxB,MAAM,IAAIG,MACR,CAAC,oDAAoD,EAAEC,KAAKC,SAAS,CACnEL,KAAKM,KAAK,GACT;IAEP;IAEA,MAAMC,UAAUP,KAAKQ,KAAK;IAC1B,IAAID,QAAQE,IAAI,KAAK,QAAQ;QAC3B,OAAOF,QAAQG,KAAK;IACtB;IACA,MAAM,IAAIP,MACR,CAAC,oDAAoD,EAAEQ,OAAOJ,UAAU;AAE5E,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMK,gBAAgB,CAC3BjB,QACAkB,gBAAyDpB,wBAAwB,GAC7DF,OAAOM,UAAU,CAACN,OAAOO,OAAO,CAACH,QAAQkB,gBAAgB;AAE/E,2DAA2D,GAC3D,OAAO,MAAMC,oBAAoB,CAC/BnB,QACAkB,gBAAyDpB,wBAAwB,GAEjFF,OAAOU,cAAc,CAACV,OAAOO,OAAO,CAACH,QAAQkB,gBAAgB;AAE/D,+DAA+D,GAC/D,OAAO,MAAME,MAAM,CACjBpB,SACoBJ,OAAOM,UAAU,CAACF,QAAQ;AAEhD,sEAAsE,GACtE,OAAO,MAAMqB,aAAa,OACxBrB;IAEA,MAAMK,OAAO,MAAMT,OAAOU,cAAc,CAACN;IAEzC,IAAIH,KAAKU,SAAS,CAACF,OAAO;QACxB,MAAM,IAAIG,MACR,CAAC,oDAAoD,EAAEC,KAAKC,SAAS,CACnEL,KAAKM,KAAK,GACT;IAEP;IACA,IAAIN,KAAKQ,KAAK,CAACC,IAAI,KAAK,QAAQ;QAC9B,OAAOT,KAAKQ,KAAK,CAACE,KAAK;IACzB;IACA,MAAM,IAAIP,MACR,CAAC,oDAAoD,EAAEQ,OAAOX,KAAKQ,KAAK,GAAG;AAE/E,EAAE"}
1
+ {"version":3,"sources":["../../src/effect/run.ts"],"sourcesContent":["import type { ConfigurationRepositoryTag } from '@entifix/business';\nimport { Cause, Effect, Exit, type Layer, Option } from 'effect';\n\nimport { stubConfigurationLayer } from '../doubles/configuration.js';\n\n/**\n * Runs an Effect that still requires `TContext`, providing it from `layer`.\n *\n * Use-cases are `Effect.gen` blocks yielding Context tags; every spec would\n * otherwise repeat the same `Effect.runPromise(uc.pipe(Effect.provide(…)))`\n * incantation.\n */\nexport const runUC = <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, layer));\n\n/**\n * Runs an Effect expected to fail and returns its typed error.\n *\n * Asserting on the error value beats asserting that a promise rejected: it is\n * the difference between \"something went wrong\" and \"it failed with the\n * `EntifixLockError` that the facade turns into a 409\".\n */\nexport const expectFailure = async <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(Effect.provide(effect, layer));\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n\n const failure = Cause.findErrorOption(exit.cause);\n if (Option.isSome(failure)) {\n return failure.value;\n }\n throw new Error(\n `Expected a typed failure, but the effect died with: ${String(exit.cause)}`,\n );\n};\n\n/**\n * Runs a repository Effect.\n *\n * Every `EntityRepository` method declares `ConfigurationRepositoryTag` in its\n * requirement channel — the REST-backed adapters resolve their endpoints\n * through it — so even a repository that needs no configuration has to be given\n * one. `configuration` supplies it for the adapters that do.\n */\nexport const runRepository = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>,\n configuration: Layer.Layer<ConfigurationRepositoryTag> = stubConfigurationLayer(),\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, configuration));\n\n/** {@link runRepository}, for an Effect expected to fail. */\nexport const runRepositoryExit = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>,\n configuration: Layer.Layer<ConfigurationRepositoryTag> = stubConfigurationLayer(),\n): Promise<Exit.Exit<TValue, TError>> =>\n Effect.runPromiseExit(Effect.provide(effect, configuration));\n\n/** Runs an Effect that requires nothing, returning its value. */\nexport const run = <TValue, TError>(\n effect: Effect.Effect<TValue, TError>,\n): Promise<TValue> => Effect.runPromise(effect);\n\n/** Runs an Effect that requires nothing and returns its typed error. */\nexport const runFailure = async <TValue, TError>(\n effect: Effect.Effect<TValue, TError>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(effect);\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n const failure = Cause.findErrorOption(exit.cause);\n if (Option.isSome(failure)) {\n return failure.value;\n }\n throw new Error(\n `Expected a typed failure, but the effect died with: ${String(exit.cause)}`,\n );\n};\n"],"names":["Cause","Effect","Exit","Option","stubConfigurationLayer","runUC","effect","layer","runPromise","provide","expectFailure","exit","runPromiseExit","isSuccess","Error","JSON","stringify","value","failure","findErrorOption","cause","isSome","String","runRepository","configuration","runRepositoryExit","run","runFailure"],"mappings":"AACA,SAASA,KAAK,EAAEC,MAAM,EAAEC,IAAI,EAAcC,MAAM,QAAQ,SAAS;AAEjE,SAASC,sBAAsB,QAAQ,8BAA8B;AAErE;;;;;;CAMC,GACD,OAAO,MAAMC,QAAQ,CACnBC,QACAC,QACoBN,OAAOO,UAAU,CAACP,OAAOQ,OAAO,CAACH,QAAQC,QAAQ;AAEvE;;;;;;CAMC,GACD,OAAO,MAAMG,gBAAgB,OAC3BJ,QACAC;IAEA,MAAMI,OAAO,MAAMV,OAAOW,cAAc,CAACX,OAAOQ,OAAO,CAACH,QAAQC;IAEhE,IAAIL,KAAKW,SAAS,CAACF,OAAO;QACxB,MAAM,IAAIG,MACR,CAAC,oDAAoD,EAAEC,KAAKC,SAAS,CACnEL,KAAKM,KAAK,GACT;IAEP;IAEA,MAAMC,UAAUlB,MAAMmB,eAAe,CAACR,KAAKS,KAAK;IAChD,IAAIjB,OAAOkB,MAAM,CAACH,UAAU;QAC1B,OAAOA,QAAQD,KAAK;IACtB;IACA,MAAM,IAAIH,MACR,CAAC,oDAAoD,EAAEQ,OAAOX,KAAKS,KAAK,GAAG;AAE/E,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMG,gBAAgB,CAC3BjB,QACAkB,gBAAyDpB,wBAAwB,GAC7DH,OAAOO,UAAU,CAACP,OAAOQ,OAAO,CAACH,QAAQkB,gBAAgB;AAE/E,2DAA2D,GAC3D,OAAO,MAAMC,oBAAoB,CAC/BnB,QACAkB,gBAAyDpB,wBAAwB,GAEjFH,OAAOW,cAAc,CAACX,OAAOQ,OAAO,CAACH,QAAQkB,gBAAgB;AAE/D,+DAA+D,GAC/D,OAAO,MAAME,MAAM,CACjBpB,SACoBL,OAAOO,UAAU,CAACF,QAAQ;AAEhD,sEAAsE,GACtE,OAAO,MAAMqB,aAAa,OACxBrB;IAEA,MAAMK,OAAO,MAAMV,OAAOW,cAAc,CAACN;IAEzC,IAAIJ,KAAKW,SAAS,CAACF,OAAO;QACxB,MAAM,IAAIG,MACR,CAAC,oDAAoD,EAAEC,KAAKC,SAAS,CACnEL,KAAKM,KAAK,GACT;IAEP;IACA,MAAMC,UAAUlB,MAAMmB,eAAe,CAACR,KAAKS,KAAK;IAChD,IAAIjB,OAAOkB,MAAM,CAACH,UAAU;QAC1B,OAAOA,QAAQD,KAAK;IACtB;IACA,MAAM,IAAIH,MACR,CAAC,oDAAoD,EAAEQ,OAAOX,KAAKS,KAAK,GAAG;AAE/E,EAAE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/testing-unit",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Test doubles for entifix: in-memory ports, driver fakes for Mongo, Redis, RabbitMQ and SQL, MSW handlers and port contract suites.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -76,25 +76,20 @@
76
76
  }
77
77
  },
78
78
  "dependencies": {
79
- "@effect/sql": "0.52.1",
80
79
  "@swc/helpers": "0.5.23",
81
- "@entifix/transactions": "0.1.3",
82
- "@entifix/business": "0.1.3",
83
- "@entifix/core": "0.1.3"
80
+ "@entifix/core": "0.2.0",
81
+ "@entifix/business": "0.2.0",
82
+ "@entifix/transactions": "0.2.0"
84
83
  },
85
84
  "peerDependencies": {
86
- "@effect/vitest": ">=0.30 <1",
87
85
  "@testing-library/react": "*",
88
- "effect": ">=3.22 <4",
86
+ "effect": "^4.0.5",
89
87
  "msw": "*",
90
88
  "react": "*",
91
89
  "vitest": "*",
92
- "@entifix/react-controls": "0.1.3"
90
+ "@entifix/react-controls": "0.2.0"
93
91
  },
94
92
  "peerDependenciesMeta": {
95
- "@effect/vitest": {
96
- "optional": true
97
- },
98
93
  "@entifix/react-controls": {
99
94
  "optional": true
100
95
  },
@@ -115,7 +110,6 @@
115
110
  }
116
111
  },
117
112
  "devDependencies": {
118
- "@effect/vitest": "^0.30.0",
119
- "effect": "3.22.1"
113
+ "effect": "4.0.5"
120
114
  }
121
115
  }