@entifix/testing-unit 0.1.0-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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/builders/entity-builder.d.ts +26 -0
  4. package/dist/builders/entity-builder.d.ts.map +1 -0
  5. package/dist/builders/entity-builder.js +30 -0
  6. package/dist/builders/entity-builder.js.map +1 -0
  7. package/dist/contracts/entity-repository.contract.d.ts +36 -0
  8. package/dist/contracts/entity-repository.contract.d.ts.map +1 -0
  9. package/dist/contracts/entity-repository.contract.js +273 -0
  10. package/dist/contracts/entity-repository.contract.js.map +1 -0
  11. package/dist/contracts/index.d.ts +3 -0
  12. package/dist/contracts/index.d.ts.map +1 -0
  13. package/dist/contracts/index.js +4 -0
  14. package/dist/contracts/index.js.map +1 -0
  15. package/dist/contracts/transaction-ports.contract.d.ts +44 -0
  16. package/dist/contracts/transaction-ports.contract.d.ts.map +1 -0
  17. package/dist/contracts/transaction-ports.contract.js +203 -0
  18. package/dist/contracts/transaction-ports.contract.js.map +1 -0
  19. package/dist/doubles/configuration.d.ts +36 -0
  20. package/dist/doubles/configuration.d.ts.map +1 -0
  21. package/dist/doubles/configuration.js +31 -0
  22. package/dist/doubles/configuration.js.map +1 -0
  23. package/dist/doubles/entity-repository.d.ts +24 -0
  24. package/dist/doubles/entity-repository.d.ts.map +1 -0
  25. package/dist/doubles/entity-repository.js +159 -0
  26. package/dist/doubles/entity-repository.js.map +1 -0
  27. package/dist/doubles/link-resolver.d.ts +17 -0
  28. package/dist/doubles/link-resolver.d.ts.map +1 -0
  29. package/dist/doubles/link-resolver.js +34 -0
  30. package/dist/doubles/link-resolver.js.map +1 -0
  31. package/dist/doubles/transaction-ports.d.ts +70 -0
  32. package/dist/doubles/transaction-ports.d.ts.map +1 -0
  33. package/dist/doubles/transaction-ports.js +145 -0
  34. package/dist/doubles/transaction-ports.js.map +1 -0
  35. package/dist/drivers/fake-amqp.d.ts +91 -0
  36. package/dist/drivers/fake-amqp.d.ts.map +1 -0
  37. package/dist/drivers/fake-amqp.js +164 -0
  38. package/dist/drivers/fake-amqp.js.map +1 -0
  39. package/dist/drivers/fake-mongo.d.ts +59 -0
  40. package/dist/drivers/fake-mongo.d.ts.map +1 -0
  41. package/dist/drivers/fake-mongo.js +603 -0
  42. package/dist/drivers/fake-mongo.js.map +1 -0
  43. package/dist/drivers/fake-redis.d.ts +32 -0
  44. package/dist/drivers/fake-redis.d.ts.map +1 -0
  45. package/dist/drivers/fake-redis.js +164 -0
  46. package/dist/drivers/fake-redis.js.map +1 -0
  47. package/dist/drivers/fake-sql.d.ts +58 -0
  48. package/dist/drivers/fake-sql.d.ts.map +1 -0
  49. package/dist/drivers/fake-sql.js +102 -0
  50. package/dist/drivers/fake-sql.js.map +1 -0
  51. package/dist/drivers/index.d.ts +5 -0
  52. package/dist/drivers/index.d.ts.map +1 -0
  53. package/dist/drivers/index.js +6 -0
  54. package/dist/drivers/index.js.map +1 -0
  55. package/dist/effect/run.d.ts +34 -0
  56. package/dist/effect/run.d.ts.map +1 -0
  57. package/dist/effect/run.js +48 -0
  58. package/dist/effect/run.js.map +1 -0
  59. package/dist/http/entity-handlers.d.ts +37 -0
  60. package/dist/http/entity-handlers.d.ts.map +1 -0
  61. package/dist/http/entity-handlers.js +92 -0
  62. package/dist/http/entity-handlers.js.map +1 -0
  63. package/dist/http/index.d.ts +3 -0
  64. package/dist/http/index.d.ts.map +1 -0
  65. package/dist/http/index.js +4 -0
  66. package/dist/http/index.js.map +1 -0
  67. package/dist/http/server.d.ts +19 -0
  68. package/dist/http/server.d.ts.map +1 -0
  69. package/dist/http/server.js +31 -0
  70. package/dist/http/server.js.map +1 -0
  71. package/dist/index.d.ts +7 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +8 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/react/index.d.ts +3 -0
  76. package/dist/react/index.d.ts.map +1 -0
  77. package/dist/react/index.js +4 -0
  78. package/dist/react/index.js.map +1 -0
  79. package/dist/react/render.d.ts +30 -0
  80. package/dist/react/render.d.ts.map +1 -0
  81. package/dist/react/render.js +38 -0
  82. package/dist/react/render.js.map +1 -0
  83. package/dist/react/ui-preferences.d.ts +17 -0
  84. package/dist/react/ui-preferences.d.ts.map +1 -0
  85. package/dist/react/ui-preferences.js +29 -0
  86. package/dist/react/ui-preferences.js.map +1 -0
  87. package/package.json +122 -0
