@entifix/testing-unit 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/unit-of-work.contract.d.ts +24 -0
- package/dist/contracts/unit-of-work.contract.d.ts.map +1 -0
- package/dist/contracts/unit-of-work.contract.js +178 -0
- package/dist/contracts/unit-of-work.contract.js.map +1 -0
- package/dist/doubles/unit-of-work.d.ts +27 -0
- package/dist/doubles/unit-of-work.d.ts.map +1 -0
- package/dist/doubles/unit-of-work.js +82 -0
- package/dist/doubles/unit-of-work.js.map +1 -0
- package/dist/drivers/fake-mongo.d.ts +15 -0
- package/dist/drivers/fake-mongo.d.ts.map +1 -1
- package/dist/drivers/fake-mongo.js +83 -19
- package/dist/drivers/fake-mongo.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -52,6 +52,11 @@ describeEntityRepositoryContract('mongo adapter over a fake driver', { makeRepos
|
|
|
52
52
|
A read-only adapter runs `describeEntityRepositoryReadContract` alone;
|
|
53
53
|
`describeEntityRepositoryContract` is it and `describeEntityRepositoryWriteContract`.
|
|
54
54
|
|
|
55
|
+
`describeUnitOfWorkContract` holds a unit of work to the same rules whatever
|
|
56
|
+
its store: writes, events and the claim commit or vanish together, a taken
|
|
57
|
+
claim is a `duplicate` that never runs the body, and a business failure is an
|
|
58
|
+
`aborted` value. `makeInMemoryUnitOfWork` is the double that passes it.
|
|
59
|
+
|
|
55
60
|
This is what keeps a fake from quietly becoming a more forgiving version of the
|
|
56
61
|
thing it stands in for. `@entifix/testing-integration` will later run
|
|
57
62
|
the same suites against real infrastructure.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC"}
|
package/dist/contracts/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/contracts/index.ts"],"sourcesContent":["export * from './entity-repository.contract.js';\nexport * from './saga-store.contract.js';\nexport * from './transaction-ports.contract.js';\n"],"names":[],"mappings":"AAAA,cAAc,kCAAkC;AAChD,cAAc,2BAA2B;AACzC,cAAc,kCAAkC"}
|
|
1
|
+
{"version":3,"sources":["../../src/contracts/index.ts"],"sourcesContent":["export * from './entity-repository.contract.js';\nexport * from './saga-store.contract.js';\nexport * from './transaction-ports.contract.js';\nexport * from './unit-of-work.contract.js';\n"],"names":[],"mappings":"AAAA,cAAc,kCAAkC;AAChD,cAAc,2BAA2B;AACzC,cAAc,kCAAkC;AAChD,cAAc,6BAA6B"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EntifixConnError } from '@entifix/core';
|
|
2
|
+
import type { UnitOfWork } from '@entifix/transactions';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
/** What {@link describeUnitOfWorkContract} needs to observe a store. */
|
|
5
|
+
export interface UnitOfWorkContractHarness<H> {
|
|
6
|
+
readonly unitOfWork: UnitOfWork<H>;
|
|
7
|
+
/** Writes one record through the unit's handle. */
|
|
8
|
+
write(handle: H, id: string): Effect.Effect<void, EntifixConnError>;
|
|
9
|
+
/** Whether a record exists, read through the unit's handle. */
|
|
10
|
+
readIn(handle: H, id: string): Effect.Effect<boolean, EntifixConnError>;
|
|
11
|
+
/** Whether a record is durable, read outside any unit. */
|
|
12
|
+
stored(id: string): Promise<boolean>;
|
|
13
|
+
/** The ids of every event the outbox holds. */
|
|
14
|
+
recorded(): Promise<readonly string[]>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What every {@link UnitOfWork} must guarantee: the writes, the events and the
|
|
18
|
+
* claim of one unit commit or vanish together; a taken claim turns the next
|
|
19
|
+
* unit into a `duplicate` without running it; a business failure is an
|
|
20
|
+
* `aborted` value and a store failure is an error; and what a unit wrote is
|
|
21
|
+
* visible to itself before it commits.
|
|
22
|
+
*/
|
|
23
|
+
export declare const describeUnitOfWorkContract: <H>(name: string, makeHarness: () => UnitOfWorkContractHarness<H> | Promise<UnitOfWorkContractHarness<H>>) => void;
|
|
24
|
+
//# sourceMappingURL=unit-of-work.contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit-of-work.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/unit-of-work.contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAGxC,wEAAwE;AACxE,MAAM,WAAW,yBAAyB,CAAC,CAAC;IAC1C,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACnC,mDAAmD;IACnD,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACpE,+DAA+D;IAC/D,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;IACxE,0DAA0D;IAC1D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,+CAA+C;IAC/C,QAAQ,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;CACxC;AAaD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GAAI,CAAC,EAC1C,MAAM,MAAM,EACZ,aAAa,MACX,yBAAyB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,KACrE,IA6MF,CAAC"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { EntifixConnError } from '@entifix/core';
|
|
2
|
+
import { Effect, Option } from 'effect';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
const anEvent = (id)=>({
|
|
5
|
+
name: 'contract.widget.changed',
|
|
6
|
+
id,
|
|
7
|
+
source: 'contract-slice',
|
|
8
|
+
at: '2026-01-01T00:00:00.000Z',
|
|
9
|
+
correlationId: id,
|
|
10
|
+
data: {}
|
|
11
|
+
});
|
|
12
|
+
const run = (effect)=>Effect.runPromise(effect);
|
|
13
|
+
/**
|
|
14
|
+
* What every {@link UnitOfWork} must guarantee: the writes, the events and the
|
|
15
|
+
* claim of one unit commit or vanish together; a taken claim turns the next
|
|
16
|
+
* unit into a `duplicate` without running it; a business failure is an
|
|
17
|
+
* `aborted` value and a store failure is an error; and what a unit wrote is
|
|
18
|
+
* visible to itself before it commits.
|
|
19
|
+
*/ export const describeUnitOfWorkContract = (name, makeHarness)=>{
|
|
20
|
+
describe(`UnitOfWork contract: ${name}`, ()=>{
|
|
21
|
+
it('commits the writes and the events together', async ()=>{
|
|
22
|
+
const harness = await makeHarness();
|
|
23
|
+
const outcome = await run(harness.unitOfWork.run((unit)=>harness.write(unit.handle, 'w-1').pipe(Effect.tap(()=>Effect.sync(()=>unit.emit(anEvent('e-1')))), Effect.as('done'))));
|
|
24
|
+
expect(outcome).toEqual({
|
|
25
|
+
_tag: 'committed',
|
|
26
|
+
value: 'done'
|
|
27
|
+
});
|
|
28
|
+
expect(await harness.stored('w-1')).toBe(true);
|
|
29
|
+
expect(await harness.recorded()).toContain('e-1');
|
|
30
|
+
});
|
|
31
|
+
it('answers a business failure as aborted, and writes nothing', async ()=>{
|
|
32
|
+
const harness = await makeHarness();
|
|
33
|
+
const outcome = await run(harness.unitOfWork.run((unit)=>harness.write(unit.handle, 'w-2').pipe(Effect.tap(()=>Effect.sync(()=>unit.emit(anEvent('e-2')))), Effect.andThen(Effect.fail('outOfStock')))));
|
|
34
|
+
expect(outcome).toEqual({
|
|
35
|
+
_tag: 'aborted',
|
|
36
|
+
reason: 'outOfStock'
|
|
37
|
+
});
|
|
38
|
+
expect(await harness.stored('w-2')).toBe(false);
|
|
39
|
+
expect(await harness.recorded()).not.toContain('e-2');
|
|
40
|
+
});
|
|
41
|
+
it('surfaces a store failure as an error, and writes nothing', async ()=>{
|
|
42
|
+
const harness = await makeHarness();
|
|
43
|
+
const error = await run(harness.unitOfWork.run((unit)=>harness.write(unit.handle, 'w-3').pipe(Effect.andThen(Effect.fail(new EntifixConnError('store went away'))))).pipe(Effect.flip));
|
|
44
|
+
expect(error).toBeInstanceOf(EntifixConnError);
|
|
45
|
+
expect(await harness.stored('w-3')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('lets a unit read its own uncommitted writes', async ()=>{
|
|
48
|
+
const harness = await makeHarness();
|
|
49
|
+
const outcome = await run(harness.unitOfWork.run((unit)=>harness.write(unit.handle, 'w-4').pipe(Effect.andThen(harness.readIn(unit.handle, 'w-4')))));
|
|
50
|
+
expect(outcome).toEqual({
|
|
51
|
+
_tag: 'committed',
|
|
52
|
+
value: true
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('turns a second unit on a taken event claim into a duplicate that does not run', async ()=>{
|
|
56
|
+
const harness = await makeHarness();
|
|
57
|
+
const claim = {
|
|
58
|
+
kind: 'event',
|
|
59
|
+
consumer: 'contract.fold',
|
|
60
|
+
eventId: 'm-1'
|
|
61
|
+
};
|
|
62
|
+
let runs = 0;
|
|
63
|
+
const body = (unit)=>Effect.suspend(()=>{
|
|
64
|
+
runs += 1;
|
|
65
|
+
return harness.write(unit.handle, `fold-${runs}`);
|
|
66
|
+
});
|
|
67
|
+
const first = await run(harness.unitOfWork.run(body, {
|
|
68
|
+
claim
|
|
69
|
+
}));
|
|
70
|
+
const second = await run(harness.unitOfWork.run(body, {
|
|
71
|
+
claim
|
|
72
|
+
}));
|
|
73
|
+
expect(first._tag).toBe('committed');
|
|
74
|
+
expect(second).toEqual({
|
|
75
|
+
_tag: 'duplicate',
|
|
76
|
+
replay: Option.none()
|
|
77
|
+
});
|
|
78
|
+
expect(await harness.stored('fold-2')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
it('keys an event claim on the consumer as well as the event', async ()=>{
|
|
81
|
+
const harness = await makeHarness();
|
|
82
|
+
const noop = ()=>Effect.void;
|
|
83
|
+
const one = await run(harness.unitOfWork.run(noop, {
|
|
84
|
+
claim: {
|
|
85
|
+
kind: 'event',
|
|
86
|
+
consumer: 'contract.a',
|
|
87
|
+
eventId: 'm-2'
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
const other = await run(harness.unitOfWork.run(noop, {
|
|
91
|
+
claim: {
|
|
92
|
+
kind: 'event',
|
|
93
|
+
consumer: 'contract.b',
|
|
94
|
+
eventId: 'm-2'
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
expect(one._tag).toBe('committed');
|
|
98
|
+
expect(other._tag).toBe('committed');
|
|
99
|
+
});
|
|
100
|
+
it('answers a duplicate command with what the first run remembered', async ()=>{
|
|
101
|
+
const harness = await makeHarness();
|
|
102
|
+
const claim = {
|
|
103
|
+
kind: 'command',
|
|
104
|
+
commandId: 'c-1'
|
|
105
|
+
};
|
|
106
|
+
const options = {
|
|
107
|
+
claim,
|
|
108
|
+
replay: (value)=>value
|
|
109
|
+
};
|
|
110
|
+
let runs = 0;
|
|
111
|
+
const capture = (unit)=>Effect.suspend(()=>{
|
|
112
|
+
runs += 1;
|
|
113
|
+
const paymentId = `pay-${runs}`;
|
|
114
|
+
return harness.write(unit.handle, paymentId).pipe(Effect.as({
|
|
115
|
+
paymentId
|
|
116
|
+
}));
|
|
117
|
+
});
|
|
118
|
+
await run(harness.unitOfWork.run(capture, options));
|
|
119
|
+
const again = await run(harness.unitOfWork.run(capture, options));
|
|
120
|
+
expect(again).toEqual({
|
|
121
|
+
_tag: 'duplicate',
|
|
122
|
+
replay: Option.some({
|
|
123
|
+
paymentId: 'pay-1'
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
// A redelivered capture must not charge again, so the body never reruns.
|
|
127
|
+
expect(runs).toBe(1);
|
|
128
|
+
expect(await run(harness.unitOfWork.replay('c-1'))).toEqual(Option.some({
|
|
129
|
+
paymentId: 'pay-1'
|
|
130
|
+
}));
|
|
131
|
+
expect(await run(harness.unitOfWork.replay('c-unknown'))).toEqual(Option.none());
|
|
132
|
+
});
|
|
133
|
+
it('leaves a claim untaken when its unit aborts', async ()=>{
|
|
134
|
+
const harness = await makeHarness();
|
|
135
|
+
const claim = {
|
|
136
|
+
kind: 'command',
|
|
137
|
+
commandId: 'c-2'
|
|
138
|
+
};
|
|
139
|
+
const refused = await run(harness.unitOfWork.run(()=>Effect.fail('declined'), {
|
|
140
|
+
claim
|
|
141
|
+
}));
|
|
142
|
+
const retried = await run(harness.unitOfWork.run(()=>Effect.succeed('ok'), {
|
|
143
|
+
claim
|
|
144
|
+
}));
|
|
145
|
+
expect(refused._tag).toBe('aborted');
|
|
146
|
+
expect(retried).toEqual({
|
|
147
|
+
_tag: 'committed',
|
|
148
|
+
value: 'ok'
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it('treats an event id already in the outbox as a duplicate', async ()=>{
|
|
152
|
+
const harness = await makeHarness();
|
|
153
|
+
const announce = (id)=>harness.unitOfWork.run((unit)=>harness.write(unit.handle, id).pipe(Effect.tap(()=>Effect.sync(()=>unit.emit(anEvent('e-same'))))));
|
|
154
|
+
await run(announce('w-5'));
|
|
155
|
+
const again = await run(announce('w-6'));
|
|
156
|
+
expect(again._tag).toBe('duplicate');
|
|
157
|
+
expect(await harness.stored('w-6')).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
it('runs afterCommit only on commit, and never lets it change the outcome', async ()=>{
|
|
160
|
+
const harness = await makeHarness();
|
|
161
|
+
const ran = [];
|
|
162
|
+
const after = (label)=>({
|
|
163
|
+
afterCommit: Effect.sync(()=>ran.push(label)).pipe(Effect.andThen(Effect.fail('drain failed')))
|
|
164
|
+
});
|
|
165
|
+
const committed = await run(harness.unitOfWork.run(()=>Effect.succeed(1), after('commit')));
|
|
166
|
+
await run(harness.unitOfWork.run(()=>Effect.fail('no'), after('abort')));
|
|
167
|
+
expect(committed).toEqual({
|
|
168
|
+
_tag: 'committed',
|
|
169
|
+
value: 1
|
|
170
|
+
});
|
|
171
|
+
expect(ran).toEqual([
|
|
172
|
+
'commit'
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
//# sourceMappingURL=unit-of-work.contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/contracts/unit-of-work.contract.ts"],"sourcesContent":["import { type DomainEvent, EntifixConnError } from '@entifix/core';\nimport type { UnitOfWork } from '@entifix/transactions';\nimport { Effect, Option } from 'effect';\nimport { describe, expect, it } from 'vitest';\n\n/** What {@link describeUnitOfWorkContract} needs to observe a store. */\nexport interface UnitOfWorkContractHarness<H> {\n readonly unitOfWork: UnitOfWork<H>;\n /** Writes one record through the unit's handle. */\n write(handle: H, id: string): Effect.Effect<void, EntifixConnError>;\n /** Whether a record exists, read through the unit's handle. */\n readIn(handle: H, id: string): Effect.Effect<boolean, EntifixConnError>;\n /** Whether a record is durable, read outside any unit. */\n stored(id: string): Promise<boolean>;\n /** The ids of every event the outbox holds. */\n recorded(): Promise<readonly string[]>;\n}\n\nconst anEvent = (id: string): DomainEvent => ({\n name: 'contract.widget.changed',\n id,\n source: 'contract-slice',\n at: '2026-01-01T00:00:00.000Z',\n correlationId: id,\n data: {},\n});\n\nconst run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromise(effect);\n\n/**\n * What every {@link UnitOfWork} must guarantee: the writes, the events and the\n * claim of one unit commit or vanish together; a taken claim turns the next\n * unit into a `duplicate` without running it; a business failure is an\n * `aborted` value and a store failure is an error; and what a unit wrote is\n * visible to itself before it commits.\n */\nexport const describeUnitOfWorkContract = <H>(\n name: string,\n makeHarness: () =>\n UnitOfWorkContractHarness<H> | Promise<UnitOfWorkContractHarness<H>>,\n): void => {\n describe(`UnitOfWork contract: ${name}`, () => {\n it('commits the writes and the events together', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness.write(unit.handle, 'w-1').pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-1')))),\n Effect.as('done'),\n ),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'committed', value: 'done' });\n expect(await harness.stored('w-1')).toBe(true);\n expect(await harness.recorded()).toContain('e-1');\n });\n\n it('answers a business failure as aborted, and writes nothing', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness.write(unit.handle, 'w-2').pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-2')))),\n Effect.andThen(Effect.fail('outOfStock' as const)),\n ),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'aborted', reason: 'outOfStock' });\n expect(await harness.stored('w-2')).toBe(false);\n expect(await harness.recorded()).not.toContain('e-2');\n });\n\n it('surfaces a store failure as an error, and writes nothing', async () => {\n const harness = await makeHarness();\n\n const error = await run(\n harness.unitOfWork\n .run(unit =>\n harness\n .write(unit.handle, 'w-3')\n .pipe(\n Effect.andThen(\n Effect.fail(new EntifixConnError('store went away')),\n ),\n ),\n )\n .pipe(Effect.flip),\n );\n\n expect(error).toBeInstanceOf(EntifixConnError);\n expect(await harness.stored('w-3')).toBe(false);\n });\n\n it('lets a unit read its own uncommitted writes', async () => {\n const harness = await makeHarness();\n\n const outcome = await run(\n harness.unitOfWork.run(unit =>\n harness\n .write(unit.handle, 'w-4')\n .pipe(Effect.andThen(harness.readIn(unit.handle, 'w-4'))),\n ),\n );\n\n expect(outcome).toEqual({ _tag: 'committed', value: true });\n });\n\n it('turns a second unit on a taken event claim into a duplicate that does not run', async () => {\n const harness = await makeHarness();\n const claim = {\n kind: 'event',\n consumer: 'contract.fold',\n eventId: 'm-1',\n } as const;\n let runs = 0;\n const body = (unit: { handle: H }) =>\n Effect.suspend(() => {\n runs += 1;\n return harness.write(unit.handle, `fold-${runs}`);\n });\n\n const first = await run(harness.unitOfWork.run(body, { claim }));\n const second = await run(harness.unitOfWork.run(body, { claim }));\n\n expect(first._tag).toBe('committed');\n expect(second).toEqual({ _tag: 'duplicate', replay: Option.none() });\n expect(await harness.stored('fold-2')).toBe(false);\n });\n\n it('keys an event claim on the consumer as well as the event', async () => {\n const harness = await makeHarness();\n const noop = () => Effect.void;\n\n const one = await run(\n harness.unitOfWork.run(noop, {\n claim: { kind: 'event', consumer: 'contract.a', eventId: 'm-2' },\n }),\n );\n const other = await run(\n harness.unitOfWork.run(noop, {\n claim: { kind: 'event', consumer: 'contract.b', eventId: 'm-2' },\n }),\n );\n\n expect(one._tag).toBe('committed');\n expect(other._tag).toBe('committed');\n });\n\n it('answers a duplicate command with what the first run remembered', async () => {\n const harness = await makeHarness();\n const claim = { kind: 'command', commandId: 'c-1' } as const;\n const options = {\n claim,\n replay: (value: { paymentId: string }) => value,\n };\n\n let runs = 0;\n const capture = (unit: { handle: H }) =>\n Effect.suspend(() => {\n runs += 1;\n const paymentId = `pay-${runs}`;\n return harness\n .write(unit.handle, paymentId)\n .pipe(Effect.as({ paymentId }));\n });\n\n await run(harness.unitOfWork.run(capture, options));\n const again = await run(harness.unitOfWork.run(capture, options));\n\n expect(again).toEqual({\n _tag: 'duplicate',\n replay: Option.some({ paymentId: 'pay-1' }),\n });\n // A redelivered capture must not charge again, so the body never reruns.\n expect(runs).toBe(1);\n expect(await run(harness.unitOfWork.replay('c-1'))).toEqual(\n Option.some({ paymentId: 'pay-1' }),\n );\n expect(await run(harness.unitOfWork.replay('c-unknown'))).toEqual(\n Option.none(),\n );\n });\n\n it('leaves a claim untaken when its unit aborts', async () => {\n const harness = await makeHarness();\n const claim = { kind: 'command', commandId: 'c-2' } as const;\n\n const refused = await run(\n harness.unitOfWork.run(() => Effect.fail('declined' as const), {\n claim,\n }),\n );\n const retried = await run(\n harness.unitOfWork.run(() => Effect.succeed('ok'), { claim }),\n );\n\n expect(refused._tag).toBe('aborted');\n expect(retried).toEqual({ _tag: 'committed', value: 'ok' });\n });\n\n it('treats an event id already in the outbox as a duplicate', async () => {\n const harness = await makeHarness();\n const announce = (id: string) =>\n harness.unitOfWork.run(unit =>\n harness\n .write(unit.handle, id)\n .pipe(\n Effect.tap(() => Effect.sync(() => unit.emit(anEvent('e-same')))),\n ),\n );\n\n await run(announce('w-5'));\n const again = await run(announce('w-6'));\n\n expect(again._tag).toBe('duplicate');\n expect(await harness.stored('w-6')).toBe(false);\n });\n\n it('runs afterCommit only on commit, and never lets it change the outcome', async () => {\n const harness = await makeHarness();\n const ran: string[] = [];\n const after = (label: string) => ({\n afterCommit: Effect.sync(() => ran.push(label)).pipe(\n Effect.andThen(Effect.fail('drain failed')),\n ),\n });\n\n const committed = await run(\n harness.unitOfWork.run(() => Effect.succeed(1), after('commit')),\n );\n await run(\n harness.unitOfWork.run(\n () => Effect.fail('no' as const),\n after('abort'),\n ),\n );\n\n expect(committed).toEqual({ _tag: 'committed', value: 1 });\n expect(ran).toEqual(['commit']);\n });\n });\n};\n"],"names":["EntifixConnError","Effect","Option","describe","expect","it","anEvent","id","name","source","at","correlationId","data","run","effect","runPromise","describeUnitOfWorkContract","makeHarness","harness","outcome","unitOfWork","unit","write","handle","pipe","tap","sync","emit","as","toEqual","_tag","value","stored","toBe","recorded","toContain","andThen","fail","reason","not","error","flip","toBeInstanceOf","readIn","claim","kind","consumer","eventId","runs","body","suspend","first","second","replay","none","noop","void","one","other","commandId","options","capture","paymentId","again","some","refused","retried","succeed","announce","ran","after","label","afterCommit","push","committed"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type DomainEvent } from '@entifix/core';
|
|
2
|
+
import type { UnitOfWork } from '@entifix/transactions';
|
|
3
|
+
/**
|
|
4
|
+
* The handle an in-memory unit hands its body: a working copy of the records,
|
|
5
|
+
* which becomes the store only if the unit commits.
|
|
6
|
+
*/
|
|
7
|
+
export interface InMemoryUnitHandle {
|
|
8
|
+
readonly records: Map<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface InMemoryUnitOfWork extends UnitOfWork<InMemoryUnitHandle> {
|
|
11
|
+
/** The committed records. */
|
|
12
|
+
readonly records: ReadonlyMap<string, unknown>;
|
|
13
|
+
/** Every event a committed unit emitted, in commit order. */
|
|
14
|
+
readonly outbox: readonly DomainEvent[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A {@link UnitOfWork} over plain maps, for testing code that runs units
|
|
18
|
+
* without a database.
|
|
19
|
+
*
|
|
20
|
+
* Atomic the way a real one is: the body writes to a copy, and a body that
|
|
21
|
+
* fails — with a business reason or with a store error — leaves the committed
|
|
22
|
+
* records, the outbox and the claims exactly as they were. Like the Mongo
|
|
23
|
+
* driver fake it is atomicity and not isolation; nothing here runs
|
|
24
|
+
* concurrently, so a race is a `live` test's to prove.
|
|
25
|
+
*/
|
|
26
|
+
export declare const makeInMemoryUnitOfWork: (seed?: Iterable<readonly [string, unknown]>) => InMemoryUnitOfWork;
|
|
27
|
+
//# sourceMappingURL=unit-of-work.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit-of-work.d.ts","sourceRoot":"","sources":["../../src/doubles/unit-of-work.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAoB,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAa,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGnE;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,kBAAmB,SAAQ,UAAU,CAAC,kBAAkB,CAAC;IACxE,6BAA6B;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;CACzC;AAOD;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAM,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAM,KAC9C,kBA0EF,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { EntifixConnError } from '@entifix/core';
|
|
2
|
+
import { Effect, Option, Result } from 'effect';
|
|
3
|
+
const claimKey = (claim)=>claim.kind === 'event' ? `event:${claim.consumer}:${claim.eventId}` : `command:${claim.commandId}`;
|
|
4
|
+
/**
|
|
5
|
+
* A {@link UnitOfWork} over plain maps, for testing code that runs units
|
|
6
|
+
* without a database.
|
|
7
|
+
*
|
|
8
|
+
* Atomic the way a real one is: the body writes to a copy, and a body that
|
|
9
|
+
* fails — with a business reason or with a store error — leaves the committed
|
|
10
|
+
* records, the outbox and the claims exactly as they were. Like the Mongo
|
|
11
|
+
* driver fake it is atomicity and not isolation; nothing here runs
|
|
12
|
+
* concurrently, so a race is a `live` test's to prove.
|
|
13
|
+
*/ export const makeInMemoryUnitOfWork = (seed = [])=>{
|
|
14
|
+
let records = new Map(seed);
|
|
15
|
+
const outbox = [];
|
|
16
|
+
const eventIds = new Set();
|
|
17
|
+
// A taken claim, and for a command what it is to be answered with.
|
|
18
|
+
const claims = new Map();
|
|
19
|
+
const unitOfWork = {
|
|
20
|
+
get records () {
|
|
21
|
+
return records;
|
|
22
|
+
},
|
|
23
|
+
get outbox () {
|
|
24
|
+
return outbox;
|
|
25
|
+
},
|
|
26
|
+
run: (body, options)=>Effect.gen(function*() {
|
|
27
|
+
const key = (options == null ? void 0 : options.claim) ? claimKey(options.claim) : undefined;
|
|
28
|
+
const taken = key === undefined ? undefined : claims.get(key);
|
|
29
|
+
if (taken !== undefined) {
|
|
30
|
+
return {
|
|
31
|
+
_tag: 'duplicate',
|
|
32
|
+
replay: taken
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const handle = {
|
|
36
|
+
records: new Map(records)
|
|
37
|
+
};
|
|
38
|
+
const emitted = [];
|
|
39
|
+
const outcome = yield* Effect.result(body({
|
|
40
|
+
handle,
|
|
41
|
+
emit: (event)=>void emitted.push(event)
|
|
42
|
+
}));
|
|
43
|
+
if (Result.isFailure(outcome)) {
|
|
44
|
+
if (outcome.failure instanceof EntifixConnError) {
|
|
45
|
+
return yield* Effect.fail(outcome.failure);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
_tag: 'aborted',
|
|
49
|
+
reason: outcome.failure
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// An event id already recorded is the outbox's own idempotency claim:
|
|
53
|
+
// the work it announces has committed before.
|
|
54
|
+
if (emitted.some((event)=>eventIds.has(event.id))) {
|
|
55
|
+
return {
|
|
56
|
+
_tag: 'duplicate',
|
|
57
|
+
replay: Option.none()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
records = handle.records;
|
|
61
|
+
for (const event of emitted){
|
|
62
|
+
eventIds.add(event.id);
|
|
63
|
+
outbox.push(event);
|
|
64
|
+
}
|
|
65
|
+
if (key !== undefined) {
|
|
66
|
+
var _options_claim;
|
|
67
|
+
claims.set(key, (options == null ? void 0 : options.replay) && ((_options_claim = options.claim) == null ? void 0 : _options_claim.kind) === 'command' ? Option.some(options.replay(outcome.success)) : Option.none());
|
|
68
|
+
}
|
|
69
|
+
if (options == null ? void 0 : options.afterCommit) {
|
|
70
|
+
yield* options.afterCommit.pipe(Effect.ignore);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
_tag: 'committed',
|
|
74
|
+
value: outcome.success
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
replay: (commandId)=>Effect.sync(()=>Option.flatten(Option.fromUndefinedOr(claims.get(`command:${commandId}`))))
|
|
78
|
+
};
|
|
79
|
+
return unitOfWork;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=unit-of-work.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/doubles/unit-of-work.ts"],"sourcesContent":["import { type DomainEvent, EntifixConnError } from '@entifix/core';\nimport type { UnitClaim, UnitOfWork } from '@entifix/transactions';\nimport { Effect, Option, Result } from 'effect';\n\n/**\n * The handle an in-memory unit hands its body: a working copy of the records,\n * which becomes the store only if the unit commits.\n */\nexport interface InMemoryUnitHandle {\n readonly records: Map<string, unknown>;\n}\n\nexport interface InMemoryUnitOfWork extends UnitOfWork<InMemoryUnitHandle> {\n /** The committed records. */\n readonly records: ReadonlyMap<string, unknown>;\n /** Every event a committed unit emitted, in commit order. */\n readonly outbox: readonly DomainEvent[];\n}\n\nconst claimKey = (claim: UnitClaim): string =>\n claim.kind === 'event'\n ? `event:${claim.consumer}:${claim.eventId}`\n : `command:${claim.commandId}`;\n\n/**\n * A {@link UnitOfWork} over plain maps, for testing code that runs units\n * without a database.\n *\n * Atomic the way a real one is: the body writes to a copy, and a body that\n * fails — with a business reason or with a store error — leaves the committed\n * records, the outbox and the claims exactly as they were. Like the Mongo\n * driver fake it is atomicity and not isolation; nothing here runs\n * concurrently, so a race is a `live` test's to prove.\n */\nexport const makeInMemoryUnitOfWork = (\n seed: Iterable<readonly [string, unknown]> = [],\n): InMemoryUnitOfWork => {\n let records = new Map<string, unknown>(seed);\n const outbox: DomainEvent[] = [];\n const eventIds = new Set<string>();\n // A taken claim, and for a command what it is to be answered with.\n const claims = new Map<string, Option.Option<unknown>>();\n\n const unitOfWork: InMemoryUnitOfWork = {\n get records() {\n return records;\n },\n get outbox() {\n return outbox;\n },\n\n run: (body, options) =>\n Effect.gen(function* () {\n const key = options?.claim ? claimKey(options.claim) : undefined;\n const taken = key === undefined ? undefined : claims.get(key);\n if (taken !== undefined) {\n return { _tag: 'duplicate' as const, replay: taken };\n }\n\n const handle: InMemoryUnitHandle = { records: new Map(records) };\n const emitted: DomainEvent[] = [];\n const outcome = yield* Effect.result(\n body({ handle, emit: event => void emitted.push(event) }),\n );\n\n if (Result.isFailure(outcome)) {\n if (outcome.failure instanceof EntifixConnError) {\n return yield* Effect.fail(outcome.failure);\n }\n return {\n _tag: 'aborted' as const,\n reason: outcome.failure as never,\n };\n }\n\n // An event id already recorded is the outbox's own idempotency claim:\n // the work it announces has committed before.\n if (emitted.some(event => eventIds.has(event.id))) {\n return { _tag: 'duplicate' as const, replay: Option.none() };\n }\n\n records = handle.records;\n for (const event of emitted) {\n eventIds.add(event.id);\n outbox.push(event);\n }\n if (key !== undefined) {\n claims.set(\n key,\n options?.replay && options.claim?.kind === 'command'\n ? Option.some(options.replay(outcome.success))\n : Option.none(),\n );\n }\n\n if (options?.afterCommit) {\n yield* options.afterCommit.pipe(Effect.ignore);\n }\n return { _tag: 'committed' as const, value: outcome.success };\n }),\n\n replay: commandId =>\n Effect.sync(() =>\n Option.flatten(\n Option.fromUndefinedOr(claims.get(`command:${commandId}`)),\n ),\n ),\n };\n\n return unitOfWork;\n};\n"],"names":["EntifixConnError","Effect","Option","Result","claimKey","claim","kind","consumer","eventId","commandId","makeInMemoryUnitOfWork","seed","records","Map","outbox","eventIds","Set","claims","unitOfWork","run","body","options","gen","key","undefined","taken","get","_tag","replay","handle","emitted","outcome","result","emit","event","push","isFailure","failure","fail","reason","some","has","id","none","add","set","success","afterCommit","pipe","ignore","value","sync","flatten","fromUndefinedOr"],"mappings":"AAAA,SAA2BA,gBAAgB,QAAQ,gBAAgB;AAEnE,SAASC,MAAM,EAAEC,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAiBhD,MAAMC,WAAW,CAACC,QAChBA,MAAMC,IAAI,KAAK,UACX,CAAC,MAAM,EAAED,MAAME,QAAQ,CAAC,CAAC,EAAEF,MAAMG,OAAO,EAAE,GAC1C,CAAC,QAAQ,EAAEH,MAAMI,SAAS,EAAE;AAElC;;;;;;;;;CASC,GACD,OAAO,MAAMC,yBAAyB,CACpCC,OAA6C,EAAE;IAE/C,IAAIC,UAAU,IAAIC,IAAqBF;IACvC,MAAMG,SAAwB,EAAE;IAChC,MAAMC,WAAW,IAAIC;IACrB,mEAAmE;IACnE,MAAMC,SAAS,IAAIJ;IAEnB,MAAMK,aAAiC;QACrC,IAAIN;YACF,OAAOA;QACT;QACA,IAAIE;YACF,OAAOA;QACT;QAEAK,KAAK,CAACC,MAAMC,UACVpB,OAAOqB,GAAG,CAAC;gBACT,MAAMC,MAAMF,CAAAA,2BAAAA,QAAShB,KAAK,IAAGD,SAASiB,QAAQhB,KAAK,IAAImB;gBACvD,MAAMC,QAAQF,QAAQC,YAAYA,YAAYP,OAAOS,GAAG,CAACH;gBACzD,IAAIE,UAAUD,WAAW;oBACvB,OAAO;wBAAEG,MAAM;wBAAsBC,QAAQH;oBAAM;gBACrD;gBAEA,MAAMI,SAA6B;oBAAEjB,SAAS,IAAIC,IAAID;gBAAS;gBAC/D,MAAMkB,UAAyB,EAAE;gBACjC,MAAMC,UAAU,OAAO9B,OAAO+B,MAAM,CAClCZ,KAAK;oBAAES;oBAAQI,MAAMC,CAAAA,QAAS,KAAKJ,QAAQK,IAAI,CAACD;gBAAO;gBAGzD,IAAI/B,OAAOiC,SAAS,CAACL,UAAU;oBAC7B,IAAIA,QAAQM,OAAO,YAAYrC,kBAAkB;wBAC/C,OAAO,OAAOC,OAAOqC,IAAI,CAACP,QAAQM,OAAO;oBAC3C;oBACA,OAAO;wBACLV,MAAM;wBACNY,QAAQR,QAAQM,OAAO;oBACzB;gBACF;gBAEA,sEAAsE;gBACtE,8CAA8C;gBAC9C,IAAIP,QAAQU,IAAI,CAACN,CAAAA,QAASnB,SAAS0B,GAAG,CAACP,MAAMQ,EAAE,IAAI;oBACjD,OAAO;wBAAEf,MAAM;wBAAsBC,QAAQ1B,OAAOyC,IAAI;oBAAG;gBAC7D;gBAEA/B,UAAUiB,OAAOjB,OAAO;gBACxB,KAAK,MAAMsB,SAASJ,QAAS;oBAC3Bf,SAAS6B,GAAG,CAACV,MAAMQ,EAAE;oBACrB5B,OAAOqB,IAAI,CAACD;gBACd;gBACA,IAAIX,QAAQC,WAAW;wBAGAH;oBAFrBJ,OAAO4B,GAAG,CACRtB,KACAF,CAAAA,2BAAAA,QAASO,MAAM,KAAIP,EAAAA,iBAAAA,QAAQhB,KAAK,qBAAbgB,eAAef,IAAI,MAAK,YACvCJ,OAAOsC,IAAI,CAACnB,QAAQO,MAAM,CAACG,QAAQe,OAAO,KAC1C5C,OAAOyC,IAAI;gBAEnB;gBAEA,IAAItB,2BAAAA,QAAS0B,WAAW,EAAE;oBACxB,OAAO1B,QAAQ0B,WAAW,CAACC,IAAI,CAAC/C,OAAOgD,MAAM;gBAC/C;gBACA,OAAO;oBAAEtB,MAAM;oBAAsBuB,OAAOnB,QAAQe,OAAO;gBAAC;YAC9D;QAEFlB,QAAQnB,CAAAA,YACNR,OAAOkD,IAAI,CAAC,IACVjD,OAAOkD,OAAO,CACZlD,OAAOmD,eAAe,CAACpC,OAAOS,GAAG,CAAC,CAAC,QAAQ,EAAEjB,WAAW;IAGhE;IAEA,OAAOS;AACT,EAAE"}
|
|
@@ -29,6 +29,11 @@ export interface FakeMongoDb {
|
|
|
29
29
|
* that succeeds and a count that then fails reaches a distinct error branch.
|
|
30
30
|
*/
|
|
31
31
|
failOn(operation: string, error: unknown): void;
|
|
32
|
+
/**
|
|
33
|
+
* Makes only the **next** `operation` reject, then work again — the shape of
|
|
34
|
+
* a transient conflict, which a caller is expected to retry through.
|
|
35
|
+
*/
|
|
36
|
+
failOnce(operation: string, error: unknown): void;
|
|
32
37
|
/** Every operation performed, for assertions about what the adapter did. */
|
|
33
38
|
readonly operations: ReadonlyArray<{
|
|
34
39
|
collection: string;
|
|
@@ -41,6 +46,16 @@ export interface FakeMongoDb {
|
|
|
41
46
|
*/
|
|
42
47
|
startSession(): {
|
|
43
48
|
withTransaction(body: () => Promise<unknown>): Promise<unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* The explicit form of the same transaction: `startTransaction` takes the
|
|
51
|
+
* snapshot, `abortTransaction` restores it and `commitTransaction` drops
|
|
52
|
+
* it. `commitTransaction` honours `failOn`/`failOnce`, and a commit that
|
|
53
|
+
* fails leaves the transaction open, as an unknown commit result does.
|
|
54
|
+
*/
|
|
55
|
+
startTransaction(): void;
|
|
56
|
+
commitTransaction(): Promise<void>;
|
|
57
|
+
abortTransaction(): Promise<void>;
|
|
58
|
+
inTransaction(): boolean;
|
|
44
59
|
endSession(): Promise<void>;
|
|
45
60
|
readonly hasEnded: boolean;
|
|
46
61
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-mongo.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-mongo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuMxC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACtD,6CAA6C;IAC7C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE;;;;OAIG;IACH,YAAY,IAAI;QACd,eAAe,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAM,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAM,KACpC,
|
|
1
|
+
{"version":3,"file":"fake-mongo.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-mongo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuMxC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACtD,6CAA6C;IAC7C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAClD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE;;;;OAIG;IACH,YAAY,IAAI;QACd,eAAe,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChE;;;;;WAKG;QACH,gBAAgB,IAAI,IAAI,CAAC;QACzB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QACnC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAClC,aAAa,IAAI,OAAO,CAAC;QACzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAM,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAM,KACpC,WA6lBF,CAAC"}
|
|
@@ -195,6 +195,16 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
195
195
|
const operations = [];
|
|
196
196
|
let failure;
|
|
197
197
|
const failuresByOperation = new Map();
|
|
198
|
+
const onceFailures = new Map();
|
|
199
|
+
/** The failure `op` should reject with now, consuming a one-shot one. */ const failureFor = (op)=>{
|
|
200
|
+
var _failuresByOperation_get;
|
|
201
|
+
if (onceFailures.has(op)) {
|
|
202
|
+
const once = onceFailures.get(op);
|
|
203
|
+
onceFailures.delete(op);
|
|
204
|
+
return once;
|
|
205
|
+
}
|
|
206
|
+
return (_failuresByOperation_get = failuresByOperation.get(op)) != null ? _failuresByOperation_get : failure;
|
|
207
|
+
};
|
|
198
208
|
const documentsOf = (name)=>{
|
|
199
209
|
if (!collections.has(name)) collections.set(name, []);
|
|
200
210
|
return collections.get(name);
|
|
@@ -240,9 +250,8 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
240
250
|
collection: name,
|
|
241
251
|
op
|
|
242
252
|
});
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
if (failure !== undefined) return Promise.reject(failure);
|
|
253
|
+
const rejection = failureFor(op);
|
|
254
|
+
if (rejection !== undefined) return Promise.reject(rejection);
|
|
246
255
|
// `try`/`catch` rather than `Promise.resolve(produce())`: a real driver
|
|
247
256
|
// returns a promise and *rejects*, so a fake that threw synchronously would
|
|
248
257
|
// need different handling at every call site than the thing it stands in for.
|
|
@@ -331,12 +340,18 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
331
340
|
matchedCount: 0,
|
|
332
341
|
upsertedCount: 0
|
|
333
342
|
};
|
|
343
|
+
// Held to the collection's unique indexes on both branches, as
|
|
344
|
+
// `updateOne` is: an upsert racing another for the same key is
|
|
345
|
+
// exactly what a unique index exists to refuse, and a fake that let
|
|
346
|
+
// this one through would bless the duplicate the server rejects.
|
|
347
|
+
assertUnique(name, replacement);
|
|
334
348
|
documents.push(_extends({}, replacement));
|
|
335
349
|
return {
|
|
336
350
|
matchedCount: 0,
|
|
337
351
|
upsertedCount: 1
|
|
338
352
|
};
|
|
339
353
|
}
|
|
354
|
+
assertUnique(name, replacement, documents[index]);
|
|
340
355
|
documents[index] = _extends({}, replacement);
|
|
341
356
|
return {
|
|
342
357
|
matchedCount: 1,
|
|
@@ -344,11 +359,15 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
344
359
|
};
|
|
345
360
|
}),
|
|
346
361
|
/**
|
|
347
|
-
* Partial update. `$set`, `$addToSet`, `$inc` and `$setOnInsert`
|
|
348
|
-
* implemented — the operators the adapters and the stock
|
|
349
|
-
* **anything else throws**, because a fake that silently
|
|
350
|
-
* without applying it would let a spec pass on an update
|
|
351
|
-
* happened.
|
|
362
|
+
* Partial update. `$set`, `$unset`, `$addToSet`, `$inc` and `$setOnInsert`
|
|
363
|
+
* are implemented — the operators the adapters, the outbox and the stock
|
|
364
|
+
* ledger use — and **anything else throws**, because a fake that silently
|
|
365
|
+
* accepted `$rename` without applying it would let a spec pass on an update
|
|
366
|
+
* that never happened.
|
|
367
|
+
*
|
|
368
|
+
* `$unset` removes the field whatever value it is given, as the server
|
|
369
|
+
* does; it is how the outbox clears a quarantined entry's failure note when
|
|
370
|
+
* it puts the entry back in the queue.
|
|
352
371
|
*
|
|
353
372
|
* `$addToSet` is what links an identifier onto `user-identity.identifiers`
|
|
354
373
|
* without duplicating it on a repair run; supporting it here rather than
|
|
@@ -367,9 +386,10 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
367
386
|
* compensated. Both are what keep an array update from being a
|
|
368
387
|
* read-modify-write, which is the point of using them.
|
|
369
388
|
*/ updateOne: (query, update, options)=>record(name, 'updateOne', ()=>{
|
|
370
|
-
var _update_$setOnInsert;
|
|
389
|
+
var _update_$setOnInsert, _update_$unset;
|
|
371
390
|
const supported = [
|
|
372
391
|
'$set',
|
|
392
|
+
'$unset',
|
|
373
393
|
'$addToSet',
|
|
374
394
|
'$inc',
|
|
375
395
|
'$setOnInsert',
|
|
@@ -390,8 +410,16 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
390
410
|
if (conflicting.length > 0) {
|
|
391
411
|
throw new Error(`fake-mongo: updateOne has conflicting paths in $setOnInsert and $inc/$set: ${conflicting.join(', ')}`);
|
|
392
412
|
}
|
|
413
|
+
// The same refusal for a field both written and removed.
|
|
414
|
+
const unsetConflicts = Object.keys((_update_$unset = update.$unset) != null ? _update_$unset : {}).filter((field)=>{
|
|
415
|
+
var _update_$set, _update_$inc;
|
|
416
|
+
return field in ((_update_$set = update.$set) != null ? _update_$set : {}) || field in ((_update_$inc = update.$inc) != null ? _update_$inc : {});
|
|
417
|
+
});
|
|
418
|
+
if (unsetConflicts.length > 0) {
|
|
419
|
+
throw new Error(`fake-mongo: updateOne has conflicting paths in $unset and $inc/$set: ${unsetConflicts.join(', ')}`);
|
|
420
|
+
}
|
|
393
421
|
/** `$set`, `$addToSet`, `$inc` and `$push`, which apply on both branches. */ const applyOperators = (base)=>{
|
|
394
|
-
var _update_$set, _update_$set1, _update_$push, _update_$addToSet, _update_$inc;
|
|
422
|
+
var _update_$set, _update_$set1, _update_$push, _update_$addToSet, _update_$inc, _update_$unset;
|
|
395
423
|
const plain = Object.entries((_update_$set = update.$set) != null ? _update_$set : {}).filter(([field])=>!field.includes('.$['));
|
|
396
424
|
const updated = _extends({}, base, Object.fromEntries(plain));
|
|
397
425
|
for (const [path, value] of Object.entries((_update_$set1 = update.$set) != null ? _update_$set1 : {})){
|
|
@@ -435,6 +463,9 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
435
463
|
const current = updated[field];
|
|
436
464
|
updated[field] = (typeof current === 'number' ? current : 0) + delta;
|
|
437
465
|
}
|
|
466
|
+
for (const field of Object.keys((_update_$unset = update.$unset) != null ? _update_$unset : {})){
|
|
467
|
+
delete updated[field];
|
|
468
|
+
}
|
|
438
469
|
return updated;
|
|
439
470
|
};
|
|
440
471
|
const documents = documentsOf(name);
|
|
@@ -587,21 +618,51 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
587
618
|
* moves a counter and then fails to write its row cannot leave the counter
|
|
588
619
|
* moved. That is a real bug shape, and one a fake without rollback would hide
|
|
589
620
|
* by committing half of it.
|
|
590
|
-
*/ const
|
|
621
|
+
*/ const snapshotOf = ()=>new Map([
|
|
622
|
+
...collections
|
|
623
|
+
].map(([name, docs])=>[
|
|
624
|
+
name,
|
|
625
|
+
docs.map((doc)=>_extends({}, doc))
|
|
626
|
+
]));
|
|
627
|
+
const restore = (snapshot)=>{
|
|
628
|
+
collections.clear();
|
|
629
|
+
for (const [name, docs] of snapshot)collections.set(name, docs);
|
|
630
|
+
};
|
|
631
|
+
const startSession = ()=>{
|
|
591
632
|
let open = true;
|
|
633
|
+
let pending;
|
|
634
|
+
const noTransaction = ()=>Promise.reject(new Error('fake-mongo: no transaction in progress'));
|
|
592
635
|
return {
|
|
636
|
+
startTransaction: ()=>{
|
|
637
|
+
if (pending !== undefined) {
|
|
638
|
+
throw new Error('fake-mongo: a transaction is already in progress');
|
|
639
|
+
}
|
|
640
|
+
pending = snapshotOf();
|
|
641
|
+
},
|
|
642
|
+
commitTransaction: ()=>{
|
|
643
|
+
if (pending === undefined) return noTransaction();
|
|
644
|
+
operations.push({
|
|
645
|
+
collection: '',
|
|
646
|
+
op: 'commitTransaction'
|
|
647
|
+
});
|
|
648
|
+
const rejection = failureFor('commitTransaction');
|
|
649
|
+
if (rejection !== undefined) return Promise.reject(rejection);
|
|
650
|
+
pending = undefined;
|
|
651
|
+
return Promise.resolve();
|
|
652
|
+
},
|
|
653
|
+
abortTransaction: ()=>{
|
|
654
|
+
if (pending === undefined) return noTransaction();
|
|
655
|
+
restore(pending);
|
|
656
|
+
pending = undefined;
|
|
657
|
+
return Promise.resolve();
|
|
658
|
+
},
|
|
659
|
+
inTransaction: ()=>pending !== undefined,
|
|
593
660
|
withTransaction: async (body)=>{
|
|
594
|
-
const snapshot =
|
|
595
|
-
...collections
|
|
596
|
-
].map(([name, docs])=>[
|
|
597
|
-
name,
|
|
598
|
-
docs.map((doc)=>_extends({}, doc))
|
|
599
|
-
]));
|
|
661
|
+
const snapshot = snapshotOf();
|
|
600
662
|
try {
|
|
601
663
|
return await body();
|
|
602
664
|
} catch (error) {
|
|
603
|
-
|
|
604
|
-
for (const [name, docs] of snapshot)collections.set(name, docs);
|
|
665
|
+
restore(snapshot);
|
|
605
666
|
throw error;
|
|
606
667
|
}
|
|
607
668
|
},
|
|
@@ -625,6 +686,9 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
625
686
|
failOn: (operation, error)=>{
|
|
626
687
|
failuresByOperation.set(operation, error);
|
|
627
688
|
},
|
|
689
|
+
failOnce: (operation, error)=>{
|
|
690
|
+
onceFailures.set(operation, error);
|
|
691
|
+
},
|
|
628
692
|
get operations () {
|
|
629
693
|
return operations;
|
|
630
694
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/drivers/fake-mongo.ts"],"sourcesContent":["/**\n * A fake of the mongodb driver, not of the repository port.\n *\n * It sits one level *below* `makeMongoRepository`, so the adapter's own code —\n * the filter translation, the projection, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would\n * skip all of it.\n *\n * Only the surface the adapter uses is implemented; anything else is a\n * deliberate `TypeError` rather than a silent no-op.\n */\n\n/** The subset of a Mongo query document the adapter produces. */\ntype QueryDocument = Record<string, unknown>;\ntype Document = Record<string, unknown>;\n\nconst isPlainObject = (value: unknown): value is Document =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst compare = (left: unknown, right: unknown): number => {\n if (left == null && right == null) return 0;\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 if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n};\n\n/** Evaluates one `{ $op: value }` condition against a document field. */\nconst matchesCondition = (actual: unknown, condition: unknown): boolean => {\n if (!isPlainObject(condition)) {\n return actual === condition;\n }\n\n return Object.entries(condition).every(([operator, operand]) => {\n switch (operator) {\n case '$eq':\n return actual === operand;\n case '$ne':\n return actual !== operand;\n case '$gt':\n return compare(actual, operand) > 0;\n case '$gte':\n return compare(actual, operand) >= 0;\n case '$lt':\n return compare(actual, operand) < 0;\n case '$lte':\n return compare(actual, operand) <= 0;\n case '$in':\n return (operand as unknown[]).includes(actual);\n // Absence, not falsiness. A settlement ledger keys \"unpaid\" on a `runId`\n // that is not there at all, and `undefined` is what a document without\n // the field reads as — so `{ $exists: false }` must be a different test\n // from `{ $eq: undefined }` would be if the serializer ever wrote one.\n case '$exists':\n return (actual !== undefined) === (operand as boolean);\n case '$nin':\n return !(operand as unknown[]).includes(actual);\n case '$not':\n return !matchesCondition(actual, operand);\n case '$regex':\n return new RegExp(\n operand as string,\n (condition['$options'] as string) ?? '',\n ).test(String(actual));\n case '$options':\n // Handled together with `$regex`.\n return true;\n default:\n throw new TypeError(`fake mongo: unsupported operator \"${operator}\"`);\n }\n });\n};\n\n/**\n * Evaluates one node of an aggregation expression — the `$expr` half of a\n * query, which is a different language from the operators above: it compares\n * two *fields of the same document* rather than a field against a constant.\n *\n * Only the shape a conditional stock write sends is implemented — a\n * `$subtract` of two field paths compared with `$gte` — and anything else is a\n * `TypeError`. That is the same rule the rest of this file follows: a fake that\n * quietly evaluated an expression it does not understand would answer `true`\n * for a document the real server would have refused, which turns an\n * oversell-prevention test into a test that asserts nothing.\n */\nconst evaluateExpression = (doc: Document, node: unknown): unknown => {\n if (typeof node === 'string' && node.startsWith('$')) {\n return doc[node.slice(1)];\n }\n if (!isPlainObject(node)) {\n return node;\n }\n\n const entries = Object.entries(node);\n if (entries.length !== 1) {\n throw new TypeError(\n `fake mongo: $expr node must have exactly one operator, got ${entries.length}`,\n );\n }\n const [operator, operand] = entries[0];\n const operands = (operand as unknown[]).map(child =>\n evaluateExpression(doc, child),\n );\n\n switch (operator) {\n case '$subtract':\n return (operands[0] as number) - (operands[1] as number);\n case '$add':\n return (operands[0] as number) + (operands[1] as number);\n case '$gte':\n return compare(operands[0], operands[1]) >= 0;\n case '$gt':\n return compare(operands[0], operands[1]) > 0;\n case '$lte':\n return compare(operands[0], operands[1]) <= 0;\n case '$lt':\n return compare(operands[0], operands[1]) < 0;\n case '$eq':\n return operands[0] === operands[1];\n default:\n throw new TypeError(\n `fake mongo: unsupported $expr operator \"${operator}\"`,\n );\n }\n};\n\n/**\n * Every value a field path names, the way the server reads one.\n *\n * A dotted path descends, and **an array on the way down is walked\n * element-wise** — which is what makes `{'items.vendorId': x}` match an order\n * with a line for that vendor rather than nothing at all. Without it a query\n * into an embedded collection silently answers empty here while working against\n * a real server, which is worse than an unsupported-operator throw: the spec\n * passes and asserts the opposite of what it claims.\n *\n * The array itself is kept as a candidate beside its elements, because the\n * server matches both — `{ tags: 'a' }` matches `{ tags: ['a'] }` and\n * `{ tags: 'a' }` alike.\n */\nconst valuesAt = (doc: Document, field: string): unknown[] => {\n let current: unknown[] = [doc];\n for (const segment of field.split('.')) {\n const next: unknown[] = [];\n for (const value of current) {\n const container = Array.isArray(value) ? value : [value];\n for (const element of container) {\n if (isPlainObject(element)) {\n next.push(element[segment]);\n }\n }\n }\n current = next;\n }\n return current.flatMap(value =>\n Array.isArray(value) ? [value, ...value] : [value],\n );\n};\n\n/** Operators that exclude a document when a single value matches them. */\nconst NEGATIVE_OPERATORS = new Set(['$ne', '$nin', '$not']);\n\nconst isNegativeCondition = (condition: unknown): boolean =>\n isPlainObject(condition) &&\n Object.keys(condition).some(operator => NEGATIVE_OPERATORS.has(operator));\n\nconst matches = (doc: Document, query: QueryDocument): boolean =>\n Object.entries(query).every(([field, condition]) => {\n if (field === '$and') {\n return (condition as QueryDocument[]).every(child => matches(doc, child));\n }\n if (field === '$or') {\n return (condition as QueryDocument[]).some(child => matches(doc, child));\n }\n if (field === '$expr') {\n return evaluateExpression(doc, condition) === true;\n }\n const candidates = valuesAt(doc, field);\n if (candidates.length === 0) {\n return matchesCondition(undefined, condition);\n }\n // A positive condition matches when **any** value at the path satisfies it;\n // a negative one only when **every** value does. That asymmetry is the\n // server's: `{ 'items.vendorId': { $ne: 'a' } }` must exclude an order that\n // has a line for `a`, not include it because some other line differs.\n return isNegativeCondition(condition)\n ? candidates.every(value => matchesCondition(value, condition))\n : candidates.some(value => matchesCondition(value, condition));\n });\n\n/**\n * The error a real server raises when a write violates a unique index.\n *\n * The `code` matters and the message does not: callers recognise a duplicate\n * key by `error.code === 11000` (or the same code inside `writeErrors`), and a\n * fake that threw a plain `Error` would send a retry path that exists precisely\n * for this case down the \"unexpected failure\" branch instead.\n */\nclass FakeDuplicateKeyError extends Error {\n readonly code = 11000;\n\n constructor(collection: string, key: Document) {\n super(\n `fake mongo: E11000 duplicate key error collection: ${collection} dup key: ${JSON.stringify(key)}`,\n );\n this.name = 'MongoServerError';\n }\n}\n\nexport interface FakeMongoCollection {\n /** The documents currently stored, as the driver would return them. */\n readonly documents: Document[];\n}\n\nexport interface FakeMongoDb {\n /** Replaces a collection's contents. */\n seed(collection: string, documents: Document[]): void;\n /** Reads a collection's current contents. */\n read(collection: string): Document[];\n /**\n * Makes every subsequent driver call reject with `error`, so the adapter's\n * `EntifixConnError` mapping is reachable. Pass `undefined` to restore.\n */\n failWith(error: unknown): void;\n /**\n * Makes only `operation` reject, leaving the rest working — the way a read\n * that succeeds and a count that then fails reaches a distinct error branch.\n */\n failOn(operation: string, error: unknown): void;\n /** Every operation performed, for assertions about what the adapter did. */\n readonly operations: ReadonlyArray<{ collection: string; op: string }>;\n /**\n * Opens a session whose `withTransaction` rolls the whole store back when its\n * body throws. What a service hands to `client.startSession()`; see the\n * implementation for why it is atomicity and not isolation.\n */\n startSession(): {\n withTransaction(body: () => Promise<unknown>): Promise<unknown>;\n endSession(): Promise<void>;\n readonly hasEnded: boolean;\n };\n /** The object to hand to `makeMongoRepository` in place of a real `Db`. */\n readonly db: unknown;\n}\n\n/**\n * Builds an in-memory stand-in for a mongodb `Db`.\n *\n * `db` is typed `unknown` on purpose: casting it at the call site keeps this\n * package free of a hard dependency on the `mongodb` types while remaining\n * honest that it is not a real driver.\n */\nexport const makeFakeMongoDb = (\n seed: Record<string, Document[]> = {},\n): FakeMongoDb => {\n const collections = new Map<string, Document[]>(\n Object.entries(seed).map(([name, docs]) => [\n name,\n docs.map(d => ({ ...d })),\n ]),\n );\n const operations: Array<{ collection: string; op: string }> = [];\n let failure: unknown;\n const failuresByOperation = new Map<string, unknown>();\n\n const documentsOf = (name: string): Document[] => {\n if (!collections.has(name)) collections.set(name, []);\n return collections.get(name) as Document[];\n };\n\n /**\n * The unique indexes `createIndex` has been asked for, per collection, as the\n * field lists they key on.\n *\n * Recording them is not bookkeeping: `updateOne` with `upsert` is atomic per\n * *document*, so two concurrent upserts for the same key each fail to see the\n * other's uncommitted insert and each create a row. The index is what makes\n * the fold singular, and a fake that accepted `createIndex` as a no-op would\n * report a service as covered while the property the index exists for went\n * untested.\n */\n const uniqueIndexes = new Map<string, string[][]>();\n\n const uniqueIndexesOf = (name: string): string[][] =>\n uniqueIndexes.get(name) ?? [];\n\n /**\n * Refuses `candidate` if it would duplicate an existing document under one of\n * the collection's unique indexes.\n *\n * `ignored` is the document being replaced by an update, which must not count\n * as its own duplicate. A key whose value is `undefined` on the candidate is\n * skipped: a real index treats a missing field as `null` and would collide,\n * but nothing in this repo indexes an optional member, and guessing wrong\n * here would fail writes the server accepts.\n */\n const assertUnique = (\n name: string,\n candidate: Document,\n ignored?: Document,\n ): void => {\n for (const fields of uniqueIndexesOf(name)) {\n if (fields.some(field => candidate[field] === undefined)) continue;\n const clash = documentsOf(name).find(\n doc =>\n doc !== ignored &&\n fields.every(field => doc[field] === candidate[field]),\n );\n if (clash !== undefined) {\n throw new FakeDuplicateKeyError(\n name,\n Object.fromEntries(fields.map(field => [field, candidate[field]])),\n );\n }\n }\n };\n\n const record = <TValue>(name: string, op: string, produce: () => TValue) => {\n operations.push({ collection: name, op });\n const scoped = failuresByOperation.get(op);\n if (scoped !== undefined) return Promise.reject(scoped);\n if (failure !== undefined) return Promise.reject(failure);\n // `try`/`catch` rather than `Promise.resolve(produce())`: a real driver\n // returns a promise and *rejects*, so a fake that threw synchronously would\n // need different handling at every call site than the thing it stands in for.\n try {\n return Promise.resolve(produce());\n } catch (error) {\n return Promise.reject(error);\n }\n };\n\n /** Applies `{ projection: { _id: 0 } }`, the only projection the adapter uses. */\n const project = (doc: Document, options?: { projection?: Document }) => {\n const copy = { ...doc };\n if (options?.projection && options.projection['_id'] === 0) {\n delete copy['_id'];\n }\n return copy;\n };\n\n const collection = (name: string) => ({\n find: (query: QueryDocument = {}, options?: { projection?: Document }) => {\n let sortSpec: Record<string, 1 | -1> = {};\n let skipCount = 0;\n let limitCount = Infinity;\n\n const cursor = {\n sort(spec: Record<string, 1 | -1>) {\n sortSpec = spec;\n return cursor;\n },\n skip(count: number) {\n skipCount = count;\n return cursor;\n },\n limit(count: number) {\n limitCount = count;\n return cursor;\n },\n toArray: () =>\n record(name, 'find', () => {\n const matched = documentsOf(name).filter(doc =>\n matches(doc, query),\n );\n const sorted = Object.entries(sortSpec).reduceRight(\n (items, [field, direction]) =>\n [...items].sort(\n (left, right) =>\n // A dotted field sorts by the value it names, as the\n // server does — one locale of a localized member, say.\n compare(\n valuesAt(left, field)[0],\n valuesAt(right, field)[0],\n ) * direction,\n ),\n matched,\n );\n return sorted\n .slice(skipCount, skipCount + limitCount)\n .map(doc => project(doc, options));\n }),\n };\n return cursor;\n },\n\n findOne: (query: QueryDocument, options?: { projection?: Document }) =>\n record(name, 'findOne', () => {\n const found = documentsOf(name).find(doc => matches(doc, query));\n return found === undefined ? null : project(found, options);\n }),\n\n countDocuments: (query: QueryDocument = {}) =>\n record(\n name,\n 'countDocuments',\n () => documentsOf(name).filter(doc => matches(doc, query)).length,\n ),\n\n // Used by the services' own seeding, which the e2e `mock` profile runs for\n // real so both profiles serve the same catalog.\n insertMany: (documents: Document[]) =>\n record(name, 'insertMany', () => {\n const stored = documentsOf(name);\n for (const document of documents) {\n assertUnique(name, document);\n stored.push({ ...document });\n }\n return { insertedCount: documents.length };\n }),\n\n /**\n * A single insert — how an append-only ledger grows, and how a hold is\n * written beside the counter it moved.\n *\n * The options argument is accepted and ignored except for its `session`,\n * which the transaction wrapper already handles: a caller that passes one\n * is inside `withTransaction`, and this fake's rollback is a snapshot of\n * the whole store rather than anything per-operation.\n */\n insertOne: (document: Document) =>\n record(name, 'insertOne', () => {\n assertUnique(name, document);\n documentsOf(name).push({ ...document });\n return { acknowledged: true, insertedId: document['id'] ?? null };\n }),\n\n replaceOne: (\n query: QueryDocument,\n replacement: Document,\n options?: { upsert?: boolean },\n ) =>\n record(name, 'replaceOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) {\n if (!options?.upsert) return { matchedCount: 0, upsertedCount: 0 };\n documents.push({ ...replacement });\n return { matchedCount: 0, upsertedCount: 1 };\n }\n documents[index] = { ...replacement };\n return { matchedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * Partial update. `$set`, `$addToSet`, `$inc` and `$setOnInsert` are\n * implemented — the operators the adapters and the stock ledger use — and\n * **anything else throws**, because a fake that silently accepted `$unset`\n * without applying it would let a spec pass on an update that never\n * happened.\n *\n * `$addToSet` is what links an identifier onto `user-identity.identifiers`\n * without duplicating it on a repair run; supporting it here rather than\n * ignoring it is the difference between the e2e proving the link and merely\n * not noticing its absence.\n *\n * `$inc` with `upsert` is how a quantity moves. It matters that it is a\n * real increment and not a read-modify-write even here: the whole rule it\n * implements is that an absolute-value write loses updates, so a fake that\n * computed the new total from a value it had already read would model the\n * bug rather than the fix.\n *\n * `$push` appends, and a `$set` key of the form `field.$[name].member`\n * applies to every element of `field` that `options.arrayFilters` selects —\n * the pair a saga store records an outcome with and later flags one\n * compensated. Both are what keep an array update from being a\n * read-modify-write, which is the point of using them.\n */\n updateOne: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n $push?: Document;\n },\n options?: { upsert?: boolean; arrayFilters?: readonly Document[] },\n ) =>\n record(name, 'updateOne', () => {\n const supported = [\n '$set',\n '$addToSet',\n '$inc',\n '$setOnInsert',\n '$push',\n ];\n const unsupported = Object.keys(update).filter(\n operator => !supported.includes(operator),\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateOne does not implement ${unsupported.join(', ')}`,\n );\n }\n\n // A real server rejects an update whose operators both name one field,\n // and the message is the one this repro'd against: `$setOnInsert` that\n // also named a field `$inc` touches. Reproducing the refusal is what\n // keeps the fake from blessing an update Mongo would refuse.\n const conflicting = Object.keys(update.$setOnInsert ?? {}).filter(\n field => field in (update.$inc ?? {}) || field in (update.$set ?? {}),\n );\n if (conflicting.length > 0) {\n throw new Error(\n `fake-mongo: updateOne has conflicting paths in $setOnInsert and $inc/$set: ${conflicting.join(', ')}`,\n );\n }\n\n /** `$set`, `$addToSet`, `$inc` and `$push`, which apply on both branches. */\n const applyOperators = (base: Document): Document => {\n const plain = Object.entries(update.$set ?? {}).filter(\n ([field]) => !field.includes('.$['),\n );\n const updated = { ...base, ...Object.fromEntries(plain) };\n for (const [path, value] of Object.entries(update.$set ?? {})) {\n const filtered = /^([^.]+)\\.\\$\\[([^\\]]+)\\]\\.(.+)$/.exec(path);\n if (filtered === null) continue;\n const [, field, identifier, member] = filtered as unknown as [\n string,\n string,\n string,\n string,\n ];\n // The server refuses a filtered position with no filter naming it.\n const conditions = (options?.arrayFilters ?? []).flatMap(filter =>\n Object.entries(filter)\n .filter(([key]) => key.startsWith(`${identifier}.`))\n .map(\n ([key, condition]) =>\n [key.slice(identifier.length + 1), condition] as const,\n ),\n );\n if (conditions.length === 0) {\n throw new Error(\n `fake-mongo: no array filter found for identifier ${identifier}`,\n );\n }\n const current = updated[field];\n updated[field] = (Array.isArray(current) ? current : []).map(\n element =>\n isPlainObject(element) &&\n conditions.every(([key, condition]) =>\n matchesCondition(element[key], condition),\n )\n ? { ...element, [member]: value }\n : element,\n );\n }\n for (const [field, value] of Object.entries(update.$push ?? {})) {\n const current = updated[field];\n updated[field] = [\n ...(Array.isArray(current) ? current : []),\n value,\n ];\n }\n for (const [field, value] of Object.entries(update.$addToSet ?? {})) {\n const current = updated[field];\n const array = Array.isArray(current) ? current : [];\n if (!array.includes(value)) {\n updated[field] = [...array, value];\n }\n }\n for (const [field, delta] of Object.entries(update.$inc ?? {})) {\n // An absent field is created at the increment's value, which is\n // exactly why `$setOnInsert` must not also name it.\n const current = updated[field];\n updated[field] =\n (typeof current === 'number' ? current : 0) + (delta as number);\n }\n return updated;\n };\n\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n if (index === -1) {\n if (options?.upsert !== true) {\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 0 };\n }\n // The upserted document starts from the query's *equality* fields,\n // the way a real server seeds one — an operator condition contributes\n // nothing, which is why a `$expr`-guarded upsert would insert a row\n // missing the very field it filtered on.\n const seedFields = Object.fromEntries(\n Object.entries(query).filter(\n ([field, condition]) =>\n !field.startsWith('$') && !isPlainObject(condition),\n ),\n );\n const inserted = applyOperators({\n ...seedFields,\n ...(update.$setOnInsert ?? {}),\n });\n assertUnique(name, inserted);\n documents.push(inserted);\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 1 };\n }\n\n const updated = applyOperators(documents[index]);\n assertUnique(name, updated, documents[index]);\n documents[index] = updated;\n return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * `$set` across every matching document, and the count of what changed.\n *\n * ⚠️ **The count is what a caller branches on.** A settlement run claims its\n * ledger lines with this and reads `modifiedCount === 0` as \"another replica\n * took them all\" — so a fake returning a fixed `1`, or the match count\n * rather than the modified count, would make the losing replica behave like\n * the winning one and the conditional claim would test nothing.\n *\n * `$set` only. Anything else throws, for the reason `updateOne` gives:\n * silently accepting an operator it does not apply lets a spec pass on an\n * update that never happened.\n */\n updateMany: (query: QueryDocument, update: { $set?: Document }) =>\n record(name, 'updateMany', () => {\n const unsupported = Object.keys(update).filter(\n operator => operator !== '$set',\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateMany does not implement ${unsupported.join(', ')}`,\n );\n }\n\n const documents = documentsOf(name);\n let modifiedCount = 0;\n documents.forEach((doc, index) => {\n if (!matches(doc, query)) return;\n const updated = { ...doc, ...(update.$set ?? {}) };\n assertUnique(name, updated, doc);\n documents[index] = updated;\n modifiedCount += 1;\n });\n return { matchedCount: modifiedCount, modifiedCount };\n }),\n\n /**\n * Update one document and hand back the result, upserting when asked.\n *\n * ⚠️ **`returnDocument: 'after'` is the only mode implemented, and the\n * default here is `'after'` rather than the driver's `'before'`** — because\n * the callers that need this primitive at all need the merged document. A\n * two-message join upserts its own half and then asks \"is the pair complete\n * now?\", which is a question only the *after* state answers. `'before'`\n * throws rather than quietly returning the wrong half.\n *\n * Implemented over {@link updateOne} so the upsert seeding rule, the\n * conflicting-path refusal and the unique-index enforcement are the same\n * ones, rather than a second copy that could drift from them.\n */\n findOneAndUpdate: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n $push?: Document;\n },\n options?: { upsert?: boolean; returnDocument?: 'before' | 'after' },\n ) =>\n record(name, 'findOneAndUpdate', async () => {\n if (options?.returnDocument === 'before') {\n throw new Error(\n 'fake-mongo: findOneAndUpdate only implements returnDocument: \"after\"',\n );\n }\n // Where the document is *before* the write, because afterwards the\n // query may no longer find it: an upsert's operator conditions need not\n // hold of the row it just inserted — `{ folded: { $exists: false } }`\n // matches nothing once `folded` has been set. `documentsOf` hands back\n // the live array, which `updateOne` mutates in place or pushes onto, so\n // the position is enough to identify it either way.\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n const result = await collection(name).updateOne(query, update, {\n upsert: options?.upsert === true,\n });\n if (result.matchedCount === 0 && result.upsertedCount === 0) {\n return null;\n }\n\n return index === -1\n ? documents[documents.length - 1]\n : documents[index];\n }),\n\n deleteOne: (query: QueryDocument) =>\n record(name, 'deleteOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) return { deletedCount: 0 };\n documents.splice(index, 1);\n return { deletedCount: 1 };\n }),\n\n /**\n * Declares an index, and **enforces it when it is unique**.\n *\n * Idempotent, like the real one: a service that ensures its index per\n * request rather than at boot — because a tenant database only appears on\n * first write — calls this on every write.\n */\n createIndex: (spec: Document, options?: Document) =>\n record(name, 'createIndex', () => {\n if (options?.['unique'] === true) {\n const fields = Object.keys(spec);\n const existing = uniqueIndexes.get(name) ?? [];\n const already = existing.some(\n candidate =>\n candidate.length === fields.length &&\n candidate.every((field, at) => field === fields[at]),\n );\n if (!already) {\n uniqueIndexes.set(name, [...existing, fields]);\n }\n }\n return { spec, options };\n }),\n });\n\n /**\n * A session whose `withTransaction` really rolls back.\n *\n * ⚠️ **It is atomicity, not isolation, and the difference decides where a\n * test may live.** The store is a single-threaded object graph, so nothing\n * runs concurrently with the body and a \"race\" here resolves in the order it\n * was written: a concurrency property — two writers contending for the last\n * unit — is green under this fake no matter what the code does, and belongs\n * in a `*.live.spec.ts` against real Mongo.\n *\n * What it *does* model is the abort path, which is worth having: a body that\n * throws leaves the store exactly as it found it, so a conditional write that\n * moves a counter and then fails to write its row cannot leave the counter\n * moved. That is a real bug shape, and one a fake without rollback would hide\n * by committing half of it.\n */\n const startSession = () => {\n let open = true;\n return {\n withTransaction: async (body: () => Promise<unknown>) => {\n const snapshot = new Map(\n [...collections].map(([name, docs]) => [\n name,\n docs.map(doc => ({ ...doc })),\n ]),\n );\n try {\n return await body();\n } catch (error) {\n collections.clear();\n for (const [name, docs] of snapshot) collections.set(name, docs);\n throw error;\n }\n },\n endSession: () => {\n open = false;\n return Promise.resolve();\n },\n get hasEnded() {\n return !open;\n },\n };\n };\n\n return {\n seed: (name, documents) => {\n collections.set(\n name,\n documents.map(doc => ({ ...doc })),\n );\n },\n read: name => documentsOf(name).map(doc => ({ ...doc })),\n failWith: error => {\n failure = error;\n },\n failOn: (operation, error) => {\n failuresByOperation.set(operation, error);\n },\n get operations() {\n return operations;\n },\n startSession,\n db: { collection },\n };\n};\n"],"names":["isPlainObject","value","Array","isArray","compare","left","right","Date","getTime","String","localeCompare","matchesCondition","actual","condition","Object","entries","every","operator","operand","includes","undefined","RegExp","test","TypeError","evaluateExpression","doc","node","startsWith","slice","length","operands","map","child","valuesAt","field","current","segment","split","next","container","element","push","flatMap","NEGATIVE_OPERATORS","Set","isNegativeCondition","keys","some","has","matches","query","candidates","FakeDuplicateKeyError","Error","collection","key","JSON","stringify","code","name","makeFakeMongoDb","seed","collections","Map","docs","d","operations","failure","failuresByOperation","documentsOf","set","get","uniqueIndexes","uniqueIndexesOf","assertUnique","candidate","ignored","fields","clash","find","fromEntries","record","op","produce","scoped","Promise","reject","resolve","error","project","options","copy","projection","sortSpec","skipCount","limitCount","Infinity","cursor","sort","spec","skip","count","limit","toArray","matched","filter","sorted","reduceRight","items","direction","findOne","found","countDocuments","insertMany","documents","stored","document","insertedCount","insertOne","acknowledged","insertedId","replaceOne","replacement","index","findIndex","upsert","matchedCount","upsertedCount","updateOne","update","supported","unsupported","join","conflicting","$setOnInsert","$inc","$set","applyOperators","base","plain","updated","path","filtered","exec","identifier","member","conditions","arrayFilters","$push","$addToSet","array","delta","modifiedCount","seedFields","inserted","updateMany","forEach","findOneAndUpdate","returnDocument","result","deleteOne","deletedCount","splice","createIndex","existing","already","at","startSession","open","withTransaction","body","snapshot","clear","endSession","hasEnded","read","failWith","failOn","operation","db"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/drivers/fake-mongo.ts"],"sourcesContent":["/**\n * A fake of the mongodb driver, not of the repository port.\n *\n * It sits one level *below* `makeMongoRepository`, so the adapter's own code —\n * the filter translation, the projection, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would\n * skip all of it.\n *\n * Only the surface the adapter uses is implemented; anything else is a\n * deliberate `TypeError` rather than a silent no-op.\n */\n\n/** The subset of a Mongo query document the adapter produces. */\ntype QueryDocument = Record<string, unknown>;\ntype Document = Record<string, unknown>;\n\nconst isPlainObject = (value: unknown): value is Document =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst compare = (left: unknown, right: unknown): number => {\n if (left == null && right == null) return 0;\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 if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n};\n\n/** Evaluates one `{ $op: value }` condition against a document field. */\nconst matchesCondition = (actual: unknown, condition: unknown): boolean => {\n if (!isPlainObject(condition)) {\n return actual === condition;\n }\n\n return Object.entries(condition).every(([operator, operand]) => {\n switch (operator) {\n case '$eq':\n return actual === operand;\n case '$ne':\n return actual !== operand;\n case '$gt':\n return compare(actual, operand) > 0;\n case '$gte':\n return compare(actual, operand) >= 0;\n case '$lt':\n return compare(actual, operand) < 0;\n case '$lte':\n return compare(actual, operand) <= 0;\n case '$in':\n return (operand as unknown[]).includes(actual);\n // Absence, not falsiness. A settlement ledger keys \"unpaid\" on a `runId`\n // that is not there at all, and `undefined` is what a document without\n // the field reads as — so `{ $exists: false }` must be a different test\n // from `{ $eq: undefined }` would be if the serializer ever wrote one.\n case '$exists':\n return (actual !== undefined) === (operand as boolean);\n case '$nin':\n return !(operand as unknown[]).includes(actual);\n case '$not':\n return !matchesCondition(actual, operand);\n case '$regex':\n return new RegExp(\n operand as string,\n (condition['$options'] as string) ?? '',\n ).test(String(actual));\n case '$options':\n // Handled together with `$regex`.\n return true;\n default:\n throw new TypeError(`fake mongo: unsupported operator \"${operator}\"`);\n }\n });\n};\n\n/**\n * Evaluates one node of an aggregation expression — the `$expr` half of a\n * query, which is a different language from the operators above: it compares\n * two *fields of the same document* rather than a field against a constant.\n *\n * Only the shape a conditional stock write sends is implemented — a\n * `$subtract` of two field paths compared with `$gte` — and anything else is a\n * `TypeError`. That is the same rule the rest of this file follows: a fake that\n * quietly evaluated an expression it does not understand would answer `true`\n * for a document the real server would have refused, which turns an\n * oversell-prevention test into a test that asserts nothing.\n */\nconst evaluateExpression = (doc: Document, node: unknown): unknown => {\n if (typeof node === 'string' && node.startsWith('$')) {\n return doc[node.slice(1)];\n }\n if (!isPlainObject(node)) {\n return node;\n }\n\n const entries = Object.entries(node);\n if (entries.length !== 1) {\n throw new TypeError(\n `fake mongo: $expr node must have exactly one operator, got ${entries.length}`,\n );\n }\n const [operator, operand] = entries[0];\n const operands = (operand as unknown[]).map(child =>\n evaluateExpression(doc, child),\n );\n\n switch (operator) {\n case '$subtract':\n return (operands[0] as number) - (operands[1] as number);\n case '$add':\n return (operands[0] as number) + (operands[1] as number);\n case '$gte':\n return compare(operands[0], operands[1]) >= 0;\n case '$gt':\n return compare(operands[0], operands[1]) > 0;\n case '$lte':\n return compare(operands[0], operands[1]) <= 0;\n case '$lt':\n return compare(operands[0], operands[1]) < 0;\n case '$eq':\n return operands[0] === operands[1];\n default:\n throw new TypeError(\n `fake mongo: unsupported $expr operator \"${operator}\"`,\n );\n }\n};\n\n/**\n * Every value a field path names, the way the server reads one.\n *\n * A dotted path descends, and **an array on the way down is walked\n * element-wise** — which is what makes `{'items.vendorId': x}` match an order\n * with a line for that vendor rather than nothing at all. Without it a query\n * into an embedded collection silently answers empty here while working against\n * a real server, which is worse than an unsupported-operator throw: the spec\n * passes and asserts the opposite of what it claims.\n *\n * The array itself is kept as a candidate beside its elements, because the\n * server matches both — `{ tags: 'a' }` matches `{ tags: ['a'] }` and\n * `{ tags: 'a' }` alike.\n */\nconst valuesAt = (doc: Document, field: string): unknown[] => {\n let current: unknown[] = [doc];\n for (const segment of field.split('.')) {\n const next: unknown[] = [];\n for (const value of current) {\n const container = Array.isArray(value) ? value : [value];\n for (const element of container) {\n if (isPlainObject(element)) {\n next.push(element[segment]);\n }\n }\n }\n current = next;\n }\n return current.flatMap(value =>\n Array.isArray(value) ? [value, ...value] : [value],\n );\n};\n\n/** Operators that exclude a document when a single value matches them. */\nconst NEGATIVE_OPERATORS = new Set(['$ne', '$nin', '$not']);\n\nconst isNegativeCondition = (condition: unknown): boolean =>\n isPlainObject(condition) &&\n Object.keys(condition).some(operator => NEGATIVE_OPERATORS.has(operator));\n\nconst matches = (doc: Document, query: QueryDocument): boolean =>\n Object.entries(query).every(([field, condition]) => {\n if (field === '$and') {\n return (condition as QueryDocument[]).every(child => matches(doc, child));\n }\n if (field === '$or') {\n return (condition as QueryDocument[]).some(child => matches(doc, child));\n }\n if (field === '$expr') {\n return evaluateExpression(doc, condition) === true;\n }\n const candidates = valuesAt(doc, field);\n if (candidates.length === 0) {\n return matchesCondition(undefined, condition);\n }\n // A positive condition matches when **any** value at the path satisfies it;\n // a negative one only when **every** value does. That asymmetry is the\n // server's: `{ 'items.vendorId': { $ne: 'a' } }` must exclude an order that\n // has a line for `a`, not include it because some other line differs.\n return isNegativeCondition(condition)\n ? candidates.every(value => matchesCondition(value, condition))\n : candidates.some(value => matchesCondition(value, condition));\n });\n\n/**\n * The error a real server raises when a write violates a unique index.\n *\n * The `code` matters and the message does not: callers recognise a duplicate\n * key by `error.code === 11000` (or the same code inside `writeErrors`), and a\n * fake that threw a plain `Error` would send a retry path that exists precisely\n * for this case down the \"unexpected failure\" branch instead.\n */\nclass FakeDuplicateKeyError extends Error {\n readonly code = 11000;\n\n constructor(collection: string, key: Document) {\n super(\n `fake mongo: E11000 duplicate key error collection: ${collection} dup key: ${JSON.stringify(key)}`,\n );\n this.name = 'MongoServerError';\n }\n}\n\nexport interface FakeMongoCollection {\n /** The documents currently stored, as the driver would return them. */\n readonly documents: Document[];\n}\n\nexport interface FakeMongoDb {\n /** Replaces a collection's contents. */\n seed(collection: string, documents: Document[]): void;\n /** Reads a collection's current contents. */\n read(collection: string): Document[];\n /**\n * Makes every subsequent driver call reject with `error`, so the adapter's\n * `EntifixConnError` mapping is reachable. Pass `undefined` to restore.\n */\n failWith(error: unknown): void;\n /**\n * Makes only `operation` reject, leaving the rest working — the way a read\n * that succeeds and a count that then fails reaches a distinct error branch.\n */\n failOn(operation: string, error: unknown): void;\n /**\n * Makes only the **next** `operation` reject, then work again — the shape of\n * a transient conflict, which a caller is expected to retry through.\n */\n failOnce(operation: string, error: unknown): void;\n /** Every operation performed, for assertions about what the adapter did. */\n readonly operations: ReadonlyArray<{ collection: string; op: string }>;\n /**\n * Opens a session whose `withTransaction` rolls the whole store back when its\n * body throws. What a service hands to `client.startSession()`; see the\n * implementation for why it is atomicity and not isolation.\n */\n startSession(): {\n withTransaction(body: () => Promise<unknown>): Promise<unknown>;\n /**\n * The explicit form of the same transaction: `startTransaction` takes the\n * snapshot, `abortTransaction` restores it and `commitTransaction` drops\n * it. `commitTransaction` honours `failOn`/`failOnce`, and a commit that\n * fails leaves the transaction open, as an unknown commit result does.\n */\n startTransaction(): void;\n commitTransaction(): Promise<void>;\n abortTransaction(): Promise<void>;\n inTransaction(): boolean;\n endSession(): Promise<void>;\n readonly hasEnded: boolean;\n };\n /** The object to hand to `makeMongoRepository` in place of a real `Db`. */\n readonly db: unknown;\n}\n\n/**\n * Builds an in-memory stand-in for a mongodb `Db`.\n *\n * `db` is typed `unknown` on purpose: casting it at the call site keeps this\n * package free of a hard dependency on the `mongodb` types while remaining\n * honest that it is not a real driver.\n */\nexport const makeFakeMongoDb = (\n seed: Record<string, Document[]> = {},\n): FakeMongoDb => {\n const collections = new Map<string, Document[]>(\n Object.entries(seed).map(([name, docs]) => [\n name,\n docs.map(d => ({ ...d })),\n ]),\n );\n const operations: Array<{ collection: string; op: string }> = [];\n let failure: unknown;\n const failuresByOperation = new Map<string, unknown>();\n const onceFailures = new Map<string, unknown>();\n\n /** The failure `op` should reject with now, consuming a one-shot one. */\n const failureFor = (op: string): unknown => {\n if (onceFailures.has(op)) {\n const once = onceFailures.get(op);\n onceFailures.delete(op);\n return once;\n }\n return failuresByOperation.get(op) ?? failure;\n };\n\n const documentsOf = (name: string): Document[] => {\n if (!collections.has(name)) collections.set(name, []);\n return collections.get(name) as Document[];\n };\n\n /**\n * The unique indexes `createIndex` has been asked for, per collection, as the\n * field lists they key on.\n *\n * Recording them is not bookkeeping: `updateOne` with `upsert` is atomic per\n * *document*, so two concurrent upserts for the same key each fail to see the\n * other's uncommitted insert and each create a row. The index is what makes\n * the fold singular, and a fake that accepted `createIndex` as a no-op would\n * report a service as covered while the property the index exists for went\n * untested.\n */\n const uniqueIndexes = new Map<string, string[][]>();\n\n const uniqueIndexesOf = (name: string): string[][] =>\n uniqueIndexes.get(name) ?? [];\n\n /**\n * Refuses `candidate` if it would duplicate an existing document under one of\n * the collection's unique indexes.\n *\n * `ignored` is the document being replaced by an update, which must not count\n * as its own duplicate. A key whose value is `undefined` on the candidate is\n * skipped: a real index treats a missing field as `null` and would collide,\n * but nothing in this repo indexes an optional member, and guessing wrong\n * here would fail writes the server accepts.\n */\n const assertUnique = (\n name: string,\n candidate: Document,\n ignored?: Document,\n ): void => {\n for (const fields of uniqueIndexesOf(name)) {\n if (fields.some(field => candidate[field] === undefined)) continue;\n const clash = documentsOf(name).find(\n doc =>\n doc !== ignored &&\n fields.every(field => doc[field] === candidate[field]),\n );\n if (clash !== undefined) {\n throw new FakeDuplicateKeyError(\n name,\n Object.fromEntries(fields.map(field => [field, candidate[field]])),\n );\n }\n }\n };\n\n const record = <TValue>(name: string, op: string, produce: () => TValue) => {\n operations.push({ collection: name, op });\n const rejection = failureFor(op);\n if (rejection !== undefined) return Promise.reject(rejection);\n // `try`/`catch` rather than `Promise.resolve(produce())`: a real driver\n // returns a promise and *rejects*, so a fake that threw synchronously would\n // need different handling at every call site than the thing it stands in for.\n try {\n return Promise.resolve(produce());\n } catch (error) {\n return Promise.reject(error);\n }\n };\n\n /** Applies `{ projection: { _id: 0 } }`, the only projection the adapter uses. */\n const project = (doc: Document, options?: { projection?: Document }) => {\n const copy = { ...doc };\n if (options?.projection && options.projection['_id'] === 0) {\n delete copy['_id'];\n }\n return copy;\n };\n\n const collection = (name: string) => ({\n find: (query: QueryDocument = {}, options?: { projection?: Document }) => {\n let sortSpec: Record<string, 1 | -1> = {};\n let skipCount = 0;\n let limitCount = Infinity;\n\n const cursor = {\n sort(spec: Record<string, 1 | -1>) {\n sortSpec = spec;\n return cursor;\n },\n skip(count: number) {\n skipCount = count;\n return cursor;\n },\n limit(count: number) {\n limitCount = count;\n return cursor;\n },\n toArray: () =>\n record(name, 'find', () => {\n const matched = documentsOf(name).filter(doc =>\n matches(doc, query),\n );\n const sorted = Object.entries(sortSpec).reduceRight(\n (items, [field, direction]) =>\n [...items].sort(\n (left, right) =>\n // A dotted field sorts by the value it names, as the\n // server does — one locale of a localized member, say.\n compare(\n valuesAt(left, field)[0],\n valuesAt(right, field)[0],\n ) * direction,\n ),\n matched,\n );\n return sorted\n .slice(skipCount, skipCount + limitCount)\n .map(doc => project(doc, options));\n }),\n };\n return cursor;\n },\n\n findOne: (query: QueryDocument, options?: { projection?: Document }) =>\n record(name, 'findOne', () => {\n const found = documentsOf(name).find(doc => matches(doc, query));\n return found === undefined ? null : project(found, options);\n }),\n\n countDocuments: (query: QueryDocument = {}) =>\n record(\n name,\n 'countDocuments',\n () => documentsOf(name).filter(doc => matches(doc, query)).length,\n ),\n\n // Used by the services' own seeding, which the e2e `mock` profile runs for\n // real so both profiles serve the same catalog.\n insertMany: (documents: Document[]) =>\n record(name, 'insertMany', () => {\n const stored = documentsOf(name);\n for (const document of documents) {\n assertUnique(name, document);\n stored.push({ ...document });\n }\n return { insertedCount: documents.length };\n }),\n\n /**\n * A single insert — how an append-only ledger grows, and how a hold is\n * written beside the counter it moved.\n *\n * The options argument is accepted and ignored except for its `session`,\n * which the transaction wrapper already handles: a caller that passes one\n * is inside `withTransaction`, and this fake's rollback is a snapshot of\n * the whole store rather than anything per-operation.\n */\n insertOne: (document: Document) =>\n record(name, 'insertOne', () => {\n assertUnique(name, document);\n documentsOf(name).push({ ...document });\n return { acknowledged: true, insertedId: document['id'] ?? null };\n }),\n\n replaceOne: (\n query: QueryDocument,\n replacement: Document,\n options?: { upsert?: boolean },\n ) =>\n record(name, 'replaceOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) {\n if (!options?.upsert) return { matchedCount: 0, upsertedCount: 0 };\n // Held to the collection's unique indexes on both branches, as\n // `updateOne` is: an upsert racing another for the same key is\n // exactly what a unique index exists to refuse, and a fake that let\n // this one through would bless the duplicate the server rejects.\n assertUnique(name, replacement);\n documents.push({ ...replacement });\n return { matchedCount: 0, upsertedCount: 1 };\n }\n assertUnique(name, replacement, documents[index]);\n documents[index] = { ...replacement };\n return { matchedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * Partial update. `$set`, `$unset`, `$addToSet`, `$inc` and `$setOnInsert`\n * are implemented — the operators the adapters, the outbox and the stock\n * ledger use — and **anything else throws**, because a fake that silently\n * accepted `$rename` without applying it would let a spec pass on an update\n * that never happened.\n *\n * `$unset` removes the field whatever value it is given, as the server\n * does; it is how the outbox clears a quarantined entry's failure note when\n * it puts the entry back in the queue.\n *\n * `$addToSet` is what links an identifier onto `user-identity.identifiers`\n * without duplicating it on a repair run; supporting it here rather than\n * ignoring it is the difference between the e2e proving the link and merely\n * not noticing its absence.\n *\n * `$inc` with `upsert` is how a quantity moves. It matters that it is a\n * real increment and not a read-modify-write even here: the whole rule it\n * implements is that an absolute-value write loses updates, so a fake that\n * computed the new total from a value it had already read would model the\n * bug rather than the fix.\n *\n * `$push` appends, and a `$set` key of the form `field.$[name].member`\n * applies to every element of `field` that `options.arrayFilters` selects —\n * the pair a saga store records an outcome with and later flags one\n * compensated. Both are what keep an array update from being a\n * read-modify-write, which is the point of using them.\n */\n updateOne: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $unset?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n $push?: Document;\n },\n options?: { upsert?: boolean; arrayFilters?: readonly Document[] },\n ) =>\n record(name, 'updateOne', () => {\n const supported = [\n '$set',\n '$unset',\n '$addToSet',\n '$inc',\n '$setOnInsert',\n '$push',\n ];\n const unsupported = Object.keys(update).filter(\n operator => !supported.includes(operator),\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateOne does not implement ${unsupported.join(', ')}`,\n );\n }\n\n // A real server rejects an update whose operators both name one field,\n // and the message is the one this repro'd against: `$setOnInsert` that\n // also named a field `$inc` touches. Reproducing the refusal is what\n // keeps the fake from blessing an update Mongo would refuse.\n const conflicting = Object.keys(update.$setOnInsert ?? {}).filter(\n field => field in (update.$inc ?? {}) || field in (update.$set ?? {}),\n );\n if (conflicting.length > 0) {\n throw new Error(\n `fake-mongo: updateOne has conflicting paths in $setOnInsert and $inc/$set: ${conflicting.join(', ')}`,\n );\n }\n // The same refusal for a field both written and removed.\n const unsetConflicts = Object.keys(update.$unset ?? {}).filter(\n field => field in (update.$set ?? {}) || field in (update.$inc ?? {}),\n );\n if (unsetConflicts.length > 0) {\n throw new Error(\n `fake-mongo: updateOne has conflicting paths in $unset and $inc/$set: ${unsetConflicts.join(', ')}`,\n );\n }\n\n /** `$set`, `$addToSet`, `$inc` and `$push`, which apply on both branches. */\n const applyOperators = (base: Document): Document => {\n const plain = Object.entries(update.$set ?? {}).filter(\n ([field]) => !field.includes('.$['),\n );\n const updated = { ...base, ...Object.fromEntries(plain) };\n for (const [path, value] of Object.entries(update.$set ?? {})) {\n const filtered = /^([^.]+)\\.\\$\\[([^\\]]+)\\]\\.(.+)$/.exec(path);\n if (filtered === null) continue;\n const [, field, identifier, member] = filtered as unknown as [\n string,\n string,\n string,\n string,\n ];\n // The server refuses a filtered position with no filter naming it.\n const conditions = (options?.arrayFilters ?? []).flatMap(filter =>\n Object.entries(filter)\n .filter(([key]) => key.startsWith(`${identifier}.`))\n .map(\n ([key, condition]) =>\n [key.slice(identifier.length + 1), condition] as const,\n ),\n );\n if (conditions.length === 0) {\n throw new Error(\n `fake-mongo: no array filter found for identifier ${identifier}`,\n );\n }\n const current = updated[field];\n updated[field] = (Array.isArray(current) ? current : []).map(\n element =>\n isPlainObject(element) &&\n conditions.every(([key, condition]) =>\n matchesCondition(element[key], condition),\n )\n ? { ...element, [member]: value }\n : element,\n );\n }\n for (const [field, value] of Object.entries(update.$push ?? {})) {\n const current = updated[field];\n updated[field] = [\n ...(Array.isArray(current) ? current : []),\n value,\n ];\n }\n for (const [field, value] of Object.entries(update.$addToSet ?? {})) {\n const current = updated[field];\n const array = Array.isArray(current) ? current : [];\n if (!array.includes(value)) {\n updated[field] = [...array, value];\n }\n }\n for (const [field, delta] of Object.entries(update.$inc ?? {})) {\n // An absent field is created at the increment's value, which is\n // exactly why `$setOnInsert` must not also name it.\n const current = updated[field];\n updated[field] =\n (typeof current === 'number' ? current : 0) + (delta as number);\n }\n for (const field of Object.keys(update.$unset ?? {})) {\n delete updated[field];\n }\n return updated;\n };\n\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n if (index === -1) {\n if (options?.upsert !== true) {\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 0 };\n }\n // The upserted document starts from the query's *equality* fields,\n // the way a real server seeds one — an operator condition contributes\n // nothing, which is why a `$expr`-guarded upsert would insert a row\n // missing the very field it filtered on.\n const seedFields = Object.fromEntries(\n Object.entries(query).filter(\n ([field, condition]) =>\n !field.startsWith('$') && !isPlainObject(condition),\n ),\n );\n const inserted = applyOperators({\n ...seedFields,\n ...(update.$setOnInsert ?? {}),\n });\n assertUnique(name, inserted);\n documents.push(inserted);\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 1 };\n }\n\n const updated = applyOperators(documents[index]);\n assertUnique(name, updated, documents[index]);\n documents[index] = updated;\n return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * `$set` across every matching document, and the count of what changed.\n *\n * ⚠️ **The count is what a caller branches on.** A settlement run claims its\n * ledger lines with this and reads `modifiedCount === 0` as \"another replica\n * took them all\" — so a fake returning a fixed `1`, or the match count\n * rather than the modified count, would make the losing replica behave like\n * the winning one and the conditional claim would test nothing.\n *\n * `$set` only. Anything else throws, for the reason `updateOne` gives:\n * silently accepting an operator it does not apply lets a spec pass on an\n * update that never happened.\n */\n updateMany: (query: QueryDocument, update: { $set?: Document }) =>\n record(name, 'updateMany', () => {\n const unsupported = Object.keys(update).filter(\n operator => operator !== '$set',\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateMany does not implement ${unsupported.join(', ')}`,\n );\n }\n\n const documents = documentsOf(name);\n let modifiedCount = 0;\n documents.forEach((doc, index) => {\n if (!matches(doc, query)) return;\n const updated = { ...doc, ...(update.$set ?? {}) };\n assertUnique(name, updated, doc);\n documents[index] = updated;\n modifiedCount += 1;\n });\n return { matchedCount: modifiedCount, modifiedCount };\n }),\n\n /**\n * Update one document and hand back the result, upserting when asked.\n *\n * ⚠️ **`returnDocument: 'after'` is the only mode implemented, and the\n * default here is `'after'` rather than the driver's `'before'`** — because\n * the callers that need this primitive at all need the merged document. A\n * two-message join upserts its own half and then asks \"is the pair complete\n * now?\", which is a question only the *after* state answers. `'before'`\n * throws rather than quietly returning the wrong half.\n *\n * Implemented over {@link updateOne} so the upsert seeding rule, the\n * conflicting-path refusal and the unique-index enforcement are the same\n * ones, rather than a second copy that could drift from them.\n */\n findOneAndUpdate: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $unset?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n $push?: Document;\n },\n options?: { upsert?: boolean; returnDocument?: 'before' | 'after' },\n ) =>\n record(name, 'findOneAndUpdate', async () => {\n if (options?.returnDocument === 'before') {\n throw new Error(\n 'fake-mongo: findOneAndUpdate only implements returnDocument: \"after\"',\n );\n }\n // Where the document is *before* the write, because afterwards the\n // query may no longer find it: an upsert's operator conditions need not\n // hold of the row it just inserted — `{ folded: { $exists: false } }`\n // matches nothing once `folded` has been set. `documentsOf` hands back\n // the live array, which `updateOne` mutates in place or pushes onto, so\n // the position is enough to identify it either way.\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n const result = await collection(name).updateOne(query, update, {\n upsert: options?.upsert === true,\n });\n if (result.matchedCount === 0 && result.upsertedCount === 0) {\n return null;\n }\n\n return index === -1\n ? documents[documents.length - 1]\n : documents[index];\n }),\n\n deleteOne: (query: QueryDocument) =>\n record(name, 'deleteOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) return { deletedCount: 0 };\n documents.splice(index, 1);\n return { deletedCount: 1 };\n }),\n\n /**\n * Declares an index, and **enforces it when it is unique**.\n *\n * Idempotent, like the real one: a service that ensures its index per\n * request rather than at boot — because a tenant database only appears on\n * first write — calls this on every write.\n */\n createIndex: (spec: Document, options?: Document) =>\n record(name, 'createIndex', () => {\n if (options?.['unique'] === true) {\n const fields = Object.keys(spec);\n const existing = uniqueIndexes.get(name) ?? [];\n const already = existing.some(\n candidate =>\n candidate.length === fields.length &&\n candidate.every((field, at) => field === fields[at]),\n );\n if (!already) {\n uniqueIndexes.set(name, [...existing, fields]);\n }\n }\n return { spec, options };\n }),\n });\n\n /**\n * A session whose `withTransaction` really rolls back.\n *\n * ⚠️ **It is atomicity, not isolation, and the difference decides where a\n * test may live.** The store is a single-threaded object graph, so nothing\n * runs concurrently with the body and a \"race\" here resolves in the order it\n * was written: a concurrency property — two writers contending for the last\n * unit — is green under this fake no matter what the code does, and belongs\n * in a `*.live.spec.ts` against real Mongo.\n *\n * What it *does* model is the abort path, which is worth having: a body that\n * throws leaves the store exactly as it found it, so a conditional write that\n * moves a counter and then fails to write its row cannot leave the counter\n * moved. That is a real bug shape, and one a fake without rollback would hide\n * by committing half of it.\n */\n const snapshotOf = () =>\n new Map(\n [...collections].map(([name, docs]) => [\n name,\n docs.map(doc => ({ ...doc })),\n ]),\n );\n const restore = (snapshot: Map<string, Document[]>) => {\n collections.clear();\n for (const [name, docs] of snapshot) collections.set(name, docs);\n };\n\n const startSession = () => {\n let open = true;\n let pending: Map<string, Document[]> | undefined;\n const noTransaction = () =>\n Promise.reject(new Error('fake-mongo: no transaction in progress'));\n return {\n startTransaction: () => {\n if (pending !== undefined) {\n throw new Error('fake-mongo: a transaction is already in progress');\n }\n pending = snapshotOf();\n },\n commitTransaction: () => {\n if (pending === undefined) return noTransaction();\n operations.push({ collection: '', op: 'commitTransaction' });\n const rejection = failureFor('commitTransaction');\n if (rejection !== undefined) return Promise.reject(rejection);\n pending = undefined;\n return Promise.resolve();\n },\n abortTransaction: () => {\n if (pending === undefined) return noTransaction();\n restore(pending);\n pending = undefined;\n return Promise.resolve();\n },\n inTransaction: () => pending !== undefined,\n withTransaction: async (body: () => Promise<unknown>) => {\n const snapshot = snapshotOf();\n try {\n return await body();\n } catch (error) {\n restore(snapshot);\n throw error;\n }\n },\n endSession: () => {\n open = false;\n return Promise.resolve();\n },\n get hasEnded() {\n return !open;\n },\n };\n };\n\n return {\n seed: (name, documents) => {\n collections.set(\n name,\n documents.map(doc => ({ ...doc })),\n );\n },\n read: name => documentsOf(name).map(doc => ({ ...doc })),\n failWith: error => {\n failure = error;\n },\n failOn: (operation, error) => {\n failuresByOperation.set(operation, error);\n },\n failOnce: (operation, error) => {\n onceFailures.set(operation, error);\n },\n get operations() {\n return operations;\n },\n startSession,\n db: { collection },\n };\n};\n"],"names":["isPlainObject","value","Array","isArray","compare","left","right","Date","getTime","String","localeCompare","matchesCondition","actual","condition","Object","entries","every","operator","operand","includes","undefined","RegExp","test","TypeError","evaluateExpression","doc","node","startsWith","slice","length","operands","map","child","valuesAt","field","current","segment","split","next","container","element","push","flatMap","NEGATIVE_OPERATORS","Set","isNegativeCondition","keys","some","has","matches","query","candidates","FakeDuplicateKeyError","Error","collection","key","JSON","stringify","code","name","makeFakeMongoDb","seed","collections","Map","docs","d","operations","failure","failuresByOperation","onceFailures","failureFor","op","once","get","delete","documentsOf","set","uniqueIndexes","uniqueIndexesOf","assertUnique","candidate","ignored","fields","clash","find","fromEntries","record","produce","rejection","Promise","reject","resolve","error","project","options","copy","projection","sortSpec","skipCount","limitCount","Infinity","cursor","sort","spec","skip","count","limit","toArray","matched","filter","sorted","reduceRight","items","direction","findOne","found","countDocuments","insertMany","documents","stored","document","insertedCount","insertOne","acknowledged","insertedId","replaceOne","replacement","index","findIndex","upsert","matchedCount","upsertedCount","updateOne","update","supported","unsupported","join","conflicting","$setOnInsert","$inc","$set","unsetConflicts","$unset","applyOperators","base","plain","updated","path","filtered","exec","identifier","member","conditions","arrayFilters","$push","$addToSet","array","delta","modifiedCount","seedFields","inserted","updateMany","forEach","findOneAndUpdate","returnDocument","result","deleteOne","deletedCount","splice","createIndex","existing","already","at","snapshotOf","restore","snapshot","clear","startSession","open","pending","noTransaction","startTransaction","commitTransaction","abortTransaction","inTransaction","withTransaction","body","endSession","hasEnded","read","failWith","failOn","operation","failOnce","db"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export * from './doubles/configuration.js';
|
|
|
3
3
|
export * from './doubles/entity-repository.js';
|
|
4
4
|
export * from './doubles/link-resolver.js';
|
|
5
5
|
export * from './doubles/transaction-ports.js';
|
|
6
|
+
export * from './doubles/unit-of-work.js';
|
|
6
7
|
export * from './effect/run.js';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from './doubles/configuration.js';
|
|
|
3
3
|
export * from './doubles/entity-repository.js';
|
|
4
4
|
export * from './doubles/link-resolver.js';
|
|
5
5
|
export * from './doubles/transaction-ports.js';
|
|
6
|
+
export * from './doubles/unit-of-work.js';
|
|
6
7
|
export * from './effect/run.js';
|
|
7
8
|
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './builders/entity-builder.js';\nexport * from './doubles/configuration.js';\nexport * from './doubles/entity-repository.js';\nexport * from './doubles/link-resolver.js';\nexport * from './doubles/transaction-ports.js';\nexport * from './effect/run.js';\n"],"names":[],"mappings":"AAAA,cAAc,+BAA+B;AAC7C,cAAc,6BAA6B;AAC3C,cAAc,iCAAiC;AAC/C,cAAc,6BAA6B;AAC3C,cAAc,iCAAiC;AAC/C,cAAc,kBAAkB"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './builders/entity-builder.js';\nexport * from './doubles/configuration.js';\nexport * from './doubles/entity-repository.js';\nexport * from './doubles/link-resolver.js';\nexport * from './doubles/transaction-ports.js';\nexport * from './doubles/unit-of-work.js';\nexport * from './effect/run.js';\n"],"names":[],"mappings":"AAAA,cAAc,+BAA+B;AAC7C,cAAc,6BAA6B;AAC3C,cAAc,iCAAiC;AAC/C,cAAc,6BAA6B;AAC3C,cAAc,iCAAiC;AAC/C,cAAc,4BAA4B;AAC1C,cAAc,kBAAkB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/testing-unit",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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": {
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@swc/helpers": "0.5.23",
|
|
75
|
-
"@entifix/business": "0.7.
|
|
76
|
-
"@entifix/transactions": "0.7.
|
|
77
|
-
"@entifix/core": "0.7.
|
|
75
|
+
"@entifix/business": "0.7.2",
|
|
76
|
+
"@entifix/transactions": "0.7.2",
|
|
77
|
+
"@entifix/core": "0.7.2"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"@testing-library/react": "*",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"msw": ">=3",
|
|
83
83
|
"react": "*",
|
|
84
84
|
"vitest": "*",
|
|
85
|
-
"@entifix/react-controls": "0.7.
|
|
85
|
+
"@entifix/react-controls": "0.7.2"
|
|
86
86
|
},
|
|
87
87
|
"peerDependenciesMeta": {
|
|
88
88
|
"@entifix/react-controls": {
|