@entifix/testing-unit 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/contracts/entity-facet-counter.contract.d.ts +30 -0
- package/dist/contracts/entity-facet-counter.contract.d.ts.map +1 -0
- package/dist/contracts/entity-facet-counter.contract.js +194 -0
- package/dist/contracts/entity-facet-counter.contract.js.map +1 -0
- package/dist/contracts/entity-repository.contract.d.ts.map +1 -1
- package/dist/contracts/entity-repository.contract.js +43 -0
- package/dist/contracts/entity-repository.contract.js.map +1 -1
- 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/doubles/entity-facet-counter.d.ts +21 -0
- package/dist/doubles/entity-facet-counter.d.ts.map +1 -0
- package/dist/doubles/entity-facet-counter.js +28 -0
- package/dist/doubles/entity-facet-counter.js.map +1 -0
- package/dist/drivers/fake-mongo.d.ts.map +1 -1
- package/dist/drivers/fake-mongo.js +83 -0
- package/dist/drivers/fake-mongo.js.map +1 -1
- package/dist/http/entity-handlers.d.ts +2 -2
- package/dist/http/entity-handlers.d.ts.map +1 -1
- package/dist/http/entity-handlers.js +19 -3
- package/dist/http/entity-handlers.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,12 @@ 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
|
+
`describeEntityFacetCounterContract` holds a facet counter to Mongo's `$facet`
|
|
56
|
+
semantics — each member under its own filtering, an array member counted per
|
|
57
|
+
element, a missing value not counted — and `makeInMemoryFacetCounter` is the
|
|
58
|
+
double that passes it. A counter reached through the query protocol passes
|
|
59
|
+
`countsCollections: false`, since no collection member is ever `filterable`.
|
|
60
|
+
|
|
55
61
|
`describeUnitOfWorkContract` holds a unit of work to the same rules whatever
|
|
56
62
|
its store: writes, events and the claim commit or vanish together, a taken
|
|
57
63
|
claim is a `duplicate` that never runs the body, and a business failure is an
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EntityFacetCounter } from '@entifix/business';
|
|
2
|
+
import { ContractWidget } from './entity-repository.contract.js';
|
|
3
|
+
export interface EntityFacetCounterContractOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Builds a counter over `seed`. Called fresh for each test, so no state
|
|
6
|
+
* leaks between them.
|
|
7
|
+
*/
|
|
8
|
+
makeCounter(seed: ContractWidget[]): EntityFacetCounter | Promise<EntityFacetCounter>;
|
|
9
|
+
/**
|
|
10
|
+
* `false` for a counter reached through the query protocol. A collection
|
|
11
|
+
* member is never `filterable` — core refuses to declare one so — and the
|
|
12
|
+
* protocol counts only filterable members, so over the wire an array member
|
|
13
|
+
* is a `400` before any counter sees it. Defaults to `true`.
|
|
14
|
+
*/
|
|
15
|
+
readonly countsCollections?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* What every {@link EntityFacetCounter} must answer alike: how many records
|
|
19
|
+
* hold each value of each member, each member under its own filtering — the
|
|
20
|
+
* semantics of a Mongo `$facet` of `$match`, `$unwind` and `$group`.
|
|
21
|
+
*
|
|
22
|
+
* Run it against the in-memory counter *and* against each real adapter, so the
|
|
23
|
+
* fake a consumer's specs use cannot drift into a more forgiving version of
|
|
24
|
+
* the engine.
|
|
25
|
+
*
|
|
26
|
+
* Which member a request may count is the protocol's rule, not the counter's
|
|
27
|
+
* (`parseFacetQueryParams`): a counter counts what it is handed.
|
|
28
|
+
*/
|
|
29
|
+
export declare const describeEntityFacetCounterContract: (name: string, { makeCounter, countsCollections }: EntityFacetCounterContractOptions) => void;
|
|
30
|
+
//# sourceMappingURL=entity-facet-counter.contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-facet-counter.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/entity-facet-counter.contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAI5D,OAAO,EACL,cAAc,EAEf,MAAM,iCAAiC,CAAC;AAEzC,MAAM,WAAW,iCAAiC;IAChD;;;OAGG;IACH,WAAW,CACT,IAAI,EAAE,cAAc,EAAE,GACrB,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpD;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAQD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kCAAkC,GAC7C,MAAM,MAAM,EACZ,oCAA2C,iCAAiC,KAC3E,IAuHF,CAAC"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { run } from '../effect/run.js';
|
|
3
|
+
import { ContractWidget, makeContractWidget } from './entity-repository.contract.js';
|
|
4
|
+
const seeded = ()=>[
|
|
5
|
+
makeContractWidget('w-1', 'Alpha', 10, [
|
|
6
|
+
'blue',
|
|
7
|
+
'green'
|
|
8
|
+
]),
|
|
9
|
+
makeContractWidget('w-2', 'Beta', 10, [
|
|
10
|
+
'green'
|
|
11
|
+
]),
|
|
12
|
+
makeContractWidget('w-3', 'Alpha', 30, [
|
|
13
|
+
'green',
|
|
14
|
+
'red'
|
|
15
|
+
])
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* What every {@link EntityFacetCounter} must answer alike: how many records
|
|
19
|
+
* hold each value of each member, each member under its own filtering — the
|
|
20
|
+
* semantics of a Mongo `$facet` of `$match`, `$unwind` and `$group`.
|
|
21
|
+
*
|
|
22
|
+
* Run it against the in-memory counter *and* against each real adapter, so the
|
|
23
|
+
* fake a consumer's specs use cannot drift into a more forgiving version of
|
|
24
|
+
* the engine.
|
|
25
|
+
*
|
|
26
|
+
* Which member a request may count is the protocol's rule, not the counter's
|
|
27
|
+
* (`parseFacetQueryParams`): a counter counts what it is handed.
|
|
28
|
+
*/ export const describeEntityFacetCounterContract = (name, { makeCounter, countsCollections = true })=>{
|
|
29
|
+
describe(`EntityFacetCounter contract: ${name}`, ()=>{
|
|
30
|
+
const withCounter = async (use, seed = seeded())=>{
|
|
31
|
+
await use(await makeCounter(seed));
|
|
32
|
+
};
|
|
33
|
+
it('counts every value of every member, highest count first', async ()=>{
|
|
34
|
+
await withCounter(async (counter)=>{
|
|
35
|
+
const counts = await run(counter.count([
|
|
36
|
+
{
|
|
37
|
+
member: 'name'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
member: 'size'
|
|
41
|
+
}
|
|
42
|
+
]));
|
|
43
|
+
expect(counts).toEqual({
|
|
44
|
+
name: [
|
|
45
|
+
{
|
|
46
|
+
value: 'Alpha',
|
|
47
|
+
count: 2
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: 'Beta',
|
|
51
|
+
count: 1
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
size: [
|
|
55
|
+
{
|
|
56
|
+
value: 10,
|
|
57
|
+
count: 2
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: 30,
|
|
61
|
+
count: 1
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('counts each member under its own filtering only', async ()=>{
|
|
68
|
+
await withCounter(async (counter)=>{
|
|
69
|
+
const counts = await run(counter.count([
|
|
70
|
+
{
|
|
71
|
+
member: 'name',
|
|
72
|
+
filtering: [
|
|
73
|
+
{
|
|
74
|
+
property: 'size',
|
|
75
|
+
operator: 'eq',
|
|
76
|
+
value: 10
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
member: 'size',
|
|
82
|
+
filtering: [
|
|
83
|
+
{
|
|
84
|
+
property: 'name',
|
|
85
|
+
operator: 'eq',
|
|
86
|
+
value: 'Alpha'
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]));
|
|
91
|
+
expect(counts).toEqual({
|
|
92
|
+
name: [
|
|
93
|
+
{
|
|
94
|
+
value: 'Alpha',
|
|
95
|
+
count: 1
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
value: 'Beta',
|
|
99
|
+
count: 1
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
size: [
|
|
103
|
+
{
|
|
104
|
+
value: 10,
|
|
105
|
+
count: 1
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
value: 30,
|
|
109
|
+
count: 1
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
it.runIf(countsCollections)('counts each element of an array member', async ()=>{
|
|
116
|
+
await withCounter(async (counter)=>{
|
|
117
|
+
const counts = await run(counter.count([
|
|
118
|
+
{
|
|
119
|
+
member: 'tags'
|
|
120
|
+
}
|
|
121
|
+
]));
|
|
122
|
+
expect(counts).toEqual({
|
|
123
|
+
tags: [
|
|
124
|
+
{
|
|
125
|
+
value: 'green',
|
|
126
|
+
count: 3
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
value: 'blue',
|
|
130
|
+
count: 1
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
value: 'red',
|
|
134
|
+
count: 1
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
it('does not count a missing value', async ()=>{
|
|
141
|
+
const unnamed = new ContractWidget();
|
|
142
|
+
unnamed.id = 'w-9';
|
|
143
|
+
await withCounter(async (counter)=>{
|
|
144
|
+
const counts = await run(counter.count([
|
|
145
|
+
{
|
|
146
|
+
member: 'name'
|
|
147
|
+
}
|
|
148
|
+
]));
|
|
149
|
+
expect(counts).toEqual({
|
|
150
|
+
name: [
|
|
151
|
+
{
|
|
152
|
+
value: 'Alpha',
|
|
153
|
+
count: 1
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
});
|
|
157
|
+
}, [
|
|
158
|
+
makeContractWidget('w-1', 'Alpha', 10),
|
|
159
|
+
unnamed
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
it.runIf(countsCollections)('does not count an empty array', async ()=>{
|
|
163
|
+
await withCounter(async (counter)=>{
|
|
164
|
+
const counts = await run(counter.count([
|
|
165
|
+
{
|
|
166
|
+
member: 'tags'
|
|
167
|
+
}
|
|
168
|
+
]));
|
|
169
|
+
expect(counts).toEqual({
|
|
170
|
+
tags: []
|
|
171
|
+
});
|
|
172
|
+
}, [
|
|
173
|
+
makeContractWidget('w-1', 'Alpha', 10)
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
it('answers every queried member, empty, from an empty store', async ()=>{
|
|
177
|
+
await withCounter(async (counter)=>{
|
|
178
|
+
expect(await run(counter.count([
|
|
179
|
+
{
|
|
180
|
+
member: 'name'
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
member: 'size'
|
|
184
|
+
}
|
|
185
|
+
]))).toEqual({
|
|
186
|
+
name: [],
|
|
187
|
+
size: []
|
|
188
|
+
});
|
|
189
|
+
}, []);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
//# sourceMappingURL=entity-facet-counter.contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/contracts/entity-facet-counter.contract.ts"],"sourcesContent":["import type { EntityFacetCounter } from '@entifix/business';\nimport { describe, expect, it } from 'vitest';\n\nimport { run } from '../effect/run.js';\nimport {\n ContractWidget,\n makeContractWidget,\n} from './entity-repository.contract.js';\n\nexport interface EntityFacetCounterContractOptions {\n /**\n * Builds a counter over `seed`. Called fresh for each test, so no state\n * leaks between them.\n */\n makeCounter(\n seed: ContractWidget[],\n ): EntityFacetCounter | Promise<EntityFacetCounter>;\n /**\n * `false` for a counter reached through the query protocol. A collection\n * member is never `filterable` — core refuses to declare one so — and the\n * protocol counts only filterable members, so over the wire an array member\n * is a `400` before any counter sees it. Defaults to `true`.\n */\n readonly countsCollections?: boolean;\n}\n\nconst seeded = () => [\n makeContractWidget('w-1', 'Alpha', 10, ['blue', 'green']),\n makeContractWidget('w-2', 'Beta', 10, ['green']),\n makeContractWidget('w-3', 'Alpha', 30, ['green', 'red']),\n];\n\n/**\n * What every {@link EntityFacetCounter} must answer alike: how many records\n * hold each value of each member, each member under its own filtering — the\n * semantics of a Mongo `$facet` of `$match`, `$unwind` and `$group`.\n *\n * Run it against the in-memory counter *and* against each real adapter, so the\n * fake a consumer's specs use cannot drift into a more forgiving version of\n * the engine.\n *\n * Which member a request may count is the protocol's rule, not the counter's\n * (`parseFacetQueryParams`): a counter counts what it is handed.\n */\nexport const describeEntityFacetCounterContract = (\n name: string,\n { makeCounter, countsCollections = true }: EntityFacetCounterContractOptions,\n): void => {\n describe(`EntityFacetCounter contract: ${name}`, () => {\n const withCounter = async (\n use: (counter: EntityFacetCounter) => Promise<void>,\n seed: ContractWidget[] = seeded(),\n ) => {\n await use(await makeCounter(seed));\n };\n\n it('counts every value of every member, highest count first', async () => {\n await withCounter(async counter => {\n const counts = await run(\n counter.count<ContractWidget>([\n { member: 'name' },\n { member: 'size' },\n ]),\n );\n\n expect(counts).toEqual({\n name: [\n { value: 'Alpha', count: 2 },\n { value: 'Beta', count: 1 },\n ],\n size: [\n { value: 10, count: 2 },\n { value: 30, count: 1 },\n ],\n });\n });\n });\n\n it('counts each member under its own filtering only', async () => {\n await withCounter(async counter => {\n const counts = await run(\n counter.count<ContractWidget>([\n {\n member: 'name',\n filtering: [{ property: 'size', operator: 'eq', value: 10 }],\n },\n {\n member: 'size',\n filtering: [{ property: 'name', operator: 'eq', value: 'Alpha' }],\n },\n ]),\n );\n\n expect(counts).toEqual({\n name: [\n { value: 'Alpha', count: 1 },\n { value: 'Beta', count: 1 },\n ],\n size: [\n { value: 10, count: 1 },\n { value: 30, count: 1 },\n ],\n });\n });\n });\n\n it.runIf(countsCollections)(\n 'counts each element of an array member',\n async () => {\n await withCounter(async counter => {\n const counts = await run(\n counter.count<ContractWidget>([{ member: 'tags' }]),\n );\n\n expect(counts).toEqual({\n tags: [\n { value: 'green', count: 3 },\n { value: 'blue', count: 1 },\n { value: 'red', count: 1 },\n ],\n });\n });\n },\n );\n\n it('does not count a missing value', async () => {\n const unnamed = new ContractWidget();\n unnamed.id = 'w-9';\n await withCounter(\n async counter => {\n const counts = await run(\n counter.count<ContractWidget>([{ member: 'name' }]),\n );\n\n expect(counts).toEqual({ name: [{ value: 'Alpha', count: 1 }] });\n },\n [makeContractWidget('w-1', 'Alpha', 10), unnamed],\n );\n });\n\n it.runIf(countsCollections)('does not count an empty array', async () => {\n await withCounter(\n async counter => {\n const counts = await run(\n counter.count<ContractWidget>([{ member: 'tags' }]),\n );\n\n expect(counts).toEqual({ tags: [] });\n },\n [makeContractWidget('w-1', 'Alpha', 10)],\n );\n });\n\n it('answers every queried member, empty, from an empty store', async () => {\n await withCounter(async counter => {\n expect(\n await run(\n counter.count<ContractWidget>([\n { member: 'name' },\n { member: 'size' },\n ]),\n ),\n ).toEqual({ name: [], size: [] });\n }, []);\n });\n });\n};\n"],"names":["describe","expect","it","run","ContractWidget","makeContractWidget","seeded","describeEntityFacetCounterContract","name","makeCounter","countsCollections","withCounter","use","seed","counter","counts","count","member","toEqual","value","size","filtering","property","operator","runIf","tags","unnamed","id"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-repository.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/entity-repository.contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAY,KAAK,MAAM,EAAU,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAM7E;;;;GAIG;AACH,qBACa,cAAe,YAAW,MAAM;;IAO3C,IACI,EAAE,IAAI,QAAQ,CAEjB;IACD,IAAI,EAAE,CAAC,KAAK,EAAE,QAAQ,EAErB;IAED,IACI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAEjC;IAED,IACI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAEjC;IAED,IACI,IAAI,IAAI,SAAS,MAAM,EAAE,CAE5B;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAEhC;IAED,IACI,KAAK,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAExD;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,EAE5D;CACF;AAED,eAAO,MAAM,kBAAkB,GAC7B,IAAI,QAAQ,EACZ,MAAM,MAAM,EACZ,MAAM,MAAM,EACZ,OAAM,SAAS,MAAM,EAAO,KAC3B,cAOF,CAAC;AAEF,MAAM,WAAW,+BAA+B;IAC9C;;;OAGG;IACH,cAAc,CACZ,IAAI,EAAE,cAAc,EAAE,GACrB,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACjD;AAED;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAC/C,MAAM,MAAM,EACZ,oBAAoB,+BAA+B,KAClD,
|
|
1
|
+
{"version":3,"file":"entity-repository.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/entity-repository.contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAY,KAAK,MAAM,EAAU,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAM7E;;;;GAIG;AACH,qBACa,cAAe,YAAW,MAAM;;IAO3C,IACI,EAAE,IAAI,QAAQ,CAEjB;IACD,IAAI,EAAE,CAAC,KAAK,EAAE,QAAQ,EAErB;IAED,IACI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAEjC;IAED,IACI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAEjC;IAED,IACI,IAAI,IAAI,SAAS,MAAM,EAAE,CAE5B;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAEhC;IAED,IACI,KAAK,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAExD;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,EAE5D;CACF;AAED,eAAO,MAAM,kBAAkB,GAC7B,IAAI,QAAQ,EACZ,MAAM,MAAM,EACZ,MAAM,MAAM,EACZ,OAAM,SAAS,MAAM,EAAO,KAC3B,cAOF,CAAC;AAEF,MAAM,WAAW,+BAA+B;IAC9C;;;OAGG;IACH,cAAc,CACZ,IAAI,EAAE,cAAc,EAAE,GACrB,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACjD;AAED;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAC/C,MAAM,MAAM,EACZ,oBAAoB,+BAA+B,KAClD,IAuQF,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,qCAAqC,GAChD,MAAM,MAAM,EACZ,oBAAoB,+BAA+B,KAClD,IA0FF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,MAAM,EACZ,SAAS,+BAA+B,KACvC,IAGF,CAAC"}
|
|
@@ -227,6 +227,49 @@ export const makeContractWidget = (id, name, size, tags = [])=>{
|
|
|
227
227
|
]);
|
|
228
228
|
});
|
|
229
229
|
});
|
|
230
|
+
it('matches `like` and `nlike` with accents ignored on both sides', async ()=>{
|
|
231
|
+
await withRepository(async (repository)=>{
|
|
232
|
+
const like = await run(repository.load({
|
|
233
|
+
filtering: [
|
|
234
|
+
{
|
|
235
|
+
property: 'name',
|
|
236
|
+
operator: 'like',
|
|
237
|
+
value: 'cafe'
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}));
|
|
241
|
+
const accented = await run(repository.load({
|
|
242
|
+
filtering: [
|
|
243
|
+
{
|
|
244
|
+
property: 'name',
|
|
245
|
+
operator: 'like',
|
|
246
|
+
value: 'NIÑO'
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
}));
|
|
250
|
+
const nlike = await run(repository.load({
|
|
251
|
+
filtering: [
|
|
252
|
+
{
|
|
253
|
+
property: 'name',
|
|
254
|
+
operator: 'nlike',
|
|
255
|
+
value: 'café'
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
}));
|
|
259
|
+
expect(like.items.map((item)=>item.id)).toEqual([
|
|
260
|
+
'w-1'
|
|
261
|
+
]);
|
|
262
|
+
expect(accented.items.map((item)=>item.id)).toEqual([
|
|
263
|
+
'w-2'
|
|
264
|
+
]);
|
|
265
|
+
expect(nlike.items.map((item)=>item.id)).toEqual([
|
|
266
|
+
'w-2'
|
|
267
|
+
]);
|
|
268
|
+
}, [
|
|
269
|
+
makeContractWidget('w-1', 'Café de Olla', 10),
|
|
270
|
+
makeContractWidget('w-2', 'nino', 20)
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
230
273
|
it('matches `in` and `nin` against any element of an array member', async ()=>{
|
|
231
274
|
await withRepository(async (repository)=>{
|
|
232
275
|
const tagged = await run(repository.load({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/contracts/entity-repository.contract.ts"],"sourcesContent":["import type { EntityRepository } from '@entifix/business';\nimport { accessor, type Entity, entity, type EntityId } from '@entifix/core';\nimport { Exit } from 'effect';\nimport { describe, expect, it } from 'vitest';\n\nimport { run, runExit } from '../effect/run.js';\n\n/**\n * The entity every implementation of the contract is exercised against. It is\n * declared here rather than by each caller so the suite controls exactly which\n * fields exist.\n */\n@entity({ key: 'contract-widget' })\nexport class ContractWidget implements Entity {\n #id?: EntityId;\n #name?: string;\n #size?: number;\n #tags: readonly string[] = [];\n #title?: Readonly<Record<string, string>>;\n\n @accessor({ type: 'id' })\n get id(): EntityId {\n return this.#id;\n }\n set id(value: EntityId) {\n this.#id = value;\n }\n\n @accessor({ type: 'string' })\n get name(): string | undefined {\n return this.#name;\n }\n set name(value: string | undefined) {\n this.#name = value;\n }\n\n @accessor({ type: 'number' })\n get size(): number | undefined {\n return this.#size;\n }\n set size(value: number | undefined) {\n this.#size = value;\n }\n\n @accessor({ type: 'scalarCollection' })\n get tags(): readonly string[] {\n return this.#tags;\n }\n set tags(value: readonly string[]) {\n this.#tags = value;\n }\n\n @accessor({ type: 'localized' })\n get title(): Readonly<Record<string, string>> | undefined {\n return this.#title;\n }\n set title(value: Readonly<Record<string, string>> | undefined) {\n this.#title = value;\n }\n}\n\nexport const makeContractWidget = (\n id: EntityId,\n name: string,\n size: number,\n tags: readonly string[] = [],\n): ContractWidget => {\n const widget = new ContractWidget();\n widget.id = id;\n widget.name = name;\n widget.size = size;\n widget.tags = tags;\n return widget;\n};\n\nexport interface EntityRepositoryContractOptions {\n /**\n * Builds a repository over `seed`. Called fresh for each test, so no state\n * leaks between them.\n */\n makeRepository(\n seed: ContractWidget[],\n ): EntityRepository | Promise<EntityRepository>;\n}\n\n/**\n * The reads every {@link EntityRepository} must answer alike: `load` with its\n * paging, filtering and sorting, and `get`. A read-only adapter — one over\n * static files, say — runs this half alone, since it refuses `save` and\n * `delete` by design.\n */\nexport const describeEntityRepositoryReadContract = (\n name: string,\n { makeRepository }: EntityRepositoryContractOptions,\n): void => {\n describe(`EntityRepository read contract: ${name}`, () => {\n const seeded = () => [\n makeContractWidget('w-1', 'Alpha', 10),\n makeContractWidget('w-2', 'Beta', 20),\n makeContractWidget('w-3', 'Gamma', 30),\n ];\n\n const withRepository = async (\n use: (repository: EntityRepository) => Promise<void>,\n seed: ContractWidget[] = seeded(),\n ) => {\n await use(await makeRepository(seed));\n };\n\n it('keeps a record unchanged when a read instance is mutated without a save', async () => {\n await withRepository(async repository => {\n const read = await run(repository.get<ContractWidget>('w-1'));\n read.name = 'Mutated';\n\n const again = await run(repository.get<ContractWidget>('w-1'));\n expect(again.name).toBe('Alpha');\n\n const page = await run(repository.load<ContractWidget>({}));\n page.items[0]!.name = 'Mutated';\n const reloaded = await run(repository.load<ContractWidget>({}));\n expect(reloaded.items[0]!.name).toBe('Alpha');\n });\n });\n\n it('loads every stored entity with its total', async () => {\n await withRepository(async repository => {\n const page = await run(repository.load<ContractWidget>({}));\n\n expect(page.total).toBe(3);\n expect(page.items.map(item => item.name)).toEqual([\n 'Alpha',\n 'Beta',\n 'Gamma',\n ]);\n });\n });\n\n it('echoes the request it served on the page', async () => {\n await withRepository(async repository => {\n const request = { page: 1, pageSize: 2 };\n const page = await run(repository.load<ContractWidget>(request));\n\n expect(page.request).toEqual(request);\n });\n });\n\n it('pages with a 1-based page number', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({ page: 2, pageSize: 2 }),\n );\n\n expect(page.items).toHaveLength(1);\n expect(page.items[0]?.name).toBe('Gamma');\n // The total counts matches, not the page.\n expect(page.total).toBe(3);\n });\n });\n\n it('filters by an equality operator', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({\n filtering: [{ property: 'name', operator: 'eq', value: 'Beta' }],\n }),\n );\n\n expect(page.items).toHaveLength(1);\n expect(page.items[0]?.id).toBe('w-2');\n expect(page.total).toBe(1);\n });\n });\n\n it('filters by a range operator', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'size', operator: 'between', start: 15, end: 35 },\n ],\n }),\n );\n\n expect(page.items.map(item => item.id)).toEqual(['w-2', 'w-3']);\n });\n });\n\n it('matches `like` case-insensitively on a substring', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({\n filtering: [{ property: 'name', operator: 'like', value: 'et' }],\n }),\n );\n\n expect(page.items.map(item => item.id)).toEqual(['w-2']);\n });\n });\n\n it('matches `in` and `nin` against any element of an array member', async () => {\n await withRepository(\n async repository => {\n const tagged = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'tags', operator: 'in', values: ['blue', 'red'] },\n ],\n }),\n );\n expect(tagged.items.map(item => item.id)).toEqual(['w-1', 'w-3']);\n\n const untagged = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'tags', operator: 'nin', values: ['blue', 'red'] },\n ],\n }),\n );\n expect(untagged.items.map(item => item.id)).toEqual(['w-2']);\n },\n [\n makeContractWidget('w-1', 'Alpha', 10, ['blue', 'green']),\n makeContractWidget('w-2', 'Beta', 20, ['green']),\n makeContractWidget('w-3', 'Gamma', 30, ['red']),\n ],\n );\n });\n\n it('filters and sorts a localized member one locale at a time', async () => {\n const titled = (\n id: string,\n name: string,\n title: Record<string, string>,\n ) => {\n const widget = makeContractWidget(id, name, 0);\n widget.title = title;\n return widget;\n };\n await withRepository(\n async repository => {\n const found = await run(\n repository.load<ContractWidget>({\n filtering: [\n {\n property: 'title.es' as keyof ContractWidget,\n operator: 'like',\n value: 'SOPA',\n },\n ],\n }),\n );\n expect(found.items.map(item => item.id)).toEqual(['w-2']);\n\n const sorted = await run(\n repository.load<ContractWidget>({\n sorting: [\n {\n 0: {\n property: 'title.en' as keyof ContractWidget,\n type: 'asc',\n },\n },\n ],\n }),\n );\n expect(sorted.items.map(item => item.id)).toEqual([\n 'w-3',\n 'w-1',\n 'w-2',\n ]);\n expect(sorted.items[0]?.title).toEqual({\n en: 'Apple',\n es: 'Manzana',\n });\n },\n [\n titled('w-1', 'Alpha', { en: 'Bread', es: 'Pan' }),\n titled('w-2', 'Beta', { en: 'Soup', es: 'Sopa' }),\n titled('w-3', 'Gamma', { en: 'Apple', es: 'Manzana' }),\n ],\n );\n });\n\n it('sorts descending when asked to', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({\n sorting: [{ 0: { property: 'size', type: 'desc' } }],\n }),\n );\n\n expect(page.items.map(item => item.size)).toEqual([30, 20, 10]);\n });\n });\n\n it('reads a single entity by id', async () => {\n await withRepository(async repository => {\n const found = await run(repository.get<ContractWidget>('w-2'));\n\n expect(found.id).toBe('w-2');\n expect(found.name).toBe('Beta');\n });\n });\n\n it('fails rather than resolving undefined for an unknown id', async () => {\n await withRepository(async repository => {\n const exit = await runExit(repository.get<ContractWidget>('missing'));\n\n expect(Exit.isFailure(exit)).toBe(true);\n });\n });\n\n it('loads an empty page from an empty store', async () => {\n await withRepository(async repository => {\n const page = await run(repository.load<ContractWidget>({}));\n\n expect(page.items).toEqual([]);\n expect(page.total).toBe(0);\n }, []);\n });\n });\n};\n\n/** The writes every {@link EntityRepository} that accepts them must honour. */\nexport const describeEntityRepositoryWriteContract = (\n name: string,\n { makeRepository }: EntityRepositoryContractOptions,\n): void => {\n describe(`EntityRepository write contract: ${name}`, () => {\n const seeded = () => [\n makeContractWidget('w-1', 'Alpha', 10),\n makeContractWidget('w-2', 'Beta', 20),\n makeContractWidget('w-3', 'Gamma', 30),\n ];\n\n const withRepository = async (\n use: (repository: EntityRepository) => Promise<void>,\n seed: ContractWidget[] = seeded(),\n ) => {\n await use(await makeRepository(seed));\n };\n\n it('keeps a saved record unchanged when the saved instance is mutated afterwards', async () => {\n await withRepository(async repository => {\n const widget = makeContractWidget('w-9', 'Delta', 40);\n const returned = await run(repository.save(widget));\n\n widget.name = 'Mutated';\n returned.name = 'Mutated too';\n\n const stored = await run(repository.get<ContractWidget>('w-9'));\n expect(stored.name).toBe('Delta');\n });\n });\n\n it('mints an id when saving an entity that has none', async () => {\n await withRepository(async repository => {\n const created = await run(\n repository.save<ContractWidget>(\n makeContractWidget(undefined, 'Delta', 40),\n ),\n );\n\n expect(created.id).toBeDefined();\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.total).toBe(4);\n });\n });\n\n it('overwrites in place when saving an entity that already has an id', async () => {\n await withRepository(async repository => {\n await run(\n repository.save<ContractWidget>(\n makeContractWidget('w-2', 'Renamed', 25),\n ),\n );\n\n const found = await run(repository.get<ContractWidget>('w-2'));\n expect(found.name).toBe('Renamed');\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.total).toBe(3);\n });\n });\n\n it('deletes by id', async () => {\n await withRepository(async repository => {\n await run(repository.delete<ContractWidget>('w-1'));\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.items.map(item => item.id)).toEqual(['w-2', 'w-3']);\n });\n });\n\n it('deletes by entity', async () => {\n await withRepository(async repository => {\n await run(\n repository.delete<ContractWidget>(\n makeContractWidget('w-3', 'Gamma', 30),\n ),\n );\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.items.map(item => item.id)).toEqual(['w-1', 'w-2']);\n });\n });\n\n it('treats deleting an unknown id as a no-op', async () => {\n await withRepository(async repository => {\n await run(repository.delete<ContractWidget>('missing'));\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.total).toBe(3);\n });\n });\n });\n};\n\n/**\n * The behaviour every {@link EntityRepository} must exhibit, regardless of what\n * backs it: both halves.\n *\n * Run it against the in-memory fake *and* against each real adapter. That is\n * what stops a fake from drifting into a more forgiving version of the thing it\n * stands in for — the failure mode where a suite is green and production is\n * not. `entifix-ts-testing-integration` will run this same suite against real\n * infrastructure later.\n */\nexport const describeEntityRepositoryContract = (\n name: string,\n options: EntityRepositoryContractOptions,\n): void => {\n describeEntityRepositoryReadContract(name, options);\n describeEntityRepositoryWriteContract(name, options);\n};\n"],"names":["accessor","entity","Exit","describe","expect","it","run","runExit","key","type","ContractWidget","id","value","name","size","tags","title","makeContractWidget","widget","describeEntityRepositoryReadContract","makeRepository","seeded","withRepository","use","seed","repository","read","get","again","toBe","page","load","items","reloaded","total","map","item","toEqual","request","pageSize","toHaveLength","filtering","property","operator","start","end","tagged","values","untagged","titled","sorted","found","sorting","en","es","exit","isFailure","describeEntityRepositoryWriteContract","returned","save","stored","created","undefined","toBeDefined","delete","describeEntityRepositoryContract","options"],"mappings":";;;;AACA,SAASA,QAAQ,EAAeC,MAAM,QAAuB,gBAAgB;AAC7E,SAASC,IAAI,QAAQ,SAAS;AAC9B,SAASC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAS;AAE9C,SAASC,GAAG,EAAEC,OAAO,QAAQ,mBAAmB;OAO/CN,OAAO;IAAEO,KAAK;AAAkB,YAQ9BR,SAAS;IAAES,MAAM;AAAK,YAQtBT,SAAS;IAAES,MAAM;AAAS,YAQ1BT,SAAS;IAAES,MAAM;AAAS,YAQ1BT,SAAS;IAAES,MAAM;AAAmB,YAQpCT,SAAS;IAAES,MAAM;AAAY;;IAtC9B,2DACA,+DACA,+DACA,+DACA;AALK,IAAA,AAAMC,iBAAN,MAAMA;IAOX,IACIC,KAAe;QACjB,OAAO,gCAAA,IAAI,EAAC,KAAA;IACd;IACA,IAAIA,GAAGC,KAAe,EAAE;QACtB,gCAAA,IAAI,EAAC,KAAA,OAAMA;IACb;IAEA,IACIC,OAA2B;QAC7B,OAAO,gCAAA,IAAI,EAAC,OAAA;IACd;IACA,IAAIA,KAAKD,KAAyB,EAAE;QAClC,gCAAA,IAAI,EAAC,OAAA,SAAQA;IACf;IAEA,IACIE,OAA2B;QAC7B,OAAO,gCAAA,IAAI,EAAC,OAAA;IACd;IACA,IAAIA,KAAKF,KAAyB,EAAE;QAClC,gCAAA,IAAI,EAAC,OAAA,SAAQA;IACf;IAEA,IACIG,OAA0B;QAC5B,OAAO,gCAAA,IAAI,EAAC,OAAA;IACd;IACA,IAAIA,KAAKH,KAAwB,EAAE;QACjC,gCAAA,IAAI,EAAC,OAAA,SAAQA;IACf;IAEA,IACII,QAAsD;QACxD,OAAO,gCAAA,IAAI,EAAC,QAAA;IACd;IACA,IAAIA,MAAMJ,KAAmD,EAAE;QAC7D,gCAAA,IAAI,EAAC,QAAA,UAASA;IAChB;;QA5CA,4BAAA;;mBAAA,KAAA;;QACA,4BAAA;;mBAAA,KAAA;;QACA,4BAAA;;mBAAA,KAAA;;QACA,4BAAA;;mBAAA,KAAA;;QACA,4BAAA;;mBAAA,KAAA;;8CADA,OAAA,SAA2B,EAAE;;;AA0C/B;4EA9CaF;;;;QAQPC;;;;;QAQAE;;;;;QAQAC;;;;;QAQAC;;;;;QAQAC;;;;;;AAQN,OAAO,MAAMC,qBAAqB,CAChCN,IACAE,MACAC,MACAC,OAA0B,EAAE;IAE5B,MAAMG,SAAS,IAAIR;IACnBQ,OAAOP,EAAE,GAAGA;IACZO,OAAOL,IAAI,GAAGA;IACdK,OAAOJ,IAAI,GAAGA;IACdI,OAAOH,IAAI,GAAGA;IACd,OAAOG;AACT,EAAE;AAYF;;;;;CAKC,GACD,OAAO,MAAMC,uCAAuC,CAClDN,MACA,EAAEO,cAAc,EAAmC;IAEnDjB,SAAS,CAAC,gCAAgC,EAAEU,MAAM,EAAE;QAClD,MAAMQ,SAAS,IAAM;gBACnBJ,mBAAmB,OAAO,SAAS;gBACnCA,mBAAmB,OAAO,QAAQ;gBAClCA,mBAAmB,OAAO,SAAS;aACpC;QAED,MAAMK,iBAAiB,OACrBC,KACAC,OAAyBH,QAAQ;YAEjC,MAAME,IAAI,MAAMH,eAAeI;QACjC;QAEAnB,GAAG,2EAA2E;YAC5E,MAAMiB,eAAe,OAAMG;gBACzB,MAAMC,OAAO,MAAMpB,IAAImB,WAAWE,GAAG,CAAiB;gBACtDD,KAAKb,IAAI,GAAG;gBAEZ,MAAMe,QAAQ,MAAMtB,IAAImB,WAAWE,GAAG,CAAiB;gBACvDvB,OAAOwB,MAAMf,IAAI,EAAEgB,IAAI,CAAC;gBAExB,MAAMC,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBACxDD,KAAKE,KAAK,CAAC,EAAE,CAAEnB,IAAI,GAAG;gBACtB,MAAMoB,WAAW,MAAM3B,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBAC5D3B,OAAO6B,SAASD,KAAK,CAAC,EAAE,CAAEnB,IAAI,EAAEgB,IAAI,CAAC;YACvC;QACF;QAEAxB,GAAG,4CAA4C;YAC7C,MAAMiB,eAAe,OAAMG;gBACzB,MAAMK,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBAExD3B,OAAO0B,KAAKI,KAAK,EAAEL,IAAI,CAAC;gBACxBzB,OAAO0B,KAAKE,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKvB,IAAI,GAAGwB,OAAO,CAAC;oBAChD;oBACA;oBACA;iBACD;YACH;QACF;QAEAhC,GAAG,4CAA4C;YAC7C,MAAMiB,eAAe,OAAMG;gBACzB,MAAMa,UAAU;oBAAER,MAAM;oBAAGS,UAAU;gBAAE;gBACvC,MAAMT,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiBO;gBAEvDlC,OAAO0B,KAAKQ,OAAO,EAAED,OAAO,CAACC;YAC/B;QACF;QAEAjC,GAAG,oCAAoC;YACrC,MAAMiB,eAAe,OAAMG;oBAMlBK;gBALP,MAAMA,OAAO,MAAMxB,IACjBmB,WAAWM,IAAI,CAAiB;oBAAED,MAAM;oBAAGS,UAAU;gBAAE;gBAGzDnC,OAAO0B,KAAKE,KAAK,EAAEQ,YAAY,CAAC;gBAChCpC,QAAO0B,eAAAA,KAAKE,KAAK,CAAC,EAAE,qBAAbF,aAAejB,IAAI,EAAEgB,IAAI,CAAC;gBACjC,0CAA0C;gBAC1CzB,OAAO0B,KAAKI,KAAK,EAAEL,IAAI,CAAC;YAC1B;QACF;QAEAxB,GAAG,mCAAmC;YACpC,MAAMiB,eAAe,OAAMG;oBAQlBK;gBAPP,MAAMA,OAAO,MAAMxB,IACjBmB,WAAWM,IAAI,CAAiB;oBAC9BU,WAAW;wBAAC;4BAAEC,UAAU;4BAAQC,UAAU;4BAAM/B,OAAO;wBAAO;qBAAE;gBAClE;gBAGFR,OAAO0B,KAAKE,KAAK,EAAEQ,YAAY,CAAC;gBAChCpC,QAAO0B,eAAAA,KAAKE,KAAK,CAAC,EAAE,qBAAbF,aAAenB,EAAE,EAAEkB,IAAI,CAAC;gBAC/BzB,OAAO0B,KAAKI,KAAK,EAAEL,IAAI,CAAC;YAC1B;QACF;QAEAxB,GAAG,+BAA+B;YAChC,MAAMiB,eAAe,OAAMG;gBACzB,MAAMK,OAAO,MAAMxB,IACjBmB,WAAWM,IAAI,CAAiB;oBAC9BU,WAAW;wBACT;4BAAEC,UAAU;4BAAQC,UAAU;4BAAWC,OAAO;4BAAIC,KAAK;wBAAG;qBAC7D;gBACH;gBAGFzC,OAAO0B,KAAKE,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKzB,EAAE,GAAG0B,OAAO,CAAC;oBAAC;oBAAO;iBAAM;YAChE;QACF;QAEAhC,GAAG,oDAAoD;YACrD,MAAMiB,eAAe,OAAMG;gBACzB,MAAMK,OAAO,MAAMxB,IACjBmB,WAAWM,IAAI,CAAiB;oBAC9BU,WAAW;wBAAC;4BAAEC,UAAU;4BAAQC,UAAU;4BAAQ/B,OAAO;wBAAK;qBAAE;gBAClE;gBAGFR,OAAO0B,KAAKE,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKzB,EAAE,GAAG0B,OAAO,CAAC;oBAAC;iBAAM;YACzD;QACF;QAEAhC,GAAG,iEAAiE;YAClE,MAAMiB,eACJ,OAAMG;gBACJ,MAAMqB,SAAS,MAAMxC,IACnBmB,WAAWM,IAAI,CAAiB;oBAC9BU,WAAW;wBACT;4BAAEC,UAAU;4BAAQC,UAAU;4BAAMI,QAAQ;gCAAC;gCAAQ;6BAAM;wBAAC;qBAC7D;gBACH;gBAEF3C,OAAO0C,OAAOd,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKzB,EAAE,GAAG0B,OAAO,CAAC;oBAAC;oBAAO;iBAAM;gBAEhE,MAAMW,WAAW,MAAM1C,IACrBmB,WAAWM,IAAI,CAAiB;oBAC9BU,WAAW;wBACT;4BAAEC,UAAU;4BAAQC,UAAU;4BAAOI,QAAQ;gCAAC;gCAAQ;6BAAM;wBAAC;qBAC9D;gBACH;gBAEF3C,OAAO4C,SAAShB,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKzB,EAAE,GAAG0B,OAAO,CAAC;oBAAC;iBAAM;YAC7D,GACA;gBACEpB,mBAAmB,OAAO,SAAS,IAAI;oBAAC;oBAAQ;iBAAQ;gBACxDA,mBAAmB,OAAO,QAAQ,IAAI;oBAAC;iBAAQ;gBAC/CA,mBAAmB,OAAO,SAAS,IAAI;oBAAC;iBAAM;aAC/C;QAEL;QAEAZ,GAAG,6DAA6D;YAC9D,MAAM4C,SAAS,CACbtC,IACAE,MACAG;gBAEA,MAAME,SAASD,mBAAmBN,IAAIE,MAAM;gBAC5CK,OAAOF,KAAK,GAAGA;gBACf,OAAOE;YACT;YACA,MAAMI,eACJ,OAAMG;oBA+BGyB;gBA9BP,MAAMC,QAAQ,MAAM7C,IAClBmB,WAAWM,IAAI,CAAiB;oBAC9BU,WAAW;wBACT;4BACEC,UAAU;4BACVC,UAAU;4BACV/B,OAAO;wBACT;qBACD;gBACH;gBAEFR,OAAO+C,MAAMnB,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKzB,EAAE,GAAG0B,OAAO,CAAC;oBAAC;iBAAM;gBAExD,MAAMa,SAAS,MAAM5C,IACnBmB,WAAWM,IAAI,CAAiB;oBAC9BqB,SAAS;wBACP;4BACE,GAAG;gCACDV,UAAU;gCACVjC,MAAM;4BACR;wBACF;qBACD;gBACH;gBAEFL,OAAO8C,OAAOlB,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKzB,EAAE,GAAG0B,OAAO,CAAC;oBAChD;oBACA;oBACA;iBACD;gBACDjC,QAAO8C,iBAAAA,OAAOlB,KAAK,CAAC,EAAE,qBAAfkB,eAAiBlC,KAAK,EAAEqB,OAAO,CAAC;oBACrCgB,IAAI;oBACJC,IAAI;gBACN;YACF,GACA;gBACEL,OAAO,OAAO,SAAS;oBAAEI,IAAI;oBAASC,IAAI;gBAAM;gBAChDL,OAAO,OAAO,QAAQ;oBAAEI,IAAI;oBAAQC,IAAI;gBAAO;gBAC/CL,OAAO,OAAO,SAAS;oBAAEI,IAAI;oBAASC,IAAI;gBAAU;aACrD;QAEL;QAEAjD,GAAG,kCAAkC;YACnC,MAAMiB,eAAe,OAAMG;gBACzB,MAAMK,OAAO,MAAMxB,IACjBmB,WAAWM,IAAI,CAAiB;oBAC9BqB,SAAS;wBAAC;4BAAE,GAAG;gCAAEV,UAAU;gCAAQjC,MAAM;4BAAO;wBAAE;qBAAE;gBACtD;gBAGFL,OAAO0B,KAAKE,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKtB,IAAI,GAAGuB,OAAO,CAAC;oBAAC;oBAAI;oBAAI;iBAAG;YAChE;QACF;QAEAhC,GAAG,+BAA+B;YAChC,MAAMiB,eAAe,OAAMG;gBACzB,MAAM0B,QAAQ,MAAM7C,IAAImB,WAAWE,GAAG,CAAiB;gBAEvDvB,OAAO+C,MAAMxC,EAAE,EAAEkB,IAAI,CAAC;gBACtBzB,OAAO+C,MAAMtC,IAAI,EAAEgB,IAAI,CAAC;YAC1B;QACF;QAEAxB,GAAG,2DAA2D;YAC5D,MAAMiB,eAAe,OAAMG;gBACzB,MAAM8B,OAAO,MAAMhD,QAAQkB,WAAWE,GAAG,CAAiB;gBAE1DvB,OAAOF,KAAKsD,SAAS,CAACD,OAAO1B,IAAI,CAAC;YACpC;QACF;QAEAxB,GAAG,2CAA2C;YAC5C,MAAMiB,eAAe,OAAMG;gBACzB,MAAMK,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBAExD3B,OAAO0B,KAAKE,KAAK,EAAEK,OAAO,CAAC,EAAE;gBAC7BjC,OAAO0B,KAAKI,KAAK,EAAEL,IAAI,CAAC;YAC1B,GAAG,EAAE;QACP;IACF;AACF,EAAE;AAEF,6EAA6E,GAC7E,OAAO,MAAM4B,wCAAwC,CACnD5C,MACA,EAAEO,cAAc,EAAmC;IAEnDjB,SAAS,CAAC,iCAAiC,EAAEU,MAAM,EAAE;QACnD,MAAMQ,SAAS,IAAM;gBACnBJ,mBAAmB,OAAO,SAAS;gBACnCA,mBAAmB,OAAO,QAAQ;gBAClCA,mBAAmB,OAAO,SAAS;aACpC;QAED,MAAMK,iBAAiB,OACrBC,KACAC,OAAyBH,QAAQ;YAEjC,MAAME,IAAI,MAAMH,eAAeI;QACjC;QAEAnB,GAAG,gFAAgF;YACjF,MAAMiB,eAAe,OAAMG;gBACzB,MAAMP,SAASD,mBAAmB,OAAO,SAAS;gBAClD,MAAMyC,WAAW,MAAMpD,IAAImB,WAAWkC,IAAI,CAACzC;gBAE3CA,OAAOL,IAAI,GAAG;gBACd6C,SAAS7C,IAAI,GAAG;gBAEhB,MAAM+C,SAAS,MAAMtD,IAAImB,WAAWE,GAAG,CAAiB;gBACxDvB,OAAOwD,OAAO/C,IAAI,EAAEgB,IAAI,CAAC;YAC3B;QACF;QAEAxB,GAAG,mDAAmD;YACpD,MAAMiB,eAAe,OAAMG;gBACzB,MAAMoC,UAAU,MAAMvD,IACpBmB,WAAWkC,IAAI,CACb1C,mBAAmB6C,WAAW,SAAS;gBAI3C1D,OAAOyD,QAAQlD,EAAE,EAAEoD,WAAW;gBAE9B,MAAMjC,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBACxD3B,OAAO0B,KAAKI,KAAK,EAAEL,IAAI,CAAC;YAC1B;QACF;QAEAxB,GAAG,oEAAoE;YACrE,MAAMiB,eAAe,OAAMG;gBACzB,MAAMnB,IACJmB,WAAWkC,IAAI,CACb1C,mBAAmB,OAAO,WAAW;gBAIzC,MAAMkC,QAAQ,MAAM7C,IAAImB,WAAWE,GAAG,CAAiB;gBACvDvB,OAAO+C,MAAMtC,IAAI,EAAEgB,IAAI,CAAC;gBAExB,MAAMC,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBACxD3B,OAAO0B,KAAKI,KAAK,EAAEL,IAAI,CAAC;YAC1B;QACF;QAEAxB,GAAG,iBAAiB;YAClB,MAAMiB,eAAe,OAAMG;gBACzB,MAAMnB,IAAImB,WAAWuC,MAAM,CAAiB;gBAE5C,MAAMlC,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBACxD3B,OAAO0B,KAAKE,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKzB,EAAE,GAAG0B,OAAO,CAAC;oBAAC;oBAAO;iBAAM;YAChE;QACF;QAEAhC,GAAG,qBAAqB;YACtB,MAAMiB,eAAe,OAAMG;gBACzB,MAAMnB,IACJmB,WAAWuC,MAAM,CACf/C,mBAAmB,OAAO,SAAS;gBAIvC,MAAMa,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBACxD3B,OAAO0B,KAAKE,KAAK,CAACG,GAAG,CAACC,CAAAA,OAAQA,KAAKzB,EAAE,GAAG0B,OAAO,CAAC;oBAAC;oBAAO;iBAAM;YAChE;QACF;QAEAhC,GAAG,4CAA4C;YAC7C,MAAMiB,eAAe,OAAMG;gBACzB,MAAMnB,IAAImB,WAAWuC,MAAM,CAAiB;gBAE5C,MAAMlC,OAAO,MAAMxB,IAAImB,WAAWM,IAAI,CAAiB,CAAC;gBACxD3B,OAAO0B,KAAKI,KAAK,EAAEL,IAAI,CAAC;YAC1B;QACF;IACF;AACF,EAAE;AAEF;;;;;;;;;CASC,GACD,OAAO,MAAMoC,mCAAmC,CAC9CpD,MACAqD;IAEA/C,qCAAqCN,MAAMqD;IAC3CT,sCAAsC5C,MAAMqD;AAC9C,EAAE;SAtaF,AAAaxD,mBAAAA,cA8CZ"}
|
|
1
|
+
{"version":3,"sources":["../../src/contracts/entity-repository.contract.ts"],"sourcesContent":["import type { EntityRepository } from '@entifix/business';\nimport { accessor, type Entity, entity, type EntityId } from '@entifix/core';\nimport { Exit } from 'effect';\nimport { describe, expect, it } from 'vitest';\n\nimport { run, runExit } from '../effect/run.js';\n\n/**\n * The entity every implementation of the contract is exercised against. It is\n * declared here rather than by each caller so the suite controls exactly which\n * fields exist.\n */\n@entity({ key: 'contract-widget' })\nexport class ContractWidget implements Entity {\n #id?: EntityId;\n #name?: string;\n #size?: number;\n #tags: readonly string[] = [];\n #title?: Readonly<Record<string, string>>;\n\n @accessor({ type: 'id' })\n get id(): EntityId {\n return this.#id;\n }\n set id(value: EntityId) {\n this.#id = value;\n }\n\n @accessor({ type: 'string' })\n get name(): string | undefined {\n return this.#name;\n }\n set name(value: string | undefined) {\n this.#name = value;\n }\n\n @accessor({ type: 'number' })\n get size(): number | undefined {\n return this.#size;\n }\n set size(value: number | undefined) {\n this.#size = value;\n }\n\n @accessor({ type: 'scalarCollection' })\n get tags(): readonly string[] {\n return this.#tags;\n }\n set tags(value: readonly string[]) {\n this.#tags = value;\n }\n\n @accessor({ type: 'localized' })\n get title(): Readonly<Record<string, string>> | undefined {\n return this.#title;\n }\n set title(value: Readonly<Record<string, string>> | undefined) {\n this.#title = value;\n }\n}\n\nexport const makeContractWidget = (\n id: EntityId,\n name: string,\n size: number,\n tags: readonly string[] = [],\n): ContractWidget => {\n const widget = new ContractWidget();\n widget.id = id;\n widget.name = name;\n widget.size = size;\n widget.tags = tags;\n return widget;\n};\n\nexport interface EntityRepositoryContractOptions {\n /**\n * Builds a repository over `seed`. Called fresh for each test, so no state\n * leaks between them.\n */\n makeRepository(\n seed: ContractWidget[],\n ): EntityRepository | Promise<EntityRepository>;\n}\n\n/**\n * The reads every {@link EntityRepository} must answer alike: `load` with its\n * paging, filtering and sorting, and `get`. A read-only adapter — one over\n * static files, say — runs this half alone, since it refuses `save` and\n * `delete` by design.\n */\nexport const describeEntityRepositoryReadContract = (\n name: string,\n { makeRepository }: EntityRepositoryContractOptions,\n): void => {\n describe(`EntityRepository read contract: ${name}`, () => {\n const seeded = () => [\n makeContractWidget('w-1', 'Alpha', 10),\n makeContractWidget('w-2', 'Beta', 20),\n makeContractWidget('w-3', 'Gamma', 30),\n ];\n\n const withRepository = async (\n use: (repository: EntityRepository) => Promise<void>,\n seed: ContractWidget[] = seeded(),\n ) => {\n await use(await makeRepository(seed));\n };\n\n it('keeps a record unchanged when a read instance is mutated without a save', async () => {\n await withRepository(async repository => {\n const read = await run(repository.get<ContractWidget>('w-1'));\n read.name = 'Mutated';\n\n const again = await run(repository.get<ContractWidget>('w-1'));\n expect(again.name).toBe('Alpha');\n\n const page = await run(repository.load<ContractWidget>({}));\n page.items[0]!.name = 'Mutated';\n const reloaded = await run(repository.load<ContractWidget>({}));\n expect(reloaded.items[0]!.name).toBe('Alpha');\n });\n });\n\n it('loads every stored entity with its total', async () => {\n await withRepository(async repository => {\n const page = await run(repository.load<ContractWidget>({}));\n\n expect(page.total).toBe(3);\n expect(page.items.map(item => item.name)).toEqual([\n 'Alpha',\n 'Beta',\n 'Gamma',\n ]);\n });\n });\n\n it('echoes the request it served on the page', async () => {\n await withRepository(async repository => {\n const request = { page: 1, pageSize: 2 };\n const page = await run(repository.load<ContractWidget>(request));\n\n expect(page.request).toEqual(request);\n });\n });\n\n it('pages with a 1-based page number', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({ page: 2, pageSize: 2 }),\n );\n\n expect(page.items).toHaveLength(1);\n expect(page.items[0]?.name).toBe('Gamma');\n // The total counts matches, not the page.\n expect(page.total).toBe(3);\n });\n });\n\n it('filters by an equality operator', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({\n filtering: [{ property: 'name', operator: 'eq', value: 'Beta' }],\n }),\n );\n\n expect(page.items).toHaveLength(1);\n expect(page.items[0]?.id).toBe('w-2');\n expect(page.total).toBe(1);\n });\n });\n\n it('filters by a range operator', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'size', operator: 'between', start: 15, end: 35 },\n ],\n }),\n );\n\n expect(page.items.map(item => item.id)).toEqual(['w-2', 'w-3']);\n });\n });\n\n it('matches `like` case-insensitively on a substring', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({\n filtering: [{ property: 'name', operator: 'like', value: 'et' }],\n }),\n );\n\n expect(page.items.map(item => item.id)).toEqual(['w-2']);\n });\n });\n\n it('matches `like` and `nlike` with accents ignored on both sides', async () => {\n await withRepository(\n async repository => {\n const like = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'name', operator: 'like', value: 'cafe' },\n ],\n }),\n );\n const accented = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'name', operator: 'like', value: 'NIÑO' },\n ],\n }),\n );\n const nlike = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'name', operator: 'nlike', value: 'café' },\n ],\n }),\n );\n\n expect(like.items.map(item => item.id)).toEqual(['w-1']);\n expect(accented.items.map(item => item.id)).toEqual(['w-2']);\n expect(nlike.items.map(item => item.id)).toEqual(['w-2']);\n },\n [\n makeContractWidget('w-1', 'Café de Olla', 10),\n makeContractWidget('w-2', 'nino', 20),\n ],\n );\n });\n\n it('matches `in` and `nin` against any element of an array member', async () => {\n await withRepository(\n async repository => {\n const tagged = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'tags', operator: 'in', values: ['blue', 'red'] },\n ],\n }),\n );\n expect(tagged.items.map(item => item.id)).toEqual(['w-1', 'w-3']);\n\n const untagged = await run(\n repository.load<ContractWidget>({\n filtering: [\n { property: 'tags', operator: 'nin', values: ['blue', 'red'] },\n ],\n }),\n );\n expect(untagged.items.map(item => item.id)).toEqual(['w-2']);\n },\n [\n makeContractWidget('w-1', 'Alpha', 10, ['blue', 'green']),\n makeContractWidget('w-2', 'Beta', 20, ['green']),\n makeContractWidget('w-3', 'Gamma', 30, ['red']),\n ],\n );\n });\n\n it('filters and sorts a localized member one locale at a time', async () => {\n const titled = (\n id: string,\n name: string,\n title: Record<string, string>,\n ) => {\n const widget = makeContractWidget(id, name, 0);\n widget.title = title;\n return widget;\n };\n await withRepository(\n async repository => {\n const found = await run(\n repository.load<ContractWidget>({\n filtering: [\n {\n property: 'title.es' as keyof ContractWidget,\n operator: 'like',\n value: 'SOPA',\n },\n ],\n }),\n );\n expect(found.items.map(item => item.id)).toEqual(['w-2']);\n\n const sorted = await run(\n repository.load<ContractWidget>({\n sorting: [\n {\n 0: {\n property: 'title.en' as keyof ContractWidget,\n type: 'asc',\n },\n },\n ],\n }),\n );\n expect(sorted.items.map(item => item.id)).toEqual([\n 'w-3',\n 'w-1',\n 'w-2',\n ]);\n expect(sorted.items[0]?.title).toEqual({\n en: 'Apple',\n es: 'Manzana',\n });\n },\n [\n titled('w-1', 'Alpha', { en: 'Bread', es: 'Pan' }),\n titled('w-2', 'Beta', { en: 'Soup', es: 'Sopa' }),\n titled('w-3', 'Gamma', { en: 'Apple', es: 'Manzana' }),\n ],\n );\n });\n\n it('sorts descending when asked to', async () => {\n await withRepository(async repository => {\n const page = await run(\n repository.load<ContractWidget>({\n sorting: [{ 0: { property: 'size', type: 'desc' } }],\n }),\n );\n\n expect(page.items.map(item => item.size)).toEqual([30, 20, 10]);\n });\n });\n\n it('reads a single entity by id', async () => {\n await withRepository(async repository => {\n const found = await run(repository.get<ContractWidget>('w-2'));\n\n expect(found.id).toBe('w-2');\n expect(found.name).toBe('Beta');\n });\n });\n\n it('fails rather than resolving undefined for an unknown id', async () => {\n await withRepository(async repository => {\n const exit = await runExit(repository.get<ContractWidget>('missing'));\n\n expect(Exit.isFailure(exit)).toBe(true);\n });\n });\n\n it('loads an empty page from an empty store', async () => {\n await withRepository(async repository => {\n const page = await run(repository.load<ContractWidget>({}));\n\n expect(page.items).toEqual([]);\n expect(page.total).toBe(0);\n }, []);\n });\n });\n};\n\n/** The writes every {@link EntityRepository} that accepts them must honour. */\nexport const describeEntityRepositoryWriteContract = (\n name: string,\n { makeRepository }: EntityRepositoryContractOptions,\n): void => {\n describe(`EntityRepository write contract: ${name}`, () => {\n const seeded = () => [\n makeContractWidget('w-1', 'Alpha', 10),\n makeContractWidget('w-2', 'Beta', 20),\n makeContractWidget('w-3', 'Gamma', 30),\n ];\n\n const withRepository = async (\n use: (repository: EntityRepository) => Promise<void>,\n seed: ContractWidget[] = seeded(),\n ) => {\n await use(await makeRepository(seed));\n };\n\n it('keeps a saved record unchanged when the saved instance is mutated afterwards', async () => {\n await withRepository(async repository => {\n const widget = makeContractWidget('w-9', 'Delta', 40);\n const returned = await run(repository.save(widget));\n\n widget.name = 'Mutated';\n returned.name = 'Mutated too';\n\n const stored = await run(repository.get<ContractWidget>('w-9'));\n expect(stored.name).toBe('Delta');\n });\n });\n\n it('mints an id when saving an entity that has none', async () => {\n await withRepository(async repository => {\n const created = await run(\n repository.save<ContractWidget>(\n makeContractWidget(undefined, 'Delta', 40),\n ),\n );\n\n expect(created.id).toBeDefined();\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.total).toBe(4);\n });\n });\n\n it('overwrites in place when saving an entity that already has an id', async () => {\n await withRepository(async repository => {\n await run(\n repository.save<ContractWidget>(\n makeContractWidget('w-2', 'Renamed', 25),\n ),\n );\n\n const found = await run(repository.get<ContractWidget>('w-2'));\n expect(found.name).toBe('Renamed');\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.total).toBe(3);\n });\n });\n\n it('deletes by id', async () => {\n await withRepository(async repository => {\n await run(repository.delete<ContractWidget>('w-1'));\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.items.map(item => item.id)).toEqual(['w-2', 'w-3']);\n });\n });\n\n it('deletes by entity', async () => {\n await withRepository(async repository => {\n await run(\n repository.delete<ContractWidget>(\n makeContractWidget('w-3', 'Gamma', 30),\n ),\n );\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.items.map(item => item.id)).toEqual(['w-1', 'w-2']);\n });\n });\n\n it('treats deleting an unknown id as a no-op', async () => {\n await withRepository(async repository => {\n await run(repository.delete<ContractWidget>('missing'));\n\n const page = await run(repository.load<ContractWidget>({}));\n expect(page.total).toBe(3);\n });\n });\n });\n};\n\n/**\n * The behaviour every {@link EntityRepository} must exhibit, regardless of what\n * backs it: both halves.\n *\n * Run it against the in-memory fake *and* against each real adapter. That is\n * what stops a fake from drifting into a more forgiving version of the thing it\n * stands in for — the failure mode where a suite is green and production is\n * not. `entifix-ts-testing-integration` will run this same suite against real\n * infrastructure later.\n */\nexport const describeEntityRepositoryContract = (\n name: string,\n options: EntityRepositoryContractOptions,\n): void => {\n describeEntityRepositoryReadContract(name, options);\n describeEntityRepositoryWriteContract(name, options);\n};\n"],"names":["accessor","entity","Exit","describe","expect","it","run","runExit","key","type","ContractWidget","id","value","name","size","tags","title","makeContractWidget","widget","describeEntityRepositoryReadContract","makeRepository","seeded","withRepository","use","seed","repository","read","get","again","toBe","page","load","items","reloaded","total","map","item","toEqual","request","pageSize","toHaveLength","filtering","property","operator","start","end","like","accented","nlike","tagged","values","untagged","titled","sorted","found","sorting","en","es","exit","isFailure","describeEntityRepositoryWriteContract","returned","save","stored","created","undefined","toBeDefined","delete","describeEntityRepositoryContract","options"],"mappings":"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"}
|
|
@@ -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;AAChD,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"AAAA,cAAc,oCAAoC,CAAC;AACnD,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';\nexport * from './unit-of-work.contract.js';\n"],"names":[],"mappings":"AAAA,cAAc,kCAAkC;AAChD,cAAc,2BAA2B;AACzC,cAAc,kCAAkC;AAChD,cAAc,6BAA6B"}
|
|
1
|
+
{"version":3,"sources":["../../src/contracts/index.ts"],"sourcesContent":["export * from './entity-facet-counter.contract.js';\nexport * 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,qCAAqC;AACnD,cAAc,kCAAkC;AAChD,cAAc,2BAA2B;AACzC,cAAc,kCAAkC;AAChD,cAAc,6BAA6B"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EntityFacetCounter } from '@entifix/business';
|
|
2
|
+
import { type EntifixError, type Entity } from '@entifix/core';
|
|
3
|
+
export interface InMemoryEntityFacetCounter extends EntityFacetCounter {
|
|
4
|
+
/**
|
|
5
|
+
* Makes the next count fail with `error`, so a caller's degraded path is
|
|
6
|
+
* reachable without reaching for a mocking library.
|
|
7
|
+
*/
|
|
8
|
+
failNext(error: EntifixError): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* An {@link EntityFacetCounter} over records held in memory, counted by the
|
|
12
|
+
* same rules as every other in-memory query.
|
|
13
|
+
*
|
|
14
|
+
* `source` is either the records themselves or anything holding them — an
|
|
15
|
+
* `InMemoryEntityRepository` — read on every count, so a spec that saves
|
|
16
|
+
* through the repository sees the counts move.
|
|
17
|
+
*/
|
|
18
|
+
export declare const makeInMemoryFacetCounter: (source: readonly Entity[] | {
|
|
19
|
+
readonly items: readonly Entity[];
|
|
20
|
+
}) => InMemoryEntityFacetCounter;
|
|
21
|
+
//# sourceMappingURL=entity-facet-counter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-facet-counter.d.ts","sourceRoot":"","sources":["../../src/doubles/entity-facet-counter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,MAAM,EAEZ,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,0BAA2B,SAAQ,kBAAkB;IACpE;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,SAAS,MAAM,EAAE,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,KAChE,0BAsBF,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { countFacets } from '@entifix/core';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* An {@link EntityFacetCounter} over records held in memory, counted by the
|
|
5
|
+
* same rules as every other in-memory query.
|
|
6
|
+
*
|
|
7
|
+
* `source` is either the records themselves or anything holding them — an
|
|
8
|
+
* `InMemoryEntityRepository` — read on every count, so a spec that saves
|
|
9
|
+
* through the repository sees the counts move.
|
|
10
|
+
*/ export const makeInMemoryFacetCounter = (source)=>{
|
|
11
|
+
let pendingFailure;
|
|
12
|
+
const records = ()=>'items' in source ? source.items : source;
|
|
13
|
+
return {
|
|
14
|
+
count: (queries)=>Effect.suspend(()=>{
|
|
15
|
+
if (pendingFailure !== undefined) {
|
|
16
|
+
const error = pendingFailure;
|
|
17
|
+
pendingFailure = undefined;
|
|
18
|
+
return Effect.fail(error);
|
|
19
|
+
}
|
|
20
|
+
return Effect.succeed(countFacets(records(), queries));
|
|
21
|
+
}),
|
|
22
|
+
failNext: (error)=>{
|
|
23
|
+
pendingFailure = error;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=entity-facet-counter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/doubles/entity-facet-counter.ts"],"sourcesContent":["import type { EntityFacetCounter } from '@entifix/business';\nimport {\n countFacets,\n type EntifixError,\n type Entity,\n type EntityFacetQuery,\n} from '@entifix/core';\nimport { Effect } from 'effect';\n\nexport interface InMemoryEntityFacetCounter extends EntityFacetCounter {\n /**\n * Makes the next count fail with `error`, so a caller's degraded path is\n * reachable without reaching for a mocking library.\n */\n failNext(error: EntifixError): void;\n}\n\n/**\n * An {@link EntityFacetCounter} over records held in memory, counted by the\n * same rules as every other in-memory query.\n *\n * `source` is either the records themselves or anything holding them — an\n * `InMemoryEntityRepository` — read on every count, so a spec that saves\n * through the repository sees the counts move.\n */\nexport const makeInMemoryFacetCounter = (\n source: readonly Entity[] | { readonly items: readonly Entity[] },\n): InMemoryEntityFacetCounter => {\n let pendingFailure: EntifixError | undefined;\n const records = () => ('items' in source ? source.items : source);\n\n return {\n count: <TEntity extends Entity>(\n queries: readonly EntityFacetQuery<TEntity>[],\n ) =>\n Effect.suspend(() => {\n if (pendingFailure !== undefined) {\n const error = pendingFailure;\n pendingFailure = undefined;\n return Effect.fail(error);\n }\n return Effect.succeed(\n countFacets(records() as readonly TEntity[], queries),\n );\n }),\n failNext: error => {\n pendingFailure = error;\n },\n };\n};\n"],"names":["countFacets","Effect","makeInMemoryFacetCounter","source","pendingFailure","records","items","count","queries","suspend","undefined","error","fail","succeed","failNext"],"mappings":"AACA,SACEA,WAAW,QAIN,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAUhC;;;;;;;CAOC,GACD,OAAO,MAAMC,2BAA2B,CACtCC;IAEA,IAAIC;IACJ,MAAMC,UAAU,IAAO,WAAWF,SAASA,OAAOG,KAAK,GAAGH;IAE1D,OAAO;QACLI,OAAO,CACLC,UAEAP,OAAOQ,OAAO,CAAC;gBACb,IAAIL,mBAAmBM,WAAW;oBAChC,MAAMC,QAAQP;oBACdA,iBAAiBM;oBACjB,OAAOT,OAAOW,IAAI,CAACD;gBACrB;gBACA,OAAOV,OAAOY,OAAO,CACnBb,YAAYK,WAAiCG;YAEjD;QACFM,UAAUH,CAAAA;YACRP,iBAAiBO;QACnB;IACF;AACF,EAAE"}
|
|
@@ -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;
|
|
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;AA0RxC,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,WA0mBF,CAAC"}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Only the surface the adapter uses is implemented; anything else is a
|
|
10
10
|
* deliberate `TypeError` rather than a silent no-op.
|
|
11
11
|
*/ /** The subset of a Mongo query document the adapter produces. */ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
12
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
12
13
|
const isPlainObject = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
14
|
const compare = (left, right)=>{
|
|
14
15
|
if (left == null && right == null) return 0;
|
|
@@ -168,6 +169,80 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
168
169
|
// has a line for `a`, not include it because some other line differs.
|
|
169
170
|
return isNegativeCondition(condition) ? candidates.every((value)=>matchesCondition(value, condition)) : candidates.some((value)=>matchesCondition(value, condition));
|
|
170
171
|
});
|
|
172
|
+
/** A `$field` reference to a top-level field, the only one the stages take. */ const fieldOf = (reference, stage)=>{
|
|
173
|
+
if (typeof reference !== 'string' || !reference.startsWith('$') || reference.includes('.')) {
|
|
174
|
+
throw new TypeError(`fake mongo: ${stage} takes a top-level "$field", got ${JSON.stringify(reference)}`);
|
|
175
|
+
}
|
|
176
|
+
return reference.slice(1);
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* `$unwind` without `preserveNullAndEmptyArrays`: an array becomes one
|
|
180
|
+
* document per element, a missing, `null` or empty one drops the document, and
|
|
181
|
+
* any other value passes through as it is.
|
|
182
|
+
*/ const unwind = (docs, spec)=>{
|
|
183
|
+
const options = isPlainObject(spec) ? spec : {
|
|
184
|
+
path: spec
|
|
185
|
+
};
|
|
186
|
+
if (options['preserveNullAndEmptyArrays'] === true) {
|
|
187
|
+
throw new TypeError('fake mongo: $unwind cannot preserve nulls');
|
|
188
|
+
}
|
|
189
|
+
const field = fieldOf(options['path'], '$unwind');
|
|
190
|
+
return docs.flatMap((doc)=>{
|
|
191
|
+
const value = doc[field];
|
|
192
|
+
if (value == null) return [];
|
|
193
|
+
if (!Array.isArray(value)) return [
|
|
194
|
+
doc
|
|
195
|
+
];
|
|
196
|
+
return value.map((element)=>_extends({}, doc, {
|
|
197
|
+
[field]: element
|
|
198
|
+
}));
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
/** `$group` by one field, counting with `{ $sum: 1 }`; a missing key is `null`. */ const group = (docs, spec)=>{
|
|
202
|
+
const _ref = isPlainObject(spec) ? spec : {}, { _id: key } = _ref, accumulators = _object_without_properties_loose(_ref, [
|
|
203
|
+
"_id"
|
|
204
|
+
]);
|
|
205
|
+
const field = fieldOf(key, '$group');
|
|
206
|
+
const entries = Object.entries(accumulators);
|
|
207
|
+
if (entries.length !== 1 || !isPlainObject(entries[0][1]) || entries[0][1]['$sum'] !== 1) {
|
|
208
|
+
throw new TypeError('fake mongo: $group supports one accumulator, { $sum: 1 }');
|
|
209
|
+
}
|
|
210
|
+
const [counted] = entries[0];
|
|
211
|
+
const tally = new Map();
|
|
212
|
+
for (const doc of docs){
|
|
213
|
+
var _doc_field, _tally_get;
|
|
214
|
+
const value = (_doc_field = doc[field]) != null ? _doc_field : null;
|
|
215
|
+
tally.set(value, ((_tally_get = tally.get(value)) != null ? _tally_get : 0) + 1);
|
|
216
|
+
}
|
|
217
|
+
return Array.from(tally, ([id, count])=>({
|
|
218
|
+
_id: id,
|
|
219
|
+
[counted]: count
|
|
220
|
+
}));
|
|
221
|
+
};
|
|
222
|
+
const runPipeline = (docs, pipeline)=>pipeline.reduce((current, stage)=>{
|
|
223
|
+
const entries = Object.entries(stage);
|
|
224
|
+
if (entries.length !== 1) {
|
|
225
|
+
throw new TypeError('fake mongo: a stage has exactly one operator');
|
|
226
|
+
}
|
|
227
|
+
const [operator, spec] = entries[0];
|
|
228
|
+
switch(operator){
|
|
229
|
+
case '$match':
|
|
230
|
+
return current.filter((doc)=>matches(doc, spec));
|
|
231
|
+
case '$unwind':
|
|
232
|
+
return unwind(current, spec);
|
|
233
|
+
case '$group':
|
|
234
|
+
return group(current, spec);
|
|
235
|
+
case '$facet':
|
|
236
|
+
return [
|
|
237
|
+
Object.fromEntries(Object.entries(spec).map(([output, branch])=>[
|
|
238
|
+
output,
|
|
239
|
+
runPipeline(current, branch)
|
|
240
|
+
]))
|
|
241
|
+
];
|
|
242
|
+
default:
|
|
243
|
+
throw new TypeError(`fake mongo: unsupported stage "${operator}"`);
|
|
244
|
+
}
|
|
245
|
+
}, docs);
|
|
171
246
|
/**
|
|
172
247
|
* The error a real server raises when a write violates a unique index.
|
|
173
248
|
*
|
|
@@ -298,6 +373,14 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
298
373
|
};
|
|
299
374
|
return cursor;
|
|
300
375
|
},
|
|
376
|
+
/**
|
|
377
|
+
* The four stages a facet count sends — `$facet`, `$match`, `$unwind` and
|
|
378
|
+
* a `$group` counting with `{ $sum: 1 }` — and nothing else: an unknown
|
|
379
|
+
* stage, or a stage in a shape not listed here, is a `TypeError` rather
|
|
380
|
+
* than a pass-through, for the reason `$expr` above gives.
|
|
381
|
+
*/ aggregate: (pipeline)=>({
|
|
382
|
+
toArray: ()=>record(name, 'aggregate', ()=>runPipeline(documentsOf(name), pipeline))
|
|
383
|
+
}),
|
|
301
384
|
findOne: (query, options)=>record(name, 'findOne', ()=>{
|
|
302
385
|
const found = documentsOf(name).find((doc)=>matches(doc, query));
|
|
303
386
|
return found === undefined ? null : project(found, options);
|
|
@@ -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 /**\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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"}
|
|
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/** A `$field` reference to a top-level field, the only one the stages take. */\nconst fieldOf = (reference: unknown, stage: string): string => {\n if (\n typeof reference !== 'string' ||\n !reference.startsWith('$') ||\n reference.includes('.')\n ) {\n throw new TypeError(\n `fake mongo: ${stage} takes a top-level \"$field\", got ${JSON.stringify(reference)}`,\n );\n }\n return reference.slice(1);\n};\n\n/**\n * `$unwind` without `preserveNullAndEmptyArrays`: an array becomes one\n * document per element, a missing, `null` or empty one drops the document, and\n * any other value passes through as it is.\n */\nconst unwind = (docs: Document[], spec: unknown): Document[] => {\n const options = isPlainObject(spec) ? spec : { path: spec };\n if (options['preserveNullAndEmptyArrays'] === true) {\n throw new TypeError('fake mongo: $unwind cannot preserve nulls');\n }\n const field = fieldOf(options['path'], '$unwind');\n return docs.flatMap(doc => {\n const value = doc[field];\n if (value == null) return [];\n if (!Array.isArray(value)) return [doc];\n return value.map(element => ({ ...doc, [field]: element }));\n });\n};\n\n/** `$group` by one field, counting with `{ $sum: 1 }`; a missing key is `null`. */\nconst group = (docs: Document[], spec: unknown): Document[] => {\n const { _id: key, ...accumulators } = isPlainObject(spec) ? spec : {};\n const field = fieldOf(key, '$group');\n const entries = Object.entries(accumulators);\n if (\n entries.length !== 1 ||\n !isPlainObject(entries[0][1]) ||\n entries[0][1]['$sum'] !== 1\n ) {\n throw new TypeError(\n 'fake mongo: $group supports one accumulator, { $sum: 1 }',\n );\n }\n const [counted] = entries[0];\n const tally = new Map<unknown, number>();\n for (const doc of docs) {\n const value = doc[field] ?? null;\n tally.set(value, (tally.get(value) ?? 0) + 1);\n }\n return Array.from(tally, ([id, count]) => ({ _id: id, [counted]: count }));\n};\n\nconst runPipeline = (docs: Document[], pipeline: Document[]): Document[] =>\n pipeline.reduce<Document[]>((current, stage) => {\n const entries = Object.entries(stage);\n if (entries.length !== 1) {\n throw new TypeError('fake mongo: a stage has exactly one operator');\n }\n const [operator, spec] = entries[0];\n switch (operator) {\n case '$match':\n return current.filter(doc => matches(doc, spec as QueryDocument));\n case '$unwind':\n return unwind(current, spec);\n case '$group':\n return group(current, spec);\n case '$facet':\n return [\n Object.fromEntries(\n Object.entries(spec as Record<string, Document[]>).map(\n ([output, branch]) => [output, runPipeline(current, branch)],\n ),\n ),\n ];\n default:\n throw new TypeError(`fake mongo: unsupported stage \"${operator}\"`);\n }\n }, docs);\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 /**\n * The four stages a facet count sends — `$facet`, `$match`, `$unwind` and\n * a `$group` counting with `{ $sum: 1 }` — and nothing else: an unknown\n * stage, or a stage in a shape not listed here, is a `TypeError` rather\n * than a pass-through, for the reason `$expr` above gives.\n */\n aggregate: (pipeline: Document[]) => ({\n toArray: () =>\n record(name, 'aggregate', () =>\n runPipeline(documentsOf(name), pipeline),\n ),\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","fieldOf","reference","stage","JSON","stringify","unwind","docs","spec","options","path","group","_id","key","accumulators","counted","tally","Map","set","get","from","id","count","runPipeline","pipeline","reduce","filter","fromEntries","output","branch","FakeDuplicateKeyError","Error","collection","code","name","makeFakeMongoDb","seed","collections","d","operations","failure","failuresByOperation","onceFailures","failureFor","op","once","delete","documentsOf","uniqueIndexes","uniqueIndexesOf","assertUnique","candidate","ignored","fields","clash","find","record","produce","rejection","Promise","reject","resolve","error","project","copy","projection","sortSpec","skipCount","limitCount","Infinity","cursor","sort","skip","limit","toArray","matched","sorted","reduceRight","items","direction","aggregate","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","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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"}
|
|
@@ -7,8 +7,8 @@ export interface EntityRestHandlerOptions<TEntity extends Entity> {
|
|
|
7
7
|
data?: TEntity[];
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* MSW handlers for the five REST operations the entity adapters issue,
|
|
11
|
-
* the real entifix envelope.
|
|
10
|
+
* MSW handlers for the five REST operations the entity adapters issue, and the
|
|
11
|
+
* `$facets` count beside them, speaking the real entifix envelope.
|
|
12
12
|
*
|
|
13
13
|
* Responses are built with `makeEntityEnvelope`/`makeEntityPageEnvelope` — the
|
|
14
14
|
* production serializer — so a fixture cannot drift from the wire format the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-handlers.d.ts","sourceRoot":"","sources":["../../src/http/entity-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"entity-handlers.d.ts","sourceRoot":"","sources":["../../src/http/entity-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EAKd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE9D,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM;IAC9D,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,MAAM,EACvD,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,mBAAwB,wBAAwB,CAAC,OAAO,CAAC,KACxD,cAAc,EAgGhB,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,GAAI,OAAO,SAAS,MAAM,EACtD,KAAK,MAAM,EACX,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,UAAU,OAAO,KAChB,cAGA,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,KAChD,cAC+D,CAAC;AAEnE,eAAO,MAAM,cAAc,GACzB,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,KAChD,cAC+D,CAAC;AAEnE,8DAA8D;AAC9D,eAAO,MAAM,4BAA4B,GACvC,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,EACjD,OAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAA0C,KACrE,cAAkE,CAAC;AAEtE,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,GAC7B,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,KAChD,cAIA,CAAC;AAEJ,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,GAClC,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,KAChD,cAA+D,CAAC;AAEnE,YAAY,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC;AAEzC,oEAAoE;AACpE,YAAY,EAAE,QAAQ,EAAE,CAAC"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { makeEntityEnvelope, makeEntityPageEnvelope } from '@entifix/core';
|
|
1
|
+
import { countFacets, makeEntityEnvelope, makeEntityFacetsEnvelope, makeEntityPageEnvelope, parseFacetQueryParams } from '@entifix/core';
|
|
2
2
|
import { http, HttpResponse } from 'msw';
|
|
3
3
|
/**
|
|
4
|
-
* MSW handlers for the five REST operations the entity adapters issue,
|
|
5
|
-
* the real entifix envelope.
|
|
4
|
+
* MSW handlers for the five REST operations the entity adapters issue, and the
|
|
5
|
+
* `$facets` count beside them, speaking the real entifix envelope.
|
|
6
6
|
*
|
|
7
7
|
* Responses are built with `makeEntityEnvelope`/`makeEntityPageEnvelope` — the
|
|
8
8
|
* production serializer — so a fixture cannot drift from the wire format the
|
|
@@ -27,6 +27,22 @@ import { http, HttpResponse } from 'msw';
|
|
|
27
27
|
}
|
|
28
28
|
}));
|
|
29
29
|
}),
|
|
30
|
+
// Registered before `/:id`, which would otherwise take `$facets` for an id.
|
|
31
|
+
// The query goes through the real protocol parser and the in-memory
|
|
32
|
+
// counter, so a refused member is a `400` here as it is at the service.
|
|
33
|
+
http.get(`${baseUrl}/$facets`, ({ request })=>{
|
|
34
|
+
try {
|
|
35
|
+
const queries = parseFacetQueryParams(entityConstructor, new URL(request.url).searchParams);
|
|
36
|
+
return HttpResponse.json(makeEntityFacetsEnvelope(entityConstructor, countFacets(rows, queries)));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return HttpResponse.json({
|
|
39
|
+
message: error.message,
|
|
40
|
+
code: 'invalidQuery'
|
|
41
|
+
}, {
|
|
42
|
+
status: 400
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
30
46
|
http.get(`${baseUrl}/:id`, ({ params })=>{
|
|
31
47
|
const found = findById(String(params['id']));
|
|
32
48
|
return found === undefined ? new HttpResponse(null, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http/entity-handlers.ts"],"sourcesContent":["import {\n type Entity,\n type EntityConstructor,\n type EntityId,\n makeEntityEnvelope,\n makeEntityPageEnvelope,\n} from '@entifix/core';\nimport { http, HttpResponse, type RequestHandler } from 'msw';\n\nexport interface EntityRestHandlerOptions<TEntity extends Entity> {\n /** The collection endpoint, e.g. `http://service/api/product`. */\n baseUrl: string;\n /** The rows the fake service serves. Mutated in place by writes. */\n data?: TEntity[];\n}\n\n/**\n * MSW handlers for the five REST operations the entity adapters issue, speaking\n * the real entifix envelope.\n *\n * Responses are built with `makeEntityEnvelope`/`makeEntityPageEnvelope` — the\n * production serializer — so a fixture cannot drift from the wire format the\n * adapters parse. Hand-written JSON literals in specs are exactly how that\n * drift starts.\n */\nexport const entityRestHandlers = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n { baseUrl, data = [] }: EntityRestHandlerOptions<TEntity>,\n): RequestHandler[] => {\n const rows = data;\n\n const findById = (id: string): TEntity | undefined =>\n rows.find(row => String(row.id) === id);\n\n return [\n http.get(baseUrl, ({ request }) => {\n const url = new URL(request.url);\n const page = Number(url.searchParams.get('page') ?? '1');\n const pageSize = Number(url.searchParams.get('pageSize') ?? '10');\n const start = (page - 1) * pageSize;\n\n return HttpResponse.json(\n makeEntityPageEnvelope(entityConstructor, {\n items: rows.slice(start, start + pageSize),\n total: rows.length,\n request: { page, pageSize },\n }),\n );\n }),\n\n http.get(`${baseUrl}/:id`, ({ params }) => {\n const found = findById(String(params['id']));\n return found === undefined\n ? new HttpResponse(null, { status: 404 })\n : HttpResponse.json(makeEntityEnvelope(entityConstructor, found));\n }),\n\n http.post(baseUrl, async ({ request }) => {\n const body = (await request.json()) as { data: Record<string, unknown> };\n // The service is what mints an id on create; echoing the request back\n // unchanged would hide that the adapter must read the response.\n const created = Object.assign(new entityConstructor(), body.data, {\n id: body.data['id'] ?? `generated-${rows.length + 1}`,\n }) as TEntity;\n rows.push(created);\n return HttpResponse.json(makeEntityEnvelope(entityConstructor, created), {\n status: 201,\n });\n }),\n\n http.put(`${baseUrl}/:id`, async ({ params, request }) => {\n const body = (await request.json()) as { data: Record<string, unknown> };\n const id = String(params['id']);\n const index = rows.findIndex(row => String(row.id) === id);\n const updated = Object.assign(\n new entityConstructor(),\n body.data,\n ) as TEntity;\n if (index === -1) {\n rows.push(updated);\n } else {\n rows[index] = updated;\n }\n return HttpResponse.json(makeEntityEnvelope(entityConstructor, updated));\n }),\n\n // A delete answers with the removed entity's envelope rather than a bare\n // `204`: the shared fetch client always parses the response as JSON, so an\n // empty body would fail that parse — which is what the services do too.\n http.delete(`${baseUrl}/:id`, ({ params }) => {\n const id = String(params['id']);\n const index = rows.findIndex(row => String(row.id) === id);\n const [removed] = index === -1 ? [] : rows.splice(index, 1);\n return HttpResponse.json(\n makeEntityEnvelope(\n entityConstructor,\n removed ?? (new entityConstructor() as TEntity),\n ),\n );\n }),\n ];\n};\n\n/** A single entity response, for specs that need one endpoint only. */\nexport const respondWithEntity = <TEntity extends Entity>(\n url: string,\n entityConstructor: EntityConstructor<TEntity>,\n instance: TEntity,\n): RequestHandler =>\n http.get(url, () =>\n HttpResponse.json(makeEntityEnvelope(entityConstructor, instance)),\n );\n\n/**\n * The failure handlers. Adapters map transport and shape failures onto\n * `EntifixError`s, and those branches are only reachable from here.\n */\nexport const respondWith500 = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](url, () => new HttpResponse(null, { status: 500 }));\n\nexport const respondWith404 = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](url, () => new HttpResponse(null, { status: 404 }));\n\n/** Answers 200 with a body that is not an envelope at all. */\nexport const respondWithMalformedEnvelope = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n body: Record<string, unknown> = { id: 'raw', name: 'not an envelope' },\n): RequestHandler => http[method](url, () => HttpResponse.json(body));\n\n/** Answers 200 with a body that is not JSON. */\nexport const respondWithNonJson = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](\n url,\n () => new HttpResponse('<html>nope</html>', { status: 200 }),\n );\n\n/** Fails the request at the transport level, as a dropped connection would. */\nexport const respondWithNetworkError = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler => http[method](url, () => HttpResponse.error());\n\nexport type { RequestHandler } from 'msw';\nexport { http, HttpResponse } from 'msw';\n\n/** Re-exported so `EntityId` stays available to handler authors. */\nexport type { EntityId };\n"],"names":["makeEntityEnvelope","makeEntityPageEnvelope","http","HttpResponse","entityRestHandlers","entityConstructor","baseUrl","data","rows","findById","id","find","row","String","get","request","url","URL","page","Number","searchParams","pageSize","start","json","items","slice","total","length","params","found","undefined","status","post","body","created","Object","assign","push","put","index","findIndex","updated","delete","removed","splice","respondWithEntity","instance","respondWith500","method","respondWith404","respondWithMalformedEnvelope","name","respondWithNonJson","respondWithNetworkError","error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/http/entity-handlers.ts"],"sourcesContent":["import {\n countFacets,\n type Entity,\n type EntityConstructor,\n type EntityId,\n makeEntityEnvelope,\n makeEntityFacetsEnvelope,\n makeEntityPageEnvelope,\n parseFacetQueryParams,\n} from '@entifix/core';\nimport { http, HttpResponse, type RequestHandler } from 'msw';\n\nexport interface EntityRestHandlerOptions<TEntity extends Entity> {\n /** The collection endpoint, e.g. `http://service/api/product`. */\n baseUrl: string;\n /** The rows the fake service serves. Mutated in place by writes. */\n data?: TEntity[];\n}\n\n/**\n * MSW handlers for the five REST operations the entity adapters issue, and the\n * `$facets` count beside them, speaking the real entifix envelope.\n *\n * Responses are built with `makeEntityEnvelope`/`makeEntityPageEnvelope` — the\n * production serializer — so a fixture cannot drift from the wire format the\n * adapters parse. Hand-written JSON literals in specs are exactly how that\n * drift starts.\n */\nexport const entityRestHandlers = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n { baseUrl, data = [] }: EntityRestHandlerOptions<TEntity>,\n): RequestHandler[] => {\n const rows = data;\n\n const findById = (id: string): TEntity | undefined =>\n rows.find(row => String(row.id) === id);\n\n return [\n http.get(baseUrl, ({ request }) => {\n const url = new URL(request.url);\n const page = Number(url.searchParams.get('page') ?? '1');\n const pageSize = Number(url.searchParams.get('pageSize') ?? '10');\n const start = (page - 1) * pageSize;\n\n return HttpResponse.json(\n makeEntityPageEnvelope(entityConstructor, {\n items: rows.slice(start, start + pageSize),\n total: rows.length,\n request: { page, pageSize },\n }),\n );\n }),\n\n // Registered before `/:id`, which would otherwise take `$facets` for an id.\n // The query goes through the real protocol parser and the in-memory\n // counter, so a refused member is a `400` here as it is at the service.\n http.get(`${baseUrl}/$facets`, ({ request }) => {\n try {\n const queries = parseFacetQueryParams(\n entityConstructor,\n new URL(request.url).searchParams,\n );\n return HttpResponse.json(\n makeEntityFacetsEnvelope(\n entityConstructor,\n countFacets(rows, queries),\n ),\n );\n } catch (error) {\n return HttpResponse.json(\n { message: (error as Error).message, code: 'invalidQuery' },\n { status: 400 },\n );\n }\n }),\n\n http.get(`${baseUrl}/:id`, ({ params }) => {\n const found = findById(String(params['id']));\n return found === undefined\n ? new HttpResponse(null, { status: 404 })\n : HttpResponse.json(makeEntityEnvelope(entityConstructor, found));\n }),\n\n http.post(baseUrl, async ({ request }) => {\n const body = (await request.json()) as { data: Record<string, unknown> };\n // The service is what mints an id on create; echoing the request back\n // unchanged would hide that the adapter must read the response.\n const created = Object.assign(new entityConstructor(), body.data, {\n id: body.data['id'] ?? `generated-${rows.length + 1}`,\n }) as TEntity;\n rows.push(created);\n return HttpResponse.json(makeEntityEnvelope(entityConstructor, created), {\n status: 201,\n });\n }),\n\n http.put(`${baseUrl}/:id`, async ({ params, request }) => {\n const body = (await request.json()) as { data: Record<string, unknown> };\n const id = String(params['id']);\n const index = rows.findIndex(row => String(row.id) === id);\n const updated = Object.assign(\n new entityConstructor(),\n body.data,\n ) as TEntity;\n if (index === -1) {\n rows.push(updated);\n } else {\n rows[index] = updated;\n }\n return HttpResponse.json(makeEntityEnvelope(entityConstructor, updated));\n }),\n\n // A delete answers with the removed entity's envelope rather than a bare\n // `204`: the shared fetch client always parses the response as JSON, so an\n // empty body would fail that parse — which is what the services do too.\n http.delete(`${baseUrl}/:id`, ({ params }) => {\n const id = String(params['id']);\n const index = rows.findIndex(row => String(row.id) === id);\n const [removed] = index === -1 ? [] : rows.splice(index, 1);\n return HttpResponse.json(\n makeEntityEnvelope(\n entityConstructor,\n removed ?? (new entityConstructor() as TEntity),\n ),\n );\n }),\n ];\n};\n\n/** A single entity response, for specs that need one endpoint only. */\nexport const respondWithEntity = <TEntity extends Entity>(\n url: string,\n entityConstructor: EntityConstructor<TEntity>,\n instance: TEntity,\n): RequestHandler =>\n http.get(url, () =>\n HttpResponse.json(makeEntityEnvelope(entityConstructor, instance)),\n );\n\n/**\n * The failure handlers. Adapters map transport and shape failures onto\n * `EntifixError`s, and those branches are only reachable from here.\n */\nexport const respondWith500 = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](url, () => new HttpResponse(null, { status: 500 }));\n\nexport const respondWith404 = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](url, () => new HttpResponse(null, { status: 404 }));\n\n/** Answers 200 with a body that is not an envelope at all. */\nexport const respondWithMalformedEnvelope = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n body: Record<string, unknown> = { id: 'raw', name: 'not an envelope' },\n): RequestHandler => http[method](url, () => HttpResponse.json(body));\n\n/** Answers 200 with a body that is not JSON. */\nexport const respondWithNonJson = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](\n url,\n () => new HttpResponse('<html>nope</html>', { status: 200 }),\n );\n\n/** Fails the request at the transport level, as a dropped connection would. */\nexport const respondWithNetworkError = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler => http[method](url, () => HttpResponse.error());\n\nexport type { RequestHandler } from 'msw';\nexport { http, HttpResponse } from 'msw';\n\n/** Re-exported so `EntityId` stays available to handler authors. */\nexport type { EntityId };\n"],"names":["countFacets","makeEntityEnvelope","makeEntityFacetsEnvelope","makeEntityPageEnvelope","parseFacetQueryParams","http","HttpResponse","entityRestHandlers","entityConstructor","baseUrl","data","rows","findById","id","find","row","String","get","request","url","URL","page","Number","searchParams","pageSize","start","json","items","slice","total","length","queries","error","message","code","status","params","found","undefined","post","body","created","Object","assign","push","put","index","findIndex","updated","delete","removed","splice","respondWithEntity","instance","respondWith500","method","respondWith404","respondWithMalformedEnvelope","name","respondWithNonJson","respondWithNetworkError"],"mappings":"AAAA,SACEA,WAAW,EAIXC,kBAAkB,EAClBC,wBAAwB,EACxBC,sBAAsB,EACtBC,qBAAqB,QAChB,gBAAgB;AACvB,SAASC,IAAI,EAAEC,YAAY,QAA6B,MAAM;AAS9D;;;;;;;;CAQC,GACD,OAAO,MAAMC,qBAAqB,CAChCC,mBACA,EAAEC,OAAO,EAAEC,OAAO,EAAE,EAAqC;IAEzD,MAAMC,OAAOD;IAEb,MAAME,WAAW,CAACC,KAChBF,KAAKG,IAAI,CAACC,CAAAA,MAAOC,OAAOD,IAAIF,EAAE,MAAMA;IAEtC,OAAO;QACLR,KAAKY,GAAG,CAACR,SAAS,CAAC,EAAES,OAAO,EAAE;gBAERC,uBACIA;YAFxB,MAAMA,MAAM,IAAIC,IAAIF,QAAQC,GAAG;YAC/B,MAAME,OAAOC,QAAOH,wBAAAA,IAAII,YAAY,CAACN,GAAG,CAAC,mBAArBE,wBAAgC;YACpD,MAAMK,WAAWF,QAAOH,yBAAAA,IAAII,YAAY,CAACN,GAAG,CAAC,uBAArBE,yBAAoC;YAC5D,MAAMM,QAAQ,AAACJ,CAAAA,OAAO,CAAA,IAAKG;YAE3B,OAAOlB,aAAaoB,IAAI,CACtBvB,uBAAuBK,mBAAmB;gBACxCmB,OAAOhB,KAAKiB,KAAK,CAACH,OAAOA,QAAQD;gBACjCK,OAAOlB,KAAKmB,MAAM;gBAClBZ,SAAS;oBAAEG;oBAAMG;gBAAS;YAC5B;QAEJ;QAEA,4EAA4E;QAC5E,oEAAoE;QACpE,wEAAwE;QACxEnB,KAAKY,GAAG,CAAC,GAAGR,QAAQ,QAAQ,CAAC,EAAE,CAAC,EAAES,OAAO,EAAE;YACzC,IAAI;gBACF,MAAMa,UAAU3B,sBACdI,mBACA,IAAIY,IAAIF,QAAQC,GAAG,EAAEI,YAAY;gBAEnC,OAAOjB,aAAaoB,IAAI,CACtBxB,yBACEM,mBACAR,YAAYW,MAAMoB;YAGxB,EAAE,OAAOC,OAAO;gBACd,OAAO1B,aAAaoB,IAAI,CACtB;oBAAEO,SAAS,AAACD,MAAgBC,OAAO;oBAAEC,MAAM;gBAAe,GAC1D;oBAAEC,QAAQ;gBAAI;YAElB;QACF;QAEA9B,KAAKY,GAAG,CAAC,GAAGR,QAAQ,IAAI,CAAC,EAAE,CAAC,EAAE2B,MAAM,EAAE;YACpC,MAAMC,QAAQzB,SAASI,OAAOoB,MAAM,CAAC,KAAK;YAC1C,OAAOC,UAAUC,YACb,IAAIhC,aAAa,MAAM;gBAAE6B,QAAQ;YAAI,KACrC7B,aAAaoB,IAAI,CAACzB,mBAAmBO,mBAAmB6B;QAC9D;QAEAhC,KAAKkC,IAAI,CAAC9B,SAAS,OAAO,EAAES,OAAO,EAAE;gBAK7BsB;YAJN,MAAMA,OAAQ,MAAMtB,QAAQQ,IAAI;YAChC,sEAAsE;YACtE,gEAAgE;YAChE,MAAMe,UAAUC,OAAOC,MAAM,CAAC,IAAInC,qBAAqBgC,KAAK9B,IAAI,EAAE;gBAChEG,EAAE,GAAE2B,gBAAAA,KAAK9B,IAAI,CAAC,KAAK,YAAf8B,gBAAmB,CAAC,UAAU,EAAE7B,KAAKmB,MAAM,GAAG,GAAG;YACvD;YACAnB,KAAKiC,IAAI,CAACH;YACV,OAAOnC,aAAaoB,IAAI,CAACzB,mBAAmBO,mBAAmBiC,UAAU;gBACvEN,QAAQ;YACV;QACF;QAEA9B,KAAKwC,GAAG,CAAC,GAAGpC,QAAQ,IAAI,CAAC,EAAE,OAAO,EAAE2B,MAAM,EAAElB,OAAO,EAAE;YACnD,MAAMsB,OAAQ,MAAMtB,QAAQQ,IAAI;YAChC,MAAMb,KAAKG,OAAOoB,MAAM,CAAC,KAAK;YAC9B,MAAMU,QAAQnC,KAAKoC,SAAS,CAAChC,CAAAA,MAAOC,OAAOD,IAAIF,EAAE,MAAMA;YACvD,MAAMmC,UAAUN,OAAOC,MAAM,CAC3B,IAAInC,qBACJgC,KAAK9B,IAAI;YAEX,IAAIoC,UAAU,CAAC,GAAG;gBAChBnC,KAAKiC,IAAI,CAACI;YACZ,OAAO;gBACLrC,IAAI,CAACmC,MAAM,GAAGE;YAChB;YACA,OAAO1C,aAAaoB,IAAI,CAACzB,mBAAmBO,mBAAmBwC;QACjE;QAEA,yEAAyE;QACzE,2EAA2E;QAC3E,wEAAwE;QACxE3C,KAAK4C,MAAM,CAAC,GAAGxC,QAAQ,IAAI,CAAC,EAAE,CAAC,EAAE2B,MAAM,EAAE;YACvC,MAAMvB,KAAKG,OAAOoB,MAAM,CAAC,KAAK;YAC9B,MAAMU,QAAQnC,KAAKoC,SAAS,CAAChC,CAAAA,MAAOC,OAAOD,IAAIF,EAAE,MAAMA;YACvD,MAAM,CAACqC,QAAQ,GAAGJ,UAAU,CAAC,IAAI,EAAE,GAAGnC,KAAKwC,MAAM,CAACL,OAAO;YACzD,OAAOxC,aAAaoB,IAAI,CACtBzB,mBACEO,mBACA0C,kBAAAA,UAAY,IAAI1C;QAGtB;KACD;AACH,EAAE;AAEF,qEAAqE,GACrE,OAAO,MAAM4C,oBAAoB,CAC/BjC,KACAX,mBACA6C,WAEAhD,KAAKY,GAAG,CAACE,KAAK,IACZb,aAAaoB,IAAI,CAACzB,mBAAmBO,mBAAmB6C,YACxD;AAEJ;;;CAGC,GACD,OAAO,MAAMC,iBAAiB,CAC5BnC,KACAoC,SAA4C,KAAK,GAEjDlD,IAAI,CAACkD,OAAO,CAACpC,KAAK,IAAM,IAAIb,aAAa,MAAM;YAAE6B,QAAQ;QAAI,IAAI;AAEnE,OAAO,MAAMqB,iBAAiB,CAC5BrC,KACAoC,SAA4C,KAAK,GAEjDlD,IAAI,CAACkD,OAAO,CAACpC,KAAK,IAAM,IAAIb,aAAa,MAAM;YAAE6B,QAAQ;QAAI,IAAI;AAEnE,4DAA4D,GAC5D,OAAO,MAAMsB,+BAA+B,CAC1CtC,KACAoC,SAA4C,KAAK,EACjDf,OAAgC;IAAE3B,IAAI;IAAO6C,MAAM;AAAkB,CAAC,GACnDrD,IAAI,CAACkD,OAAO,CAACpC,KAAK,IAAMb,aAAaoB,IAAI,CAACc,OAAO;AAEtE,8CAA8C,GAC9C,OAAO,MAAMmB,qBAAqB,CAChCxC,KACAoC,SAA4C,KAAK,GAEjDlD,IAAI,CAACkD,OAAO,CACVpC,KACA,IAAM,IAAIb,aAAa,qBAAqB;YAAE6B,QAAQ;QAAI,IAC1D;AAEJ,6EAA6E,GAC7E,OAAO,MAAMyB,0BAA0B,CACrCzC,KACAoC,SAA4C,KAAK,GAC9BlD,IAAI,CAACkD,OAAO,CAACpC,KAAK,IAAMb,aAAa0B,KAAK,IAAI;AAGnE,SAAS3B,IAAI,EAAEC,YAAY,QAAQ,MAAM"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './builders/entity-builder.js';
|
|
2
2
|
export * from './doubles/configuration.js';
|
|
3
|
+
export * from './doubles/entity-facet-counter.js';
|
|
3
4
|
export * from './doubles/entity-repository.js';
|
|
4
5
|
export * from './doubles/link-resolver.js';
|
|
5
6
|
export * from './doubles/transaction-ports.js';
|
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,2BAA2B,CAAC;AAC1C,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,mCAAmC,CAAC;AAClD,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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './builders/entity-builder.js';
|
|
2
2
|
export * from './doubles/configuration.js';
|
|
3
|
+
export * from './doubles/entity-facet-counter.js';
|
|
3
4
|
export * from './doubles/entity-repository.js';
|
|
4
5
|
export * from './doubles/link-resolver.js';
|
|
5
6
|
export * from './doubles/transaction-ports.js';
|
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 './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"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './builders/entity-builder.js';\nexport * from './doubles/configuration.js';\nexport * from './doubles/entity-facet-counter.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,oCAAoC;AAClD,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.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Test doubles for entifix: in-memory ports, driver fakes for Mongo, Redis, RabbitMQ and SQL, MSW handlers and port contract suites.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@swc/helpers": "0.5.23",
|
|
75
|
-
"@entifix/
|
|
76
|
-
"@entifix/
|
|
77
|
-
"@entifix/
|
|
75
|
+
"@entifix/transactions": "0.9.0",
|
|
76
|
+
"@entifix/core": "0.9.0",
|
|
77
|
+
"@entifix/business": "0.9.0"
|
|
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.
|
|
85
|
+
"@entifix/react-controls": "0.9.0"
|
|
86
86
|
},
|
|
87
87
|
"peerDependenciesMeta": {
|
|
88
88
|
"@entifix/react-controls": {
|