@@ -0,0 +1,203 @@
1
+ import { it as effectIt } from '@effect/vitest';
2
+ import { Duration, Effect, Exit, Fiber, TestClock } from 'effect';
3
+ import { describe, expect, it } from 'vitest';
4
+ const run = (effect)=>Effect.runPromise(effect);
5
+ /**
6
+ * What every {@link LockService} must guarantee: exclusivity while held,
7
+ * availability after release, and a release that only frees the caller's own
8
+ * lock. The last one is why `release` takes a handle rather than a key — a lock
9
+ * that lapsed and was re-acquired by someone else must not be freed by the
10
+ * previous holder.
11
+ */ export const describeLockServiceContract = (name, makeLockService)=>{
12
+ describe(`LockService contract: ${name}`, ()=>{
13
+ it('acquires an uncontended key', async ()=>{
14
+ const locks = await makeLockService();
15
+ const handle = await run(locks.acquire('contract:free'));
16
+ expect(handle.key).toBe('contract:free');
17
+ expect(handle.token).toBeTruthy();
18
+ });
19
+ // An implementation may retry before giving up (the Redis adapter waits out
20
+ // a bounded budget), so this runs on a TestClock: the assertion is about
21
+ // exclusivity, and it should not cost seconds of wall clock to make.
22
+ effectIt.effect('refuses a key that is already held', ()=>Effect.gen(function*() {
23
+ const locks = yield* Effect.promise(async ()=>makeLockService());
24
+ yield* locks.acquire('contract:taken');
25
+ const fiber = yield* Effect.fork(locks.acquire('contract:taken'));
26
+ yield* TestClock.adjust(Duration.minutes(1));
27
+ const exit = yield* Fiber.await(fiber);
28
+ expect(Exit.isFailure(exit)).toBe(true);
29
+ }));
30
+ it('makes the key available again after release', async ()=>{
31
+ const locks = await makeLockService();
32
+ const handle = await run(locks.acquire('contract:cycle'));
33
+ await run(locks.release(handle));
34
+ const reacquired = await run(locks.acquire('contract:cycle'));
35
+ expect(reacquired.key).toBe('contract:cycle');
36
+ });
37
+ effectIt.effect('ignores a release whose token does not match', ()=>Effect.gen(function*() {
38
+ const locks = yield* Effect.promise(async ()=>makeLockService());
39
+ const handle = yield* locks.acquire('contract:stolen');
40
+ yield* locks.release({
41
+ key: handle.key,
42
+ token: 'not-mine'
43
+ });
44
+ // Still held, so a fresh acquisition must fail.
45
+ const fiber = yield* Effect.fork(locks.acquire('contract:stolen'));
46
+ yield* TestClock.adjust(Duration.minutes(1));
47
+ const exit = yield* Fiber.await(fiber);
48
+ expect(Exit.isFailure(exit)).toBe(true);
49
+ }));
50
+ });
51
+ };
52
+ /**
53
+ * What every {@link SequenceService} must guarantee: strictly increasing values
54
+ * per name, never repeating, with names independent of one another. This is the
55
+ * property incremental codes rest on.
56
+ */ export const describeSequenceServiceContract = (name, makeSequenceService)=>{
57
+ describe(`SequenceService contract: ${name}`, ()=>{
58
+ it('starts at 1', async ()=>{
59
+ const sequences = await makeSequenceService();
60
+ expect(await run(sequences.next('contract:fresh'))).toBe(1);
61
+ });
62
+ it('never returns the same value twice for a name', async ()=>{
63
+ const sequences = await makeSequenceService();
64
+ const drawn = [
65
+ await run(sequences.next('contract:codes')),
66
+ await run(sequences.next('contract:codes')),
67
+ await run(sequences.next('contract:codes'))
68
+ ];
69
+ expect(drawn).toEqual([
70
+ 1,
71
+ 2,
72
+ 3
73
+ ]);
74
+ expect(new Set(drawn).size).toBe(drawn.length);
75
+ });
76
+ it('keeps separate names independent', async ()=>{
77
+ const sequences = await makeSequenceService();
78
+ await run(sequences.next('contract:a'));
79
+ await run(sequences.next('contract:a'));
80
+ expect(await run(sequences.next('contract:b'))).toBe(1);
81
+ });
82
+ });
83
+ };
84
+ /** Every subscription in this contract, unless a test says otherwise. */ const ANY_WIDGET_EVENT = 'contract.widget.*';
85
+ /**
86
+ * A `work` subscription for `pattern`. The mode and the ceiling are broker
87
+ * facts with no bearing on routing, so a test about routing states them once
88
+ * here rather than repeating them at every call.
89
+ */ const on = (pattern)=>({
90
+ slice: 'contract',
91
+ pattern,
92
+ mode: 'work',
93
+ maxAttempts: 5
94
+ });
95
+ /**
96
+ * A handler that records what it is given. Shared by both routing tests so the
97
+ * "nothing was delivered" case asserts on the *same* handler the positive case
98
+ * proves works — otherwise it could pass against a handler that was simply
99
+ * never wired up.
100
+ */ const collectInto = (sink)=>(event)=>Effect.sync(()=>{
101
+ sink.push(event);
102
+ });
103
+ const anEvent = (id, name = 'contract.widget.created')=>({
104
+ name,
105
+ id,
106
+ source: 'contract-slice',
107
+ at: new Date('2026-01-01T00:00:00.000Z').toISOString(),
108
+ correlationId: id,
109
+ data: {
110
+ entity: 'contract-widget'
111
+ }
112
+ });
113
+ /**
114
+ * What every {@link EventBus} must guarantee: what goes out comes back
115
+ * unchanged, subscribers receive deliveries, and a subscriber receives only what
116
+ * its pattern matches. The transports differ wildly (an array here, a topic
117
+ * exchange in RabbitMQ) — the observable contract does not.
118
+ */ export const describeEventBusContract = (name, makeHarness)=>{
119
+ describe(`EventBus contract: ${name}`, ()=>{
120
+ it('publishes an event verbatim', async ()=>{
121
+ const harness = await makeHarness();
122
+ const event = anEvent('tx-1');
123
+ await run(harness.bus.publish(event));
124
+ expect(harness.published()).toEqual([
125
+ event
126
+ ]);
127
+ });
128
+ it('delivers published events to a subscriber', async ()=>{
129
+ const harness = await makeHarness();
130
+ const received = [];
131
+ await run(harness.bus.subscribe(on(ANY_WIDGET_EVENT), collectInto(received)));
132
+ await harness.deliver(anEvent('tx-2'));
133
+ expect(received.map((event)=>event.id)).toEqual([
134
+ 'tx-2'
135
+ ]);
136
+ });
137
+ // Routing is the transport's job. A bus that hands every subscriber every
138
+ // event lets a consumer pass while receiving traffic it never bound to —
139
+ // which is precisely the fault the fanout exchange had.
140
+ it('delivers nothing to a subscriber whose pattern does not match', async ()=>{
141
+ const harness = await makeHarness();
142
+ const received = [];
143
+ await run(harness.bus.subscribe(on('contract.other.*'), collectInto(received)));
144
+ await harness.deliver(anEvent('tx-5'));
145
+ expect(received).toEqual([]);
146
+ });
147
+ it('preserves publication order', async ()=>{
148
+ const harness = await makeHarness();
149
+ await run(harness.bus.publish(anEvent('tx-3')));
150
+ await run(harness.bus.publish(anEvent('tx-4')));
151
+ expect(harness.published().map((event)=>event.id)).toEqual([
152
+ 'tx-3',
153
+ 'tx-4'
154
+ ]);
155
+ });
156
+ });
157
+ };
158
+ /**
159
+ * What every {@link TransactionInbox} must guarantee: a first claim is granted,
160
+ * every repeat of it is refused, and one consumer's claim says nothing about
161
+ * another's.
162
+ *
163
+ * That last one is the property with teeth. Two consumers legitimately process
164
+ * the same event — the saga tracker's fold and the SSE hub both bind
165
+ * `transaction.*` — so an implementation keyed on `eventId` alone passes the
166
+ * first two cases and silently starves every consumer but the first. It looks
167
+ * like exactly-once from inside one consumer, which is why it is asserted here
168
+ * rather than left to each adapter's own spec.
169
+ */ export const describeTransactionInboxContract = (name, makeInbox)=>{
170
+ describe(`TransactionInbox contract: ${name}`, ()=>{
171
+ it('grants a first claim', async ()=>{
172
+ const inbox = await makeInbox('contract.consumer');
173
+ expect(await run(inbox.claim('tx-1:completed'))).toBe('claimed');
174
+ });
175
+ it('refuses a redelivery of the same event', async ()=>{
176
+ const inbox = await makeInbox('contract.consumer');
177
+ await run(inbox.claim('tx-2:completed'));
178
+ expect(await run(inbox.claim('tx-2:completed'))).toBe('duplicate');
179
+ });
180
+ it('refuses every repeat, not only the second', async ()=>{
181
+ const inbox = await makeInbox('contract.consumer');
182
+ await run(inbox.claim('tx-3:completed'));
183
+ expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');
184
+ expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');
185
+ });
186
+ it('claims each step of one transaction separately', async ()=>{
187
+ // `<transactionId>:<step>`, never the transaction id alone: one
188
+ // transaction emits up to three messages, and keying on the correlation
189
+ // id would make `completed` read as a redelivery of `accepted`.
190
+ const inbox = await makeInbox('contract.consumer');
191
+ expect(await run(inbox.claim('tx-4:accepted'))).toBe('claimed');
192
+ expect(await run(inbox.claim('tx-4:completed'))).toBe('claimed');
193
+ });
194
+ it('does not let one consumer consume another consumer’s claim', async ()=>{
195
+ const first = await makeInbox('contract.first');
196
+ const second = await makeInbox('contract.second');
197
+ await run(first.claim('tx-5:completed'));
198
+ expect(await run(second.claim('tx-5:completed'))).toBe('claimed');
199
+ });
200
+ });
201
+ };
202
+
203
+ //# sourceMappingURL=transaction-ports.contract.js.map
@@ -0,0 +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":"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"}
@@ -0,0 +1,36 @@
1
+ import { ConfigurationRepositoryTag } from '@entifix/business';
2
+ import { type ConfigurationClient, type ConfigurationPlain } from '@entifix/core';
3
+ import { Layer } from 'effect';
4
+ /**
5
+ * The group the REST adapters read their entity URIs from when
6
+ * `BuildEntityRestOptions.uriConfig.group` is left unset.
7
+ */
8
+ export declare const DEFAULT_URI_GROUP = "restUri";
9
+ /**
10
+ * A {@link ConfigurationClient} over a plain `{ group: [{ key, value }] }`
11
+ * literal. This is a stub, not a fake: it answers reads and nothing else.
12
+ *
13
+ * Backed by core's own `ConfigurationClientInMemory`, so the parsing rules under
14
+ * test are the production ones rather than a second implementation that could
15
+ * drift.
16
+ */
17
+ export declare const makeStubConfigurationClient: (plain?: ConfigurationPlain) => ConfigurationClient;
18
+ /**
19
+ * The common case for REST adapter specs: every entity resolves under one base
20
+ * URL, keyed the way `buildEntityBaseUrl` looks them up.
21
+ *
22
+ * `entities` maps an entity `key` to its endpoint; passing `{ product: '…' }`
23
+ * satisfies a `uriConfig.key` of `'service.[entity]'`.
24
+ */
25
+ export declare const makeStubUriConfigurationClient: (entities: Record<string, string>, { keyTemplate, group }?: {
26
+ keyTemplate?: string | undefined;
27
+ group?: string | undefined;
28
+ }) => ConfigurationClient;
29
+ /** Provides {@link ConfigurationRepositoryTag} from a plain configuration. */
30
+ export declare const stubConfigurationLayer: (plain?: ConfigurationPlain) => Layer.Layer<ConfigurationRepositoryTag, never, never>;
31
+ /** Provides {@link ConfigurationRepositoryTag} from an entity-to-URI mapping. */
32
+ export declare const stubUriConfigurationLayer: (entities: Record<string, string>, options?: {
33
+ keyTemplate?: string;
34
+ group?: string;
35
+ }) => Layer.Layer<ConfigurationRepositoryTag, never, never>;
36
+ //# sourceMappingURL=configuration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configuration.d.ts","sourceRoot":"","sources":["../../src/doubles/configuration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EACL,KAAK,mBAAmB,EAExB,KAAK,kBAAkB,EACxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAE/B;;;GAGG;AACH,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GACtC,QAAO,kBAAuB,KAC7B,mBAA6D,CAAC;AAEjE;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,GACzC,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC;;;CAA4D,KAC3D,mBAMC,CAAC;AAEL,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,GAAI,QAAO,kBAAuB,0DACU,CAAC;AAEhF,iFAAiF;AACjF,eAAO,MAAM,yBAAyB,GACpC,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,UAAU;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,0DAKjD,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { ConfigurationRepositoryTag } from '@entifix/business';
2
+ import { ConfigurationClientInMemory } from '@entifix/core';
3
+ import { Layer } from 'effect';
4
+ /**
5
+ * The group the REST adapters read their entity URIs from when
6
+ * `BuildEntityRestOptions.uriConfig.group` is left unset.
7
+ */ export const DEFAULT_URI_GROUP = 'restUri';
8
+ /**
9
+ * A {@link ConfigurationClient} over a plain `{ group: [{ key, value }] }`
10
+ * literal. This is a stub, not a fake: it answers reads and nothing else.
11
+ *
12
+ * Backed by core's own `ConfigurationClientInMemory`, so the parsing rules under
13
+ * test are the production ones rather than a second implementation that could
14
+ * drift.
15
+ */ export const makeStubConfigurationClient = (plain = {})=>new ConfigurationClientInMemory(plain);
16
+ /**
17
+ * The common case for REST adapter specs: every entity resolves under one base
18
+ * URL, keyed the way `buildEntityBaseUrl` looks them up.
19
+ *
20
+ * `entities` maps an entity `key` to its endpoint; passing `{ product: '…' }`
21
+ * satisfies a `uriConfig.key` of `'service.[entity]'`.
22
+ */ export const makeStubUriConfigurationClient = (entities, { keyTemplate = '[entity]', group = DEFAULT_URI_GROUP } = {})=>makeStubConfigurationClient({
23
+ [group]: Object.entries(entities).map(([entity, uri])=>({
24
+ key: keyTemplate.replace('[entity]', entity),
25
+ value: uri
26
+ }))
27
+ });
28
+ /** Provides {@link ConfigurationRepositoryTag} from a plain configuration. */ export const stubConfigurationLayer = (plain = {})=>Layer.succeed(ConfigurationRepositoryTag, makeStubConfigurationClient(plain));
29
+ /** Provides {@link ConfigurationRepositoryTag} from an entity-to-URI mapping. */ export const stubUriConfigurationLayer = (entities, options)=>Layer.succeed(ConfigurationRepositoryTag, makeStubUriConfigurationClient(entities, options));
30
+
31
+ //# sourceMappingURL=configuration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/doubles/configuration.ts"],"sourcesContent":["import { ConfigurationRepositoryTag } from '@entifix/business';\nimport {\n type ConfigurationClient,\n ConfigurationClientInMemory,\n type ConfigurationPlain,\n} from '@entifix/core';\nimport { Layer } from 'effect';\n\n/**\n * The group the REST adapters read their entity URIs from when\n * `BuildEntityRestOptions.uriConfig.group` is left unset.\n */\nexport const DEFAULT_URI_GROUP = 'restUri';\n\n/**\n * A {@link ConfigurationClient} over a plain `{ group: [{ key, value }] }`\n * literal. This is a stub, not a fake: it answers reads and nothing else.\n *\n * Backed by core's own `ConfigurationClientInMemory`, so the parsing rules under\n * test are the production ones rather than a second implementation that could\n * drift.\n */\nexport const makeStubConfigurationClient = (\n plain: ConfigurationPlain = {},\n): ConfigurationClient => new ConfigurationClientInMemory(plain);\n\n/**\n * The common case for REST adapter specs: every entity resolves under one base\n * URL, keyed the way `buildEntityBaseUrl` looks them up.\n *\n * `entities` maps an entity `key` to its endpoint; passing `{ product: '…' }`\n * satisfies a `uriConfig.key` of `'service.[entity]'`.\n */\nexport const makeStubUriConfigurationClient = (\n entities: Record<string, string>,\n { keyTemplate = '[entity]', group = DEFAULT_URI_GROUP } = {},\n): ConfigurationClient =>\n makeStubConfigurationClient({\n [group]: Object.entries(entities).map(([entity, uri]) => ({\n key: keyTemplate.replace('[entity]', entity),\n value: uri,\n })),\n });\n\n/** Provides {@link ConfigurationRepositoryTag} from a plain configuration. */\nexport const stubConfigurationLayer = (plain: ConfigurationPlain = {}) =>\n Layer.succeed(ConfigurationRepositoryTag, makeStubConfigurationClient(plain));\n\n/** Provides {@link ConfigurationRepositoryTag} from an entity-to-URI mapping. */\nexport const stubUriConfigurationLayer = (\n entities: Record<string, string>,\n options?: { keyTemplate?: string; group?: string },\n) =>\n Layer.succeed(\n ConfigurationRepositoryTag,\n makeStubUriConfigurationClient(entities, options),\n );\n"],"names":["ConfigurationRepositoryTag","ConfigurationClientInMemory","Layer","DEFAULT_URI_GROUP","makeStubConfigurationClient","plain","makeStubUriConfigurationClient","entities","keyTemplate","group","Object","entries","map","entity","uri","key","replace","value","stubConfigurationLayer","succeed","stubUriConfigurationLayer","options"],"mappings":"AAAA,SAASA,0BAA0B,QAAQ,oBAAoB;AAC/D,SAEEC,2BAA2B,QAEtB,gBAAgB;AACvB,SAASC,KAAK,QAAQ,SAAS;AAE/B;;;CAGC,GACD,OAAO,MAAMC,oBAAoB,UAAU;AAE3C;;;;;;;CAOC,GACD,OAAO,MAAMC,8BAA8B,CACzCC,QAA4B,CAAC,CAAC,GACN,IAAIJ,4BAA4BI,OAAO;AAEjE;;;;;;CAMC,GACD,OAAO,MAAMC,iCAAiC,CAC5CC,UACA,EAAEC,cAAc,UAAU,EAAEC,QAAQN,iBAAiB,EAAE,GAAG,CAAC,CAAC,GAE5DC,4BAA4B;QAC1B,CAACK,MAAM,EAAEC,OAAOC,OAAO,CAACJ,UAAUK,GAAG,CAAC,CAAC,CAACC,QAAQC,IAAI,GAAM,CAAA;gBACxDC,KAAKP,YAAYQ,OAAO,CAAC,YAAYH;gBACrCI,OAAOH;YACT,CAAA;IACF,GAAG;AAEL,4EAA4E,GAC5E,OAAO,MAAMI,yBAAyB,CAACb,QAA4B,CAAC,CAAC,GACnEH,MAAMiB,OAAO,CAACnB,4BAA4BI,4BAA4BC,QAAQ;AAEhF,+EAA+E,GAC/E,OAAO,MAAMe,4BAA4B,CACvCb,UACAc,UAEAnB,MAAMiB,OAAO,CACXnB,4BACAM,+BAA+BC,UAAUc,UACzC"}
@@ -0,0 +1,24 @@
1
+ import type { EntityRepository } from '@entifix/business';
2
+ import { type EntifixError, type Entity } from '@entifix/core';
3
+ export interface InMemoryEntityRepository extends EntityRepository {
4
+ /** Everything currently stored, in insertion order. */
5
+ readonly items: Entity[];
6
+ /** Replaces the contents, discarding whatever was there. */
7
+ seed(items: Entity[]): void;
8
+ /**
9
+ * Makes the next call to any method fail with `error`, so error branches are
10
+ * reachable without reaching for a mocking library.
11
+ */
12
+ failNext(error: EntifixError): void;
13
+ }
14
+ /**
15
+ * In-memory {@link EntityRepository}: the default double for anything that
16
+ * depends on persistence.
17
+ *
18
+ * Its semantics deliberately mirror `makeMongoRepository` — same filtering and
19
+ * sorting rules, same 1-based paging with a default page size of 10, same
20
+ * "entity not found" failure, same id minting on create — and
21
+ * `describeEntityRepositoryContract` holds both to that shared contract.
22
+ */
23
+ export declare const makeInMemoryEntityRepository: (seed?: Entity[]) => InMemoryEntityRepository;
24
+ //# sourceMappingURL=entity-repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-repository.d.ts","sourceRoot":"","sources":["../../src/doubles/entity-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,MAAM,EAQZ,MAAM,eAAe,CAAC;AAwHvB,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,wBAoFF,CAAC"}
@@ -0,0 +1,159 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { EntifixConnError } from '@entifix/core';
3
+ import { Effect } from 'effect';
4
+ /** Escapes a string so `like`/`nlike` match literally, as the Mongo adapter does. */ const escapeRegex = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5
+ const isFilterGroup = (node)=>!('property' in node) && (node.operator === 'and' || node.operator === 'or');
6
+ const compareValues = (left, right)=>{
7
+ if (left === right) return 0;
8
+ // Mongo sorts missing values below present ones.
9
+ if (left == null) return -1;
10
+ if (right == null) return 1;
11
+ if (typeof left === 'number' && typeof right === 'number') {
12
+ return left - right;
13
+ }
14
+ if (left instanceof Date && right instanceof Date) {
15
+ return left.getTime() - right.getTime();
16
+ }
17
+ return String(left).localeCompare(String(right));
18
+ };
19
+ const matchesNode = (item, node)=>{
20
+ if (isFilterGroup(node)) {
21
+ return node.operator === 'or' ? node.values.some((child)=>matchesNode(item, child)) : node.values.every((child)=>matchesNode(item, child));
22
+ }
23
+ const actual = item[String(node.property)];
24
+ switch(node.operator){
25
+ case 'eq':
26
+ return actual === node.value;
27
+ case 'ne':
28
+ return actual !== node.value;
29
+ case 'gt':
30
+ return compareValues(actual, node.value) > 0;
31
+ case 'gte':
32
+ return compareValues(actual, node.value) >= 0;
33
+ case 'lt':
34
+ return compareValues(actual, node.value) < 0;
35
+ case 'lte':
36
+ return compareValues(actual, node.value) <= 0;
37
+ case 'in':
38
+ return node.values.includes(actual);
39
+ case 'nin':
40
+ return !node.values.includes(actual);
41
+ case 'between':
42
+ return compareValues(actual, node.start) >= 0 && compareValues(actual, node.end) <= 0;
43
+ case 'nbetween':
44
+ return !(compareValues(actual, node.start) >= 0 && compareValues(actual, node.end) <= 0);
45
+ case 'like':
46
+ return new RegExp(escapeRegex(node.value), 'i').test(String(actual));
47
+ case 'nlike':
48
+ return !new RegExp(escapeRegex(node.value), 'i').test(String(actual));
49
+ case 'isNull':
50
+ return actual == null;
51
+ case 'isNotNull':
52
+ return actual != null;
53
+ default:
54
+ {
55
+ // Exhaustiveness guard — a new operator must be handled explicitly.
56
+ const never = node;
57
+ return never;
58
+ }
59
+ }
60
+ };
61
+ const applyFiltering = (items, filtering)=>{
62
+ if (!filtering || filtering.length === 0) return items;
63
+ // Every top-level entry is combined with `and`, matching `translateFiltering`.
64
+ const nodes = filtering.flatMap((entry)=>Array.isArray(entry) ? entry : [
65
+ entry
66
+ ]);
67
+ return items.filter((item)=>nodes.every((node)=>matchesNode(item, node)));
68
+ };
69
+ const applySorting = (items, sorting)=>{
70
+ if (!sorting || sorting.length === 0) return items;
71
+ const entries = sorting.flatMap((record)=>Object.keys(record).map(Number).sort((left, right)=>left - right).map((priority)=>record[priority]).filter((entry)=>entry != null));
72
+ return [
73
+ ...items
74
+ ].sort((left, right)=>{
75
+ for (const entry of entries){
76
+ const property = String(entry.property);
77
+ const result = compareValues(left[property], right[property]);
78
+ if (result !== 0) return entry.type === 'desc' ? -result : result;
79
+ }
80
+ return 0;
81
+ });
82
+ };
83
+ /**
84
+ * In-memory {@link EntityRepository}: the default double for anything that
85
+ * depends on persistence.
86
+ *
87
+ * Its semantics deliberately mirror `makeMongoRepository` — same filtering and
88
+ * sorting rules, same 1-based paging with a default page size of 10, same
89
+ * "entity not found" failure, same id minting on create — and
90
+ * `describeEntityRepositoryContract` holds both to that shared contract.
91
+ */ export const makeInMemoryEntityRepository = (seed = [])=>{
92
+ let items = [
93
+ ...seed
94
+ ];
95
+ let pendingFailure;
96
+ /** Consumes a queued failure, if one was armed. */ const guard = (run)=>{
97
+ if (pendingFailure !== undefined) {
98
+ const failure = pendingFailure;
99
+ pendingFailure = undefined;
100
+ return Effect.fail(failure);
101
+ }
102
+ return run();
103
+ };
104
+ const load = (request)=>guard(()=>{
105
+ var _request_page, _request_pageSize;
106
+ const matched = applySorting(applyFiltering(items, request.filtering), request.sorting);
107
+ const page = (_request_page = request.page) != null ? _request_page : 1;
108
+ const pageSize = (_request_pageSize = request.pageSize) != null ? _request_pageSize : 10;
109
+ const start = (page - 1) * pageSize;
110
+ return Effect.succeed({
111
+ items: matched.slice(start, start + pageSize),
112
+ total: matched.length,
113
+ request
114
+ });
115
+ });
116
+ const get = (id)=>guard(()=>{
117
+ const found = items.find((item)=>item.id === id);
118
+ return found === undefined ? Effect.fail(new EntifixConnError('Entity not found', undefined, {
119
+ id
120
+ })) : Effect.succeed(found);
121
+ });
122
+ const save = (entity)=>guard(()=>{
123
+ var _entity_id;
124
+ // A create arrives without an id, so the store mints one — same rule the
125
+ // Mongo adapter follows.
126
+ entity.id = (_entity_id = entity.id) != null ? _entity_id : randomUUID();
127
+ const index = items.findIndex((item)=>item.id === entity.id);
128
+ if (index === -1) {
129
+ items.push(entity);
130
+ } else {
131
+ items[index] = entity;
132
+ }
133
+ return Effect.succeed(entity);
134
+ });
135
+ const remove = (entityOrId)=>guard(()=>{
136
+ const id = entityOrId != null && typeof entityOrId === 'object' ? entityOrId.id : entityOrId;
137
+ items = items.filter((item)=>item.id !== id);
138
+ return Effect.succeed(undefined);
139
+ });
140
+ return {
141
+ load,
142
+ get,
143
+ save,
144
+ delete: remove,
145
+ get items () {
146
+ return items;
147
+ },
148
+ seed: (next)=>{
149
+ items = [
150
+ ...next
151
+ ];
152
+ },
153
+ failNext: (error)=>{
154
+ pendingFailure = error;
155
+ }
156
+ };
157
+ };
158
+
159
+ //# sourceMappingURL=entity-repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/doubles/entity-repository.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { EntityRepository } from '@entifix/business';\nimport {\n EntifixConnError,\n type EntifixError,\n type Entity,\n type EntityFilter,\n type EntityFiltering,\n type EntityId,\n type EntityLoadRequest,\n type EntityPage,\n type EntitySorting,\n type FilterGroup,\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\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 = (item as Record<string, unknown>)[String(node.property)];\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 node.values.includes(actual as TEntity[keyof TEntity]);\n case 'nin':\n return !node.values.includes(actual as TEntity[keyof TEntity]);\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\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.succeed({\n items: matched.slice(start, start + pageSize),\n total: matched.length,\n request,\n } satisfies EntityPage<TEntity>);\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 : Effect.succeed(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.\n entity.id = entity.id ?? randomUUID();\n const index = items.findIndex(item => item.id === entity.id);\n if (index === -1) {\n items.push(entity);\n } else {\n items[index] = entity;\n }\n return Effect.succeed(entity);\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":["randomUUID","EntifixConnError","Effect","escapeRegex","value","replace","isFilterGroup","node","operator","compareValues","left","right","Date","getTime","String","localeCompare","matchesNode","item","values","some","child","every","actual","property","includes","start","end","RegExp","test","never","applyFiltering","items","filtering","length","nodes","flatMap","entry","Array","isArray","filter","applySorting","sorting","entries","record","Object","keys","map","Number","sort","priority","result","type","makeInMemoryEntityRepository","seed","pendingFailure","guard","run","undefined","failure","fail","load","request","matched","page","pageSize","succeed","slice","total","get","id","found","find","save","entity","index","findIndex","push","remove","entityOrId","delete","next","failNext","error"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type Entity, type EntityConstructor, type EntityId, type EntityLinkResolver } from '@entifix/core';
2
+ export interface InMemoryEntityLinkResolver extends EntityLinkResolver {
3
+ /** Every (constructor, id) pair the resolver was asked for, in order. */
4
+ readonly requested: ReadonlyArray<{
5
+ entity: string;
6
+ id: EntityId;
7
+ }>;
8
+ }
9
+ /**
10
+ * In-memory {@link EntityLinkResolver} over a per-constructor table of targets.
11
+ *
12
+ * A use-case that follows links yields `EntityLinkResolverTag`, so this is the
13
+ * double that lets link-following be tested without any adapter at all — and
14
+ * `requested` shows whether an already-embedded link was needlessly refetched.
15
+ */
16
+ export declare const makeInMemoryEntityLinkResolver: (entries: ReadonlyArray<readonly [EntityConstructor<Entity>, Entity[]]>) => InMemoryEntityLinkResolver;
17
+ //# sourceMappingURL=link-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-resolver.d.ts","sourceRoot":"","sources":["../../src/doubles/link-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACxB,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,0BAA2B,SAAQ,kBAAkB;IACpE,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CACrE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,GACzC,SAAS,aAAa,CAAC,SAAS,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,KACrE,0BA6BF,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { EntifixConnError } from '@entifix/core';
2
+ import { Effect } from 'effect';
3
+ /**
4
+ * In-memory {@link EntityLinkResolver} over a per-constructor table of targets.
5
+ *
6
+ * A use-case that follows links yields `EntityLinkResolverTag`, so this is the
7
+ * double that lets link-following be tested without any adapter at all — and
8
+ * `requested` shows whether an already-embedded link was needlessly refetched.
9
+ */ export const makeInMemoryEntityLinkResolver = (entries)=>{
10
+ const tables = new Map(entries.map(([constructor, items])=>[
11
+ constructor,
12
+ items
13
+ ]));
14
+ const requested = [];
15
+ return {
16
+ resolve: (entityConstructor, id)=>Effect.suspend(()=>{
17
+ var _tables_get;
18
+ requested.push({
19
+ entity: entityConstructor.name,
20
+ id
21
+ });
22
+ const found = (_tables_get = tables.get(entityConstructor)) == null ? void 0 : _tables_get.find((item)=>item.id === id);
23
+ return found === undefined ? Effect.fail(new EntifixConnError('Link target not found', undefined, {
24
+ entity: entityConstructor.name,
25
+ id
26
+ })) : Effect.succeed(found);
27
+ }),
28
+ get requested () {
29
+ return requested;
30
+ }
31
+ };
32
+ };
33
+
34
+ //# sourceMappingURL=link-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/doubles/link-resolver.ts"],"sourcesContent":["import {\n EntifixConnError,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLinkResolver,\n} from '@entifix/core';\nimport { Effect } from 'effect';\n\nexport interface InMemoryEntityLinkResolver extends EntityLinkResolver {\n /** Every (constructor, id) pair the resolver was asked for, in order. */\n readonly requested: ReadonlyArray<{ entity: string; id: EntityId }>;\n}\n\n/**\n * In-memory {@link EntityLinkResolver} over a per-constructor table of targets.\n *\n * A use-case that follows links yields `EntityLinkResolverTag`, so this is the\n * double that lets link-following be tested without any adapter at all — and\n * `requested` shows whether an already-embedded link was needlessly refetched.\n */\nexport const makeInMemoryEntityLinkResolver = (\n entries: ReadonlyArray<readonly [EntityConstructor<Entity>, Entity[]]>,\n): InMemoryEntityLinkResolver => {\n const tables = new Map<unknown, Entity[]>(\n entries.map(([constructor, items]) => [constructor, items]),\n );\n const requested: Array<{ entity: string; id: EntityId }> = [];\n\n return {\n resolve: <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n id: EntityId,\n ) =>\n Effect.suspend(() => {\n requested.push({ entity: entityConstructor.name, id });\n const found = tables\n .get(entityConstructor)\n ?.find(item => item.id === id);\n return found === undefined\n ? Effect.fail(\n new EntifixConnError('Link target not found', undefined, {\n entity: entityConstructor.name,\n id,\n }),\n )\n : Effect.succeed(found as TEntity);\n }),\n get requested() {\n return requested;\n },\n };\n};\n"],"names":["EntifixConnError","Effect","makeInMemoryEntityLinkResolver","entries","tables","Map","map","constructor","items","requested","resolve","entityConstructor","id","suspend","push","entity","name","found","get","find","item","undefined","fail","succeed"],"mappings":"AAAA,SACEA,gBAAgB,QAKX,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAOhC;;;;;;CAMC,GACD,OAAO,MAAMC,iCAAiC,CAC5CC;IAEA,MAAMC,SAAS,IAAIC,IACjBF,QAAQG,GAAG,CAAC,CAAC,CAACC,aAAaC,MAAM,GAAK;YAACD;YAAaC;SAAM;IAE5D,MAAMC,YAAqD,EAAE;IAE7D,OAAO;QACLC,SAAS,CACPC,mBACAC,KAEAX,OAAOY,OAAO,CAAC;oBAECT;gBADdK,UAAUK,IAAI,CAAC;oBAAEC,QAAQJ,kBAAkBK,IAAI;oBAAEJ;gBAAG;gBACpD,MAAMK,SAAQb,cAAAA,OACXc,GAAG,CAACP,uCADOP,YAEVe,IAAI,CAACC,CAAAA,OAAQA,KAAKR,EAAE,KAAKA;gBAC7B,OAAOK,UAAUI,YACbpB,OAAOqB,IAAI,CACT,IAAItB,iBAAiB,yBAAyBqB,WAAW;oBACvDN,QAAQJ,kBAAkBK,IAAI;oBAC9BJ;gBACF,MAEFX,OAAOsB,OAAO,CAACN;YACrB;QACF,IAAIR,aAAY;YACd,OAAOA;QACT;IACF;AACF,EAAE"}