@entifix/testing-unit 0.1.4 → 0.3.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/dist/contracts/entity-repository.contract.d.ts +2 -0
- package/dist/contracts/entity-repository.contract.d.ts.map +1 -1
- package/dist/contracts/entity-repository.contract.js +75 -3
- package/dist/contracts/entity-repository.contract.js.map +1 -1
- package/dist/contracts/transaction-ports.contract.d.ts.map +1 -1
- package/dist/contracts/transaction-ports.contract.js +10 -4
- package/dist/contracts/transaction-ports.contract.js.map +1 -1
- package/dist/doubles/entity-repository.d.ts +3 -2
- package/dist/doubles/entity-repository.d.ts.map +1 -1
- package/dist/doubles/entity-repository.js +12 -101
- package/dist/doubles/entity-repository.js.map +1 -1
- package/dist/drivers/fake-mongo.d.ts.map +1 -1
- package/dist/drivers/fake-mongo.js +3 -1
- package/dist/drivers/fake-mongo.js.map +1 -1
- package/dist/drivers/fake-sql.d.ts.map +1 -1
- package/dist/drivers/fake-sql.js +7 -5
- package/dist/drivers/fake-sql.js.map +1 -1
- package/dist/effect/run.d.ts.map +1 -1
- package/dist/effect/run.js +8 -7
- package/dist/effect/run.js.map +1 -1
- package/package.json +7 -18
|
@@ -15,6 +15,8 @@ export declare class ContractWidget implements Entity {
|
|
|
15
15
|
set size(value: number | undefined);
|
|
16
16
|
get tags(): readonly string[];
|
|
17
17
|
set tags(value: readonly string[]);
|
|
18
|
+
get title(): Readonly<Record<string, string>> | undefined;
|
|
19
|
+
set title(value: Readonly<Record<string, string>> | undefined);
|
|
18
20
|
}
|
|
19
21
|
export declare const makeContractWidget: (id: EntityId, name: string, size: number, tags?: readonly string[]) => ContractWidget;
|
|
20
22
|
export interface EntityRepositoryContractOptions {
|
|
@@ -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;;
|
|
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,IA6OF,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,qCAAqC,GAChD,MAAM,MAAM,EACZ,oBAAoB,+BAA+B,KAClD,IA8FF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,MAAM,EACZ,SAAS,+BAA+B,KACvC,IAGF,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { _ as _class_private_field_loose_base } from "@swc/helpers/_/_class_private_field_loose_base";
|
|
2
2
|
import { _ as _class_private_field_loose_key } from "@swc/helpers/_/_class_private_field_loose_key";
|
|
3
3
|
import { _ as _apply_decs_2203_r } from "@swc/helpers/_/_apply_decs_2203_r";
|
|
4
|
-
var _dec, _initClass, _dec1, _dec2, _dec3, _dec4, _initProto;
|
|
4
|
+
var _dec, _initClass, _dec1, _dec2, _dec3, _dec4, _dec5, _initProto;
|
|
5
5
|
import { accessor, entity } from '@entifix/core';
|
|
6
6
|
import { Exit } from 'effect';
|
|
7
7
|
import { describe, expect, it } from 'vitest';
|
|
@@ -16,9 +16,11 @@ _dec = entity({
|
|
|
16
16
|
type: 'number'
|
|
17
17
|
}), _dec4 = accessor({
|
|
18
18
|
type: 'scalarCollection'
|
|
19
|
+
}), _dec5 = accessor({
|
|
20
|
+
type: 'localized'
|
|
19
21
|
});
|
|
20
22
|
let _ContractWidget;
|
|
21
|
-
var _id = /*#__PURE__*/ _class_private_field_loose_key("_id"), _name = /*#__PURE__*/ _class_private_field_loose_key("_name"), _size = /*#__PURE__*/ _class_private_field_loose_key("_size"), _tags = /*#__PURE__*/ _class_private_field_loose_key("_tags");
|
|
23
|
+
var _id = /*#__PURE__*/ _class_private_field_loose_key("_id"), _name = /*#__PURE__*/ _class_private_field_loose_key("_name"), _size = /*#__PURE__*/ _class_private_field_loose_key("_size"), _tags = /*#__PURE__*/ _class_private_field_loose_key("_tags"), _title = /*#__PURE__*/ _class_private_field_loose_key("_title");
|
|
22
24
|
let ContractWidget = class ContractWidget {
|
|
23
25
|
get id() {
|
|
24
26
|
return _class_private_field_loose_base(this, _id)[_id];
|
|
@@ -44,6 +46,12 @@ let ContractWidget = class ContractWidget {
|
|
|
44
46
|
set tags(value) {
|
|
45
47
|
_class_private_field_loose_base(this, _tags)[_tags] = value;
|
|
46
48
|
}
|
|
49
|
+
get title() {
|
|
50
|
+
return _class_private_field_loose_base(this, _title)[_title];
|
|
51
|
+
}
|
|
52
|
+
set title(value) {
|
|
53
|
+
_class_private_field_loose_base(this, _title)[_title] = value;
|
|
54
|
+
}
|
|
47
55
|
constructor(){
|
|
48
56
|
Object.defineProperty(this, _id, {
|
|
49
57
|
writable: true,
|
|
@@ -61,7 +69,12 @@ let ContractWidget = class ContractWidget {
|
|
|
61
69
|
writable: true,
|
|
62
70
|
value: void 0
|
|
63
71
|
});
|
|
64
|
-
|
|
72
|
+
Object.defineProperty(this, _title, {
|
|
73
|
+
writable: true,
|
|
74
|
+
value: void 0
|
|
75
|
+
});
|
|
76
|
+
_class_private_field_loose_base(this, _tags)[_tags] = [];
|
|
77
|
+
_initProto(this);
|
|
65
78
|
}
|
|
66
79
|
};
|
|
67
80
|
({ e: [_initProto], c: [_ContractWidget, _initClass] } = _apply_decs_2203_r(ContractWidget, [
|
|
@@ -84,6 +97,11 @@ let ContractWidget = class ContractWidget {
|
|
|
84
97
|
_dec4,
|
|
85
98
|
3,
|
|
86
99
|
"tags"
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
_dec5,
|
|
103
|
+
3,
|
|
104
|
+
"title"
|
|
87
105
|
]
|
|
88
106
|
], [
|
|
89
107
|
_dec
|
|
@@ -255,6 +273,60 @@ export const makeContractWidget = (id, name, size, tags = [])=>{
|
|
|
255
273
|
])
|
|
256
274
|
]);
|
|
257
275
|
});
|
|
276
|
+
it('filters and sorts a localized member one locale at a time', async ()=>{
|
|
277
|
+
const titled = (id, name, title)=>{
|
|
278
|
+
const widget = makeContractWidget(id, name, 0);
|
|
279
|
+
widget.title = title;
|
|
280
|
+
return widget;
|
|
281
|
+
};
|
|
282
|
+
await withRepository(async (repository)=>{
|
|
283
|
+
var _sorted_items_;
|
|
284
|
+
const found = await runRepository(repository.load({
|
|
285
|
+
filtering: [
|
|
286
|
+
{
|
|
287
|
+
property: 'title.es',
|
|
288
|
+
operator: 'like',
|
|
289
|
+
value: 'SOPA'
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
}));
|
|
293
|
+
expect(found.items.map((item)=>item.id)).toEqual([
|
|
294
|
+
'w-2'
|
|
295
|
+
]);
|
|
296
|
+
const sorted = await runRepository(repository.load({
|
|
297
|
+
sorting: [
|
|
298
|
+
{
|
|
299
|
+
0: {
|
|
300
|
+
property: 'title.en',
|
|
301
|
+
type: 'asc'
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
}));
|
|
306
|
+
expect(sorted.items.map((item)=>item.id)).toEqual([
|
|
307
|
+
'w-3',
|
|
308
|
+
'w-1',
|
|
309
|
+
'w-2'
|
|
310
|
+
]);
|
|
311
|
+
expect((_sorted_items_ = sorted.items[0]) == null ? void 0 : _sorted_items_.title).toEqual({
|
|
312
|
+
en: 'Apple',
|
|
313
|
+
es: 'Manzana'
|
|
314
|
+
});
|
|
315
|
+
}, [
|
|
316
|
+
titled('w-1', 'Alpha', {
|
|
317
|
+
en: 'Bread',
|
|
318
|
+
es: 'Pan'
|
|
319
|
+
}),
|
|
320
|
+
titled('w-2', 'Beta', {
|
|
321
|
+
en: 'Soup',
|
|
322
|
+
es: 'Sopa'
|
|
323
|
+
}),
|
|
324
|
+
titled('w-3', 'Gamma', {
|
|
325
|
+
en: 'Apple',
|
|
326
|
+
es: 'Manzana'
|
|
327
|
+
})
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
258
330
|
it('sorts descending when asked to', async ()=>{
|
|
259
331
|
await withRepository(async (repository)=>{
|
|
260
332
|
const page = await runRepository(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 { runRepository, runRepositoryExit } 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\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\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 runRepository(repository.get<ContractWidget>('w-1'));\n read.name = 'Mutated';\n\n const again = await runRepository(\n repository.get<ContractWidget>('w-1'),\n );\n expect(again.name).toBe('Alpha');\n\n const page = await runRepository(repository.load<ContractWidget>({}));\n page.items[0]!.name = 'Mutated';\n const reloaded = await runRepository(\n repository.load<ContractWidget>({}),\n );\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 runRepository(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 runRepository(\n repository.load<ContractWidget>(request),\n );\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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('sorts descending when asked to', async () => {\n await withRepository(async repository => {\n const page = await runRepository(\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 runRepository(\n repository.get<ContractWidget>('w-2'),\n );\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 runRepositoryExit(\n repository.get<ContractWidget>('missing'),\n );\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 runRepository(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 runRepository(repository.save(widget));\n\n widget.name = 'Mutated';\n returned.name = 'Mutated too';\n\n const stored = await runRepository(\n repository.get<ContractWidget>('w-9'),\n );\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 runRepository(\n repository.save<ContractWidget>(\n makeContractWidget(undefined, 'Delta', 40),\n ),\n );\n\n expect(created.id).toBeDefined();\n\n const page = await runRepository(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 runRepository(\n repository.save<ContractWidget>(\n makeContractWidget('w-2', 'Renamed', 25),\n ),\n );\n\n const found = await runRepository(\n repository.get<ContractWidget>('w-2'),\n );\n expect(found.name).toBe('Renamed');\n\n const page = await runRepository(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 runRepository(repository.delete<ContractWidget>('w-1'));\n\n const page = await runRepository(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 runRepository(\n repository.delete<ContractWidget>(\n makeContractWidget('w-3', 'Gamma', 30),\n ),\n );\n\n const page = await runRepository(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 runRepository(repository.delete<ContractWidget>('missing'));\n\n const page = await runRepository(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","runRepository","runRepositoryExit","key","type","ContractWidget","id","value","name","size","tags","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","sorting","found","exit","isFailure","describeEntityRepositoryWriteContract","returned","save","stored","created","undefined","toBeDefined","delete","describeEntityRepositoryContract","options"],"mappings":"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"}
|
|
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 { runRepository, runRepositoryExit } 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 runRepository(repository.get<ContractWidget>('w-1'));\n read.name = 'Mutated';\n\n const again = await runRepository(\n repository.get<ContractWidget>('w-1'),\n );\n expect(again.name).toBe('Alpha');\n\n const page = await runRepository(repository.load<ContractWidget>({}));\n page.items[0]!.name = 'Mutated';\n const reloaded = await runRepository(\n repository.load<ContractWidget>({}),\n );\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 runRepository(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 runRepository(\n repository.load<ContractWidget>(request),\n );\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\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 runRepository(\n repository.get<ContractWidget>('w-2'),\n );\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 runRepositoryExit(\n repository.get<ContractWidget>('missing'),\n );\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 runRepository(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 runRepository(repository.save(widget));\n\n widget.name = 'Mutated';\n returned.name = 'Mutated too';\n\n const stored = await runRepository(\n repository.get<ContractWidget>('w-9'),\n );\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 runRepository(\n repository.save<ContractWidget>(\n makeContractWidget(undefined, 'Delta', 40),\n ),\n );\n\n expect(created.id).toBeDefined();\n\n const page = await runRepository(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 runRepository(\n repository.save<ContractWidget>(\n makeContractWidget('w-2', 'Renamed', 25),\n ),\n );\n\n const found = await runRepository(\n repository.get<ContractWidget>('w-2'),\n );\n expect(found.name).toBe('Renamed');\n\n const page = await runRepository(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 runRepository(repository.delete<ContractWidget>('w-1'));\n\n const page = await runRepository(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 runRepository(\n repository.delete<ContractWidget>(\n makeContractWidget('w-3', 'Gamma', 30),\n ),\n );\n\n const page = await runRepository(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 runRepository(repository.delete<ContractWidget>('missing'));\n\n const page = await runRepository(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","runRepository","runRepositoryExit","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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction-ports.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/transaction-ports.contract.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"transaction-ports.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/transaction-ports.contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,eAAe,EAEf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAqB/B;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACtC,MAAM,MAAM,EACZ,iBAAiB,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,KACxD,IAqDF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,GAC1C,MAAM,MAAM,EACZ,qBAAqB,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,KACpE,IA6BF,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,QAAQ,CAAC;IACd,oEAAoE;IACpE,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8DAA8D;IAC9D,SAAS,IAAI,WAAW,EAAE,CAAC;CAC5B;AAwCD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GACnC,MAAM,MAAM,EACZ,aAAa,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,KAC5E,IAkDF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,MAAM,EACZ,WAAW,CAAC,QAAQ,EAAE,MAAM,KAAK,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAC5E,IAyCF,CAAC"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Duration, Effect, Exit, Fiber } from 'effect';
|
|
2
|
+
import { TestClock } from 'effect/testing';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
|
+
/**
|
|
5
|
+
* Runs an Effect test on a `TestClock`, so a retry budget measured in seconds
|
|
6
|
+
* costs none of the wall clock.
|
|
7
|
+
*/ const effectIt = {
|
|
8
|
+
effect: (name, body)=>it(name, ()=>Effect.runPromise(body().pipe(Effect.provide(TestClock.layer()), Effect.scoped)))
|
|
9
|
+
};
|
|
4
10
|
const run = (effect)=>Effect.runPromise(effect);
|
|
5
11
|
/**
|
|
6
12
|
* What every {@link LockService} must guarantee: exclusivity while held,
|
|
@@ -22,7 +28,7 @@ const run = (effect)=>Effect.runPromise(effect);
|
|
|
22
28
|
effectIt.effect('refuses a key that is already held', ()=>Effect.gen(function*() {
|
|
23
29
|
const locks = yield* Effect.promise(async ()=>makeLockService());
|
|
24
30
|
yield* locks.acquire('contract:taken');
|
|
25
|
-
const fiber = yield* Effect.
|
|
31
|
+
const fiber = yield* Effect.forkChild(locks.acquire('contract:taken'));
|
|
26
32
|
yield* TestClock.adjust(Duration.minutes(1));
|
|
27
33
|
const exit = yield* Fiber.await(fiber);
|
|
28
34
|
expect(Exit.isFailure(exit)).toBe(true);
|
|
@@ -42,7 +48,7 @@ const run = (effect)=>Effect.runPromise(effect);
|
|
|
42
48
|
token: 'not-mine'
|
|
43
49
|
});
|
|
44
50
|
// Still held, so a fresh acquisition must fail.
|
|
45
|
-
const fiber = yield* Effect.
|
|
51
|
+
const fiber = yield* Effect.forkChild(locks.acquire('contract:stolen'));
|
|
46
52
|
yield* TestClock.adjust(Duration.minutes(1));
|
|
47
53
|
const exit = yield* Fiber.await(fiber);
|
|
48
54
|
expect(Exit.isFailure(exit)).toBe(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/contracts/transaction-ports.contract.ts"],"sourcesContent":["import { it as effectIt } from '@effect/vitest';\nimport type { DomainEvent } from '@entifix/core';\nimport type {\n EventBus,\n LockService,\n SequenceService,\n Subscription,\n TransactionInbox,\n} from '@entifix/transactions';\nimport { Duration, Effect, Exit, Fiber, TestClock } from 'effect';\nimport { describe, expect, it } from 'vitest';\n\nconst run = <TValue, TError>(effect: Effect.Effect<TValue, TError>) =>\n Effect.runPromise(effect);\n\n/**\n * What every {@link LockService} must guarantee: exclusivity while held,\n * availability after release, and a release that only frees the caller's own\n * lock. The last one is why `release` takes a handle rather than a key — a lock\n * that lapsed and was re-acquired by someone else must not be freed by the\n * previous holder.\n */\nexport const describeLockServiceContract = (\n name: string,\n makeLockService: () => LockService | Promise<LockService>,\n): void => {\n describe(`LockService contract: ${name}`, () => {\n it('acquires an uncontended key', async () => {\n const locks = await makeLockService();\n\n const handle = await run(locks.acquire('contract:free'));\n\n expect(handle.key).toBe('contract:free');\n expect(handle.token).toBeTruthy();\n });\n\n // An implementation may retry before giving up (the Redis adapter waits out\n // a bounded budget), so this runs on a TestClock: the assertion is about\n // exclusivity, and it should not cost seconds of wall clock to make.\n effectIt.effect('refuses a key that is already held', () =>\n Effect.gen(function* () {\n const locks = yield* Effect.promise(async () => makeLockService());\n yield* locks.acquire('contract:taken');\n\n const fiber = yield* Effect.fork(locks.acquire('contract:taken'));\n yield* TestClock.adjust(Duration.minutes(1));\n const exit = yield* Fiber.await(fiber);\n\n expect(Exit.isFailure(exit)).toBe(true);\n }),\n );\n\n it('makes the key available again after release', async () => {\n const locks = await makeLockService();\n const handle = await run(locks.acquire('contract:cycle'));\n\n await run(locks.release(handle));\n const reacquired = await run(locks.acquire('contract:cycle'));\n\n expect(reacquired.key).toBe('contract:cycle');\n });\n\n effectIt.effect('ignores a release whose token does not match', () =>\n Effect.gen(function* () {\n const locks = yield* Effect.promise(async () => makeLockService());\n const handle = yield* locks.acquire('contract:stolen');\n\n yield* locks.release({ key: handle.key, token: 'not-mine' });\n\n // Still held, so a fresh acquisition must fail.\n const fiber = yield* Effect.fork(locks.acquire('contract:stolen'));\n yield* TestClock.adjust(Duration.minutes(1));\n const exit = yield* Fiber.await(fiber);\n\n expect(Exit.isFailure(exit)).toBe(true);\n }),\n );\n });\n};\n\n/**\n * What every {@link SequenceService} must guarantee: strictly increasing values\n * per name, never repeating, with names independent of one another. This is the\n * property incremental codes rest on.\n */\nexport const describeSequenceServiceContract = (\n name: string,\n makeSequenceService: () => SequenceService | Promise<SequenceService>,\n): void => {\n describe(`SequenceService contract: ${name}`, () => {\n it('starts at 1', async () => {\n const sequences = await makeSequenceService();\n\n expect(await run(sequences.next('contract:fresh'))).toBe(1);\n });\n\n it('never returns the same value twice for a name', async () => {\n const sequences = await makeSequenceService();\n\n const drawn = [\n await run(sequences.next('contract:codes')),\n await run(sequences.next('contract:codes')),\n await run(sequences.next('contract:codes')),\n ];\n\n expect(drawn).toEqual([1, 2, 3]);\n expect(new Set(drawn).size).toBe(drawn.length);\n });\n\n it('keeps separate names independent', async () => {\n const sequences = await makeSequenceService();\n await run(sequences.next('contract:a'));\n await run(sequences.next('contract:a'));\n\n expect(await run(sequences.next('contract:b'))).toBe(1);\n });\n });\n};\n\nexport interface EventBusContractHarness {\n bus: EventBus;\n /** Delivers `event` to whatever subscribed, as the broker would. */\n deliver(event: DomainEvent): Promise<void>;\n /** Everything the bus published, decoded back into events. */\n published(): DomainEvent[];\n}\n\n/** Every subscription in this contract, unless a test says otherwise. */\nconst ANY_WIDGET_EVENT = 'contract.widget.*';\n\n/**\n * A `work` subscription for `pattern`. The mode and the ceiling are broker\n * facts with no bearing on routing, so a test about routing states them once\n * here rather than repeating them at every call.\n */\nconst on = (pattern: string): Subscription => ({\n slice: 'contract',\n pattern,\n mode: 'work',\n maxAttempts: 5,\n});\n\n/**\n * A handler that records what it is given. Shared by both routing tests so the\n * \"nothing was delivered\" case asserts on the *same* handler the positive case\n * proves works — otherwise it could pass against a handler that was simply\n * never wired up.\n */\nconst collectInto = (sink: DomainEvent[]) => (event: DomainEvent) =>\n Effect.sync(() => {\n sink.push(event);\n });\n\nconst anEvent = (\n id: string,\n name = 'contract.widget.created',\n): DomainEvent => ({\n name,\n id,\n source: 'contract-slice',\n at: new Date('2026-01-01T00:00:00.000Z').toISOString(),\n correlationId: id,\n data: { entity: 'contract-widget' },\n});\n\n/**\n * What every {@link EventBus} must guarantee: what goes out comes back\n * unchanged, subscribers receive deliveries, and a subscriber receives only what\n * its pattern matches. The transports differ wildly (an array here, a topic\n * exchange in RabbitMQ) — the observable contract does not.\n */\nexport const describeEventBusContract = (\n name: string,\n makeHarness: () => EventBusContractHarness | Promise<EventBusContractHarness>,\n): void => {\n describe(`EventBus contract: ${name}`, () => {\n it('publishes an event verbatim', async () => {\n const harness = await makeHarness();\n const event = anEvent('tx-1');\n\n await run(harness.bus.publish(event));\n\n expect(harness.published()).toEqual([event]);\n });\n\n it('delivers published events to a subscriber', async () => {\n const harness = await makeHarness();\n const received: DomainEvent[] = [];\n await run(\n harness.bus.subscribe(on(ANY_WIDGET_EVENT), collectInto(received)),\n );\n\n await harness.deliver(anEvent('tx-2'));\n\n expect(received.map(event => event.id)).toEqual(['tx-2']);\n });\n\n // Routing is the transport's job. A bus that hands every subscriber every\n // event lets a consumer pass while receiving traffic it never bound to —\n // which is precisely the fault the fanout exchange had.\n it('delivers nothing to a subscriber whose pattern does not match', async () => {\n const harness = await makeHarness();\n const received: DomainEvent[] = [];\n await run(\n harness.bus.subscribe(on('contract.other.*'), collectInto(received)),\n );\n\n await harness.deliver(anEvent('tx-5'));\n\n expect(received).toEqual([]);\n });\n\n it('preserves publication order', async () => {\n const harness = await makeHarness();\n\n await run(harness.bus.publish(anEvent('tx-3')));\n await run(harness.bus.publish(anEvent('tx-4')));\n\n expect(harness.published().map(event => event.id)).toEqual([\n 'tx-3',\n 'tx-4',\n ]);\n });\n });\n};\n\n/**\n * What every {@link TransactionInbox} must guarantee: a first claim is granted,\n * every repeat of it is refused, and one consumer's claim says nothing about\n * another's.\n *\n * That last one is the property with teeth. Two consumers legitimately process\n * the same event — the saga tracker's fold and the SSE hub both bind\n * `transaction.*` — so an implementation keyed on `eventId` alone passes the\n * first two cases and silently starves every consumer but the first. It looks\n * like exactly-once from inside one consumer, which is why it is asserted here\n * rather than left to each adapter's own spec.\n */\nexport const describeTransactionInboxContract = (\n name: string,\n makeInbox: (consumer: string) => TransactionInbox | Promise<TransactionInbox>,\n): void => {\n describe(`TransactionInbox contract: ${name}`, () => {\n it('grants a first claim', async () => {\n const inbox = await makeInbox('contract.consumer');\n\n expect(await run(inbox.claim('tx-1:completed'))).toBe('claimed');\n });\n\n it('refuses a redelivery of the same event', async () => {\n const inbox = await makeInbox('contract.consumer');\n await run(inbox.claim('tx-2:completed'));\n\n expect(await run(inbox.claim('tx-2:completed'))).toBe('duplicate');\n });\n\n it('refuses every repeat, not only the second', async () => {\n const inbox = await makeInbox('contract.consumer');\n await run(inbox.claim('tx-3:completed'));\n\n expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');\n expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');\n });\n\n it('claims each step of one transaction separately', async () => {\n // `<transactionId>:<step>`, never the transaction id alone: one\n // transaction emits up to three messages, and keying on the correlation\n // id would make `completed` read as a redelivery of `accepted`.\n const inbox = await makeInbox('contract.consumer');\n\n expect(await run(inbox.claim('tx-4:accepted'))).toBe('claimed');\n expect(await run(inbox.claim('tx-4:completed'))).toBe('claimed');\n });\n\n it('does not let one consumer consume another consumer’s claim', async () => {\n const first = await makeInbox('contract.first');\n const second = await makeInbox('contract.second');\n await run(first.claim('tx-5:completed'));\n\n expect(await run(second.claim('tx-5:completed'))).toBe('claimed');\n });\n });\n};\n"],"names":["it","effectIt","Duration","Effect","Exit","Fiber","TestClock","describe","expect","run","effect","runPromise","describeLockServiceContract","name","makeLockService","locks","handle","acquire","key","toBe","token","toBeTruthy","gen","promise","fiber","fork","adjust","minutes","exit","await","isFailure","release","reacquired","describeSequenceServiceContract","makeSequenceService","sequences","next","drawn","toEqual","Set","size","length","ANY_WIDGET_EVENT","on","pattern","slice","mode","maxAttempts","collectInto","sink","event","sync","push","anEvent","id","source","at","Date","toISOString","correlationId","data","entity","describeEventBusContract","makeHarness","harness","bus","publish","published","received","subscribe","deliver","map","describeTransactionInboxContract","makeInbox","inbox","claim","first","second"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/contracts/transaction-ports.contract.ts"],"sourcesContent":["import type { DomainEvent } from '@entifix/core';\nimport type {\n EventBus,\n LockService,\n SequenceService,\n Subscription,\n TransactionInbox,\n} from '@entifix/transactions';\nimport { Duration, Effect, Exit, Fiber } from 'effect';\nimport { TestClock } from 'effect/testing';\nimport { describe, expect, it } from 'vitest';\n\n/**\n * Runs an Effect test on a `TestClock`, so a retry budget measured in seconds\n * costs none of the wall clock.\n */\nconst effectIt = {\n effect: (name: string, body: () => Effect.Effect<unknown, unknown>) =>\n it(name, () =>\n Effect.runPromise(\n body().pipe(Effect.provide(TestClock.layer()), Effect.scoped),\n ),\n ),\n};\n\nconst run = <TValue, TError>(effect: Effect.Effect<TValue, TError>) =>\n Effect.runPromise(effect);\n\n/**\n * What every {@link LockService} must guarantee: exclusivity while held,\n * availability after release, and a release that only frees the caller's own\n * lock. The last one is why `release` takes a handle rather than a key — a lock\n * that lapsed and was re-acquired by someone else must not be freed by the\n * previous holder.\n */\nexport const describeLockServiceContract = (\n name: string,\n makeLockService: () => LockService | Promise<LockService>,\n): void => {\n describe(`LockService contract: ${name}`, () => {\n it('acquires an uncontended key', async () => {\n const locks = await makeLockService();\n\n const handle = await run(locks.acquire('contract:free'));\n\n expect(handle.key).toBe('contract:free');\n expect(handle.token).toBeTruthy();\n });\n\n // An implementation may retry before giving up (the Redis adapter waits out\n // a bounded budget), so this runs on a TestClock: the assertion is about\n // exclusivity, and it should not cost seconds of wall clock to make.\n effectIt.effect('refuses a key that is already held', () =>\n Effect.gen(function* () {\n const locks = yield* Effect.promise(async () => makeLockService());\n yield* locks.acquire('contract:taken');\n\n const fiber = yield* Effect.forkChild(locks.acquire('contract:taken'));\n yield* TestClock.adjust(Duration.minutes(1));\n const exit = yield* Fiber.await(fiber);\n\n expect(Exit.isFailure(exit)).toBe(true);\n }),\n );\n\n it('makes the key available again after release', async () => {\n const locks = await makeLockService();\n const handle = await run(locks.acquire('contract:cycle'));\n\n await run(locks.release(handle));\n const reacquired = await run(locks.acquire('contract:cycle'));\n\n expect(reacquired.key).toBe('contract:cycle');\n });\n\n effectIt.effect('ignores a release whose token does not match', () =>\n Effect.gen(function* () {\n const locks = yield* Effect.promise(async () => makeLockService());\n const handle = yield* locks.acquire('contract:stolen');\n\n yield* locks.release({ key: handle.key, token: 'not-mine' });\n\n // Still held, so a fresh acquisition must fail.\n const fiber = yield* Effect.forkChild(locks.acquire('contract:stolen'));\n yield* TestClock.adjust(Duration.minutes(1));\n const exit = yield* Fiber.await(fiber);\n\n expect(Exit.isFailure(exit)).toBe(true);\n }),\n );\n });\n};\n\n/**\n * What every {@link SequenceService} must guarantee: strictly increasing values\n * per name, never repeating, with names independent of one another. This is the\n * property incremental codes rest on.\n */\nexport const describeSequenceServiceContract = (\n name: string,\n makeSequenceService: () => SequenceService | Promise<SequenceService>,\n): void => {\n describe(`SequenceService contract: ${name}`, () => {\n it('starts at 1', async () => {\n const sequences = await makeSequenceService();\n\n expect(await run(sequences.next('contract:fresh'))).toBe(1);\n });\n\n it('never returns the same value twice for a name', async () => {\n const sequences = await makeSequenceService();\n\n const drawn = [\n await run(sequences.next('contract:codes')),\n await run(sequences.next('contract:codes')),\n await run(sequences.next('contract:codes')),\n ];\n\n expect(drawn).toEqual([1, 2, 3]);\n expect(new Set(drawn).size).toBe(drawn.length);\n });\n\n it('keeps separate names independent', async () => {\n const sequences = await makeSequenceService();\n await run(sequences.next('contract:a'));\n await run(sequences.next('contract:a'));\n\n expect(await run(sequences.next('contract:b'))).toBe(1);\n });\n });\n};\n\nexport interface EventBusContractHarness {\n bus: EventBus;\n /** Delivers `event` to whatever subscribed, as the broker would. */\n deliver(event: DomainEvent): Promise<void>;\n /** Everything the bus published, decoded back into events. */\n published(): DomainEvent[];\n}\n\n/** Every subscription in this contract, unless a test says otherwise. */\nconst ANY_WIDGET_EVENT = 'contract.widget.*';\n\n/**\n * A `work` subscription for `pattern`. The mode and the ceiling are broker\n * facts with no bearing on routing, so a test about routing states them once\n * here rather than repeating them at every call.\n */\nconst on = (pattern: string): Subscription => ({\n slice: 'contract',\n pattern,\n mode: 'work',\n maxAttempts: 5,\n});\n\n/**\n * A handler that records what it is given. Shared by both routing tests so the\n * \"nothing was delivered\" case asserts on the *same* handler the positive case\n * proves works — otherwise it could pass against a handler that was simply\n * never wired up.\n */\nconst collectInto = (sink: DomainEvent[]) => (event: DomainEvent) =>\n Effect.sync(() => {\n sink.push(event);\n });\n\nconst anEvent = (\n id: string,\n name = 'contract.widget.created',\n): DomainEvent => ({\n name,\n id,\n source: 'contract-slice',\n at: new Date('2026-01-01T00:00:00.000Z').toISOString(),\n correlationId: id,\n data: { entity: 'contract-widget' },\n});\n\n/**\n * What every {@link EventBus} must guarantee: what goes out comes back\n * unchanged, subscribers receive deliveries, and a subscriber receives only what\n * its pattern matches. The transports differ wildly (an array here, a topic\n * exchange in RabbitMQ) — the observable contract does not.\n */\nexport const describeEventBusContract = (\n name: string,\n makeHarness: () => EventBusContractHarness | Promise<EventBusContractHarness>,\n): void => {\n describe(`EventBus contract: ${name}`, () => {\n it('publishes an event verbatim', async () => {\n const harness = await makeHarness();\n const event = anEvent('tx-1');\n\n await run(harness.bus.publish(event));\n\n expect(harness.published()).toEqual([event]);\n });\n\n it('delivers published events to a subscriber', async () => {\n const harness = await makeHarness();\n const received: DomainEvent[] = [];\n await run(\n harness.bus.subscribe(on(ANY_WIDGET_EVENT), collectInto(received)),\n );\n\n await harness.deliver(anEvent('tx-2'));\n\n expect(received.map(event => event.id)).toEqual(['tx-2']);\n });\n\n // Routing is the transport's job. A bus that hands every subscriber every\n // event lets a consumer pass while receiving traffic it never bound to —\n // which is precisely the fault the fanout exchange had.\n it('delivers nothing to a subscriber whose pattern does not match', async () => {\n const harness = await makeHarness();\n const received: DomainEvent[] = [];\n await run(\n harness.bus.subscribe(on('contract.other.*'), collectInto(received)),\n );\n\n await harness.deliver(anEvent('tx-5'));\n\n expect(received).toEqual([]);\n });\n\n it('preserves publication order', async () => {\n const harness = await makeHarness();\n\n await run(harness.bus.publish(anEvent('tx-3')));\n await run(harness.bus.publish(anEvent('tx-4')));\n\n expect(harness.published().map(event => event.id)).toEqual([\n 'tx-3',\n 'tx-4',\n ]);\n });\n });\n};\n\n/**\n * What every {@link TransactionInbox} must guarantee: a first claim is granted,\n * every repeat of it is refused, and one consumer's claim says nothing about\n * another's.\n *\n * That last one is the property with teeth. Two consumers legitimately process\n * the same event — the saga tracker's fold and the SSE hub both bind\n * `transaction.*` — so an implementation keyed on `eventId` alone passes the\n * first two cases and silently starves every consumer but the first. It looks\n * like exactly-once from inside one consumer, which is why it is asserted here\n * rather than left to each adapter's own spec.\n */\nexport const describeTransactionInboxContract = (\n name: string,\n makeInbox: (consumer: string) => TransactionInbox | Promise<TransactionInbox>,\n): void => {\n describe(`TransactionInbox contract: ${name}`, () => {\n it('grants a first claim', async () => {\n const inbox = await makeInbox('contract.consumer');\n\n expect(await run(inbox.claim('tx-1:completed'))).toBe('claimed');\n });\n\n it('refuses a redelivery of the same event', async () => {\n const inbox = await makeInbox('contract.consumer');\n await run(inbox.claim('tx-2:completed'));\n\n expect(await run(inbox.claim('tx-2:completed'))).toBe('duplicate');\n });\n\n it('refuses every repeat, not only the second', async () => {\n const inbox = await makeInbox('contract.consumer');\n await run(inbox.claim('tx-3:completed'));\n\n expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');\n expect(await run(inbox.claim('tx-3:completed'))).toBe('duplicate');\n });\n\n it('claims each step of one transaction separately', async () => {\n // `<transactionId>:<step>`, never the transaction id alone: one\n // transaction emits up to three messages, and keying on the correlation\n // id would make `completed` read as a redelivery of `accepted`.\n const inbox = await makeInbox('contract.consumer');\n\n expect(await run(inbox.claim('tx-4:accepted'))).toBe('claimed');\n expect(await run(inbox.claim('tx-4:completed'))).toBe('claimed');\n });\n\n it('does not let one consumer consume another consumer’s claim', async () => {\n const first = await makeInbox('contract.first');\n const second = await makeInbox('contract.second');\n await run(first.claim('tx-5:completed'));\n\n expect(await run(second.claim('tx-5:completed'))).toBe('claimed');\n });\n });\n};\n"],"names":["Duration","Effect","Exit","Fiber","TestClock","describe","expect","it","effectIt","effect","name","body","runPromise","pipe","provide","layer","scoped","run","describeLockServiceContract","makeLockService","locks","handle","acquire","key","toBe","token","toBeTruthy","gen","promise","fiber","forkChild","adjust","minutes","exit","await","isFailure","release","reacquired","describeSequenceServiceContract","makeSequenceService","sequences","next","drawn","toEqual","Set","size","length","ANY_WIDGET_EVENT","on","pattern","slice","mode","maxAttempts","collectInto","sink","event","sync","push","anEvent","id","source","at","Date","toISOString","correlationId","data","entity","describeEventBusContract","makeHarness","harness","bus","publish","published","received","subscribe","deliver","map","describeTransactionInboxContract","makeInbox","inbox","claim","first","second"],"mappings":"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"}
|
|
@@ -15,8 +15,9 @@ export interface InMemoryEntityRepository extends EntityRepository {
|
|
|
15
15
|
* In-memory {@link EntityRepository}: the default double for anything that
|
|
16
16
|
* depends on persistence.
|
|
17
17
|
*
|
|
18
|
-
* Its semantics deliberately mirror `makeMongoRepository` — same filtering
|
|
19
|
-
* sorting rules,
|
|
18
|
+
* Its semantics deliberately mirror `makeMongoRepository` — the same filtering
|
|
19
|
+
* and sorting rules, which it takes from core's in-memory query evaluation
|
|
20
|
+
* rather than keeping a copy of its own, same 1-based paging with a default page size of 10, same
|
|
20
21
|
* "entity not found" failure, same id minting on create — and
|
|
21
22
|
* `describeEntityRepositoryContract` holds both to that shared contract.
|
|
22
23
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-repository.d.ts","sourceRoot":"","sources":["../../src/doubles/entity-repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,
|
|
1
|
+
{"version":3,"file":"entity-repository.d.ts","sourceRoot":"","sources":["../../src/doubles/entity-repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAKL,KAAK,YAAY,EACjB,KAAK,MAAM,EAOZ,MAAM,eAAe,CAAC;AAiCvB,MAAM,WAAW,wBAAyB,SAAQ,gBAAgB;IAChE,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IACzB,4DAA4D;IAC5D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,GACvC,OAAM,MAAM,EAAO,KAClB,wBAuGF,CAAC"}
|
|
@@ -1,95 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyFiltering, applySorting, deserializeSingleEntity, EntifixConnError, extractMetaAccessors, serializeEntity } from '@entifix/core';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
|
-
/** Escapes a string so `like`/`nlike` match literally, as the Mongo adapter does. */ const escapeRegex = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4
|
-
const isFilterGroup = (node)=>!('property' in node) && (node.operator === 'and' || node.operator === 'or');
|
|
5
|
-
/**
|
|
6
|
-
* A member as the Mongo adapter stores it, and so as its queries see it: a
|
|
7
|
-
* link is its target's id, a collection link the list of their ids. Without
|
|
8
|
-
* this no operator can compare a link, since `eq` against an id is never
|
|
9
|
-
* `true` for an object.
|
|
10
|
-
*/ const storedValue = (value)=>{
|
|
11
|
-
if (value instanceof EntityLink) return value.id;
|
|
12
|
-
if (value instanceof EntityCollectionLink) return value.ids;
|
|
13
|
-
return value;
|
|
14
|
-
};
|
|
15
|
-
/** `$in` semantics: a scalar matches a value, an array when any element does. */ const matchesAny = (actual, values)=>Array.isArray(actual) ? actual.some((element)=>values.includes(element)) : values.includes(actual);
|
|
16
|
-
const compareValues = (left, right)=>{
|
|
17
|
-
if (left === right) return 0;
|
|
18
|
-
// Mongo sorts missing values below present ones.
|
|
19
|
-
if (left == null) return -1;
|
|
20
|
-
if (right == null) return 1;
|
|
21
|
-
if (typeof left === 'number' && typeof right === 'number') {
|
|
22
|
-
return left - right;
|
|
23
|
-
}
|
|
24
|
-
if (left instanceof Date && right instanceof Date) {
|
|
25
|
-
return left.getTime() - right.getTime();
|
|
26
|
-
}
|
|
27
|
-
return String(left).localeCompare(String(right));
|
|
28
|
-
};
|
|
29
|
-
const matchesNode = (item, node)=>{
|
|
30
|
-
if (isFilterGroup(node)) {
|
|
31
|
-
return node.operator === 'or' ? node.values.some((child)=>matchesNode(item, child)) : node.values.every((child)=>matchesNode(item, child));
|
|
32
|
-
}
|
|
33
|
-
const actual = storedValue(item[String(node.property)]);
|
|
34
|
-
switch(node.operator){
|
|
35
|
-
case 'eq':
|
|
36
|
-
return actual === node.value;
|
|
37
|
-
case 'ne':
|
|
38
|
-
return actual !== node.value;
|
|
39
|
-
case 'gt':
|
|
40
|
-
return compareValues(actual, node.value) > 0;
|
|
41
|
-
case 'gte':
|
|
42
|
-
return compareValues(actual, node.value) >= 0;
|
|
43
|
-
case 'lt':
|
|
44
|
-
return compareValues(actual, node.value) < 0;
|
|
45
|
-
case 'lte':
|
|
46
|
-
return compareValues(actual, node.value) <= 0;
|
|
47
|
-
case 'in':
|
|
48
|
-
return matchesAny(actual, node.values);
|
|
49
|
-
case 'nin':
|
|
50
|
-
return !matchesAny(actual, node.values);
|
|
51
|
-
case 'between':
|
|
52
|
-
return compareValues(actual, node.start) >= 0 && compareValues(actual, node.end) <= 0;
|
|
53
|
-
case 'nbetween':
|
|
54
|
-
return !(compareValues(actual, node.start) >= 0 && compareValues(actual, node.end) <= 0);
|
|
55
|
-
case 'like':
|
|
56
|
-
return new RegExp(escapeRegex(node.value), 'i').test(String(actual));
|
|
57
|
-
case 'nlike':
|
|
58
|
-
return !new RegExp(escapeRegex(node.value), 'i').test(String(actual));
|
|
59
|
-
case 'isNull':
|
|
60
|
-
return actual == null;
|
|
61
|
-
case 'isNotNull':
|
|
62
|
-
return actual != null;
|
|
63
|
-
default:
|
|
64
|
-
{
|
|
65
|
-
// Exhaustiveness guard — a new operator must be handled explicitly.
|
|
66
|
-
const never = node;
|
|
67
|
-
return never;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const applyFiltering = (items, filtering)=>{
|
|
72
|
-
if (!filtering || filtering.length === 0) return items;
|
|
73
|
-
// Every top-level entry is combined with `and`, matching `translateFiltering`.
|
|
74
|
-
const nodes = filtering.flatMap((entry)=>Array.isArray(entry) ? entry : [
|
|
75
|
-
entry
|
|
76
|
-
]);
|
|
77
|
-
return items.filter((item)=>nodes.every((node)=>matchesNode(item, node)));
|
|
78
|
-
};
|
|
79
|
-
const applySorting = (items, sorting)=>{
|
|
80
|
-
if (!sorting || sorting.length === 0) return items;
|
|
81
|
-
const entries = sorting.flatMap((record)=>Object.keys(record).map(Number).sort((left, right)=>left - right).map((priority)=>record[priority]).filter((entry)=>entry != null));
|
|
82
|
-
return [
|
|
83
|
-
...items
|
|
84
|
-
].sort((left, right)=>{
|
|
85
|
-
for (const entry of entries){
|
|
86
|
-
const property = String(entry.property);
|
|
87
|
-
const result = compareValues(left[property], right[property]);
|
|
88
|
-
if (result !== 0) return entry.type === 'desc' ? -result : result;
|
|
89
|
-
}
|
|
90
|
-
return 0;
|
|
91
|
-
});
|
|
92
|
-
};
|
|
93
3
|
/**
|
|
94
4
|
* A copy of `entity` that shares no state with it.
|
|
95
5
|
*
|
|
@@ -112,8 +22,9 @@ const applySorting = (items, sorting)=>{
|
|
|
112
22
|
* In-memory {@link EntityRepository}: the default double for anything that
|
|
113
23
|
* depends on persistence.
|
|
114
24
|
*
|
|
115
|
-
* Its semantics deliberately mirror `makeMongoRepository` — same filtering
|
|
116
|
-
* sorting rules,
|
|
25
|
+
* Its semantics deliberately mirror `makeMongoRepository` — the same filtering
|
|
26
|
+
* and sorting rules, which it takes from core's in-memory query evaluation
|
|
27
|
+
* rather than keeping a copy of its own, same 1-based paging with a default page size of 10, same
|
|
117
28
|
* "entity not found" failure, same id minting on create — and
|
|
118
29
|
* `describeEntityRepositoryContract` holds both to that shared contract.
|
|
119
30
|
*/ export const makeInMemoryEntityRepository = (seed = [])=>{
|
|
@@ -157,14 +68,14 @@ const applySorting = (items, sorting)=>{
|
|
|
157
68
|
// The store keeps its own copy and hands back another, so neither the
|
|
158
69
|
// caller's instance nor the returned one can change the record without a
|
|
159
70
|
// second save — which a real adapter guarantees by construction.
|
|
160
|
-
return copyOf(entity).pipe(Effect.tap((stored)=>{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
71
|
+
return copyOf(entity).pipe(Effect.tap((stored)=>Effect.sync(()=>{
|
|
72
|
+
const index = items.findIndex((item)=>item.id === stored.id);
|
|
73
|
+
if (index === -1) {
|
|
74
|
+
items.push(stored);
|
|
75
|
+
} else {
|
|
76
|
+
items[index] = stored;
|
|
77
|
+
}
|
|
78
|
+
})), Effect.flatMap(copyOf));
|
|
168
79
|
});
|
|
169
80
|
const remove = (entityOrId)=>guard(()=>{
|
|
170
81
|
const id = entityOrId != null && typeof entityOrId === 'object' ? entityOrId.id : entityOrId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/doubles/entity-repository.ts"],"sourcesContent":["import type { EntityRepository } from '@entifix/business';\nimport {\n deserializeSingleEntity,\n EntifixConnError,\n type EntifixError,\n type Entity,\n EntityCollectionLink,\n type EntityConstructor,\n type EntityFilter,\n type EntityFiltering,\n type EntityId,\n EntityLink,\n type EntityLoadRequest,\n type EntityPage,\n type EntitySorting,\n extractMetaAccessors,\n type FilterGroup,\n serializeEntity,\n} from '@entifix/core';\nimport { Effect } from 'effect';\n\n/** Escapes a string so `like`/`nlike` match literally, as the Mongo adapter does. */\nconst escapeRegex = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst isFilterGroup = <TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> =>\n !('property' in node) && (node.operator === 'and' || node.operator === 'or');\n\n/**\n * A member as the Mongo adapter stores it, and so as its queries see it: a\n * link is its target's id, a collection link the list of their ids. Without\n * this no operator can compare a link, since `eq` against an id is never\n * `true` for an object.\n */\nconst storedValue = (value: unknown): unknown => {\n if (value instanceof EntityLink) return value.id;\n if (value instanceof EntityCollectionLink) return value.ids;\n return value;\n};\n\n/** `$in` semantics: a scalar matches a value, an array when any element does. */\nconst matchesAny = (actual: unknown, values: readonly unknown[]): boolean =>\n Array.isArray(actual)\n ? actual.some(element => values.includes(element))\n : values.includes(actual);\n\nconst compareValues = (left: unknown, right: unknown): number => {\n if (left === right) return 0;\n // Mongo sorts missing values below present ones.\n if (left == null) return -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number') {\n return left - right;\n }\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n};\n\nconst matchesNode = <TEntity extends Entity>(\n item: TEntity,\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): boolean => {\n if (isFilterGroup(node)) {\n return node.operator === 'or'\n ? node.values.some(child => matchesNode(item, child))\n : node.values.every(child => matchesNode(item, child));\n }\n\n const actual = storedValue(\n (item as Record<string, unknown>)[String(node.property)],\n );\n\n switch (node.operator) {\n case 'eq':\n return actual === node.value;\n case 'ne':\n return actual !== node.value;\n case 'gt':\n return compareValues(actual, node.value) > 0;\n case 'gte':\n return compareValues(actual, node.value) >= 0;\n case 'lt':\n return compareValues(actual, node.value) < 0;\n case 'lte':\n return compareValues(actual, node.value) <= 0;\n case 'in':\n return matchesAny(actual, node.values);\n case 'nin':\n return !matchesAny(actual, node.values);\n case 'between':\n return (\n compareValues(actual, node.start) >= 0 &&\n compareValues(actual, node.end) <= 0\n );\n case 'nbetween':\n return !(\n compareValues(actual, node.start) >= 0 &&\n compareValues(actual, node.end) <= 0\n );\n case 'like':\n return new RegExp(escapeRegex(node.value), 'i').test(String(actual));\n case 'nlike':\n return !new RegExp(escapeRegex(node.value), 'i').test(String(actual));\n case 'isNull':\n return actual == null;\n case 'isNotNull':\n return actual != null;\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const never: never = node;\n return never;\n }\n }\n};\n\nconst applyFiltering = <TEntity extends Entity>(\n items: TEntity[],\n filtering?: EntityFiltering<TEntity>[],\n): TEntity[] => {\n if (!filtering || filtering.length === 0) return items;\n // Every top-level entry is combined with `and`, matching `translateFiltering`.\n const nodes = filtering.flatMap(entry =>\n Array.isArray(entry) ? entry : [entry],\n );\n return items.filter(item => nodes.every(node => matchesNode(item, node)));\n};\n\nconst applySorting = <TEntity extends Entity>(\n items: TEntity[],\n sorting?: EntitySorting<TEntity>[],\n): TEntity[] => {\n if (!sorting || sorting.length === 0) return items;\n\n const entries = sorting.flatMap(record =>\n Object.keys(record)\n .map(Number)\n .sort((left, right) => left - right)\n .map(priority => record[priority])\n .filter(entry => entry != null),\n );\n\n return [...items].sort((left, right) => {\n for (const entry of entries) {\n const property = String(entry.property);\n const result = compareValues(\n (left as Record<string, unknown>)[property],\n (right as Record<string, unknown>)[property],\n );\n if (result !== 0) return entry.type === 'desc' ? -result : result;\n }\n return 0;\n });\n};\n\n/**\n * A copy of `entity` that shares no state with it.\n *\n * Through the entity's own mapping — serialize, then deserialize — because that\n * is what a real adapter does to every record on the way in and out: a caller\n * can never hold the instance the store keeps, and a member the mapping drops\n * (`hidden`, `readonly`) does not survive a save here either. A class that\n * declares no accessors has no mapping to go through, so it is copied member by\n * member instead; plain spec fixtures are the case this keeps working.\n */\nconst copyOf = <TEntity extends Entity>(\n entity: TEntity,\n): Effect.Effect<TEntity, EntifixError> => {\n const entityConstructor = entity.constructor as EntityConstructor<TEntity>;\n if (extractMetaAccessors(entityConstructor).length === 0) {\n return Effect.succeed(\n Object.assign(\n Object.create(Object.getPrototypeOf(entity) as object) as TEntity,\n entity,\n ),\n );\n }\n // `undefined` only comes back for `null`/`undefined` input, and\n // `serializeEntity` always hands over an object.\n return deserializeSingleEntity(\n entityConstructor,\n serializeEntity(entityConstructor, entity),\n ) as Effect.Effect<TEntity, EntifixError>;\n};\n\nexport interface InMemoryEntityRepository extends EntityRepository {\n /** Everything currently stored, in insertion order. */\n readonly items: Entity[];\n /** Replaces the contents, discarding whatever was there. */\n seed(items: Entity[]): void;\n /**\n * Makes the next call to any method fail with `error`, so error branches are\n * reachable without reaching for a mocking library.\n */\n failNext(error: EntifixError): void;\n}\n\n/**\n * In-memory {@link EntityRepository}: the default double for anything that\n * depends on persistence.\n *\n * Its semantics deliberately mirror `makeMongoRepository` — same filtering and\n * sorting rules, same 1-based paging with a default page size of 10, same\n * \"entity not found\" failure, same id minting on create — and\n * `describeEntityRepositoryContract` holds both to that shared contract.\n */\nexport const makeInMemoryEntityRepository = (\n seed: Entity[] = [],\n): InMemoryEntityRepository => {\n let items = [...seed];\n let pendingFailure: EntifixError | undefined;\n\n /** Consumes a queued failure, if one was armed. */\n const guard = <TValue>(\n run: () => Effect.Effect<TValue, EntifixError>,\n ): Effect.Effect<TValue, EntifixError> => {\n if (pendingFailure !== undefined) {\n const failure = pendingFailure;\n pendingFailure = undefined;\n return Effect.fail(failure);\n }\n return run();\n };\n\n const load = <TEntity extends Entity>(request: EntityLoadRequest<TEntity>) =>\n guard(() => {\n const matched = applySorting(\n applyFiltering(items as TEntity[], request.filtering),\n request.sorting,\n );\n const page = request.page ?? 1;\n const pageSize = request.pageSize ?? 10;\n const start = (page - 1) * pageSize;\n\n return Effect.forEach(\n matched.slice(start, start + pageSize),\n copyOf,\n ).pipe(\n Effect.map(\n copies =>\n ({\n items: copies,\n total: matched.length,\n request,\n }) satisfies EntityPage<TEntity>,\n ),\n );\n });\n\n const get = <TEntity extends Entity>(id: EntityId) =>\n guard(() => {\n const found = items.find(item => item.id === id);\n return found === undefined\n ? Effect.fail(\n new EntifixConnError('Entity not found', undefined, { id }),\n )\n : copyOf(found as TEntity);\n });\n\n const save = <TEntity extends Entity>(entity: TEntity) =>\n guard(() => {\n // A create arrives without an id, so the store mints one — same rule the\n // Mongo adapter follows. The Web Crypto global rather than `node:crypto`,\n // because a browser bundle cannot resolve a `node:` import and an example\n // with no backend runs this repository in the page.\n entity.id = entity.id ?? globalThis.crypto.randomUUID();\n // The store keeps its own copy and hands back another, so neither the\n // caller's instance nor the returned one can change the record without a\n // second save — which a real adapter guarantees by construction.\n return copyOf(entity).pipe(\n Effect.tap(stored => {\n const index = items.findIndex(item => item.id === stored.id);\n if (index === -1) {\n items.push(stored);\n } else {\n items[index] = stored;\n }\n }),\n Effect.flatMap(copyOf),\n );\n });\n\n const remove = <TEntity extends Entity>(entityOrId: EntityId | TEntity) =>\n guard(() => {\n const id =\n entityOrId != null && typeof entityOrId === 'object'\n ? entityOrId.id\n : entityOrId;\n items = items.filter(item => item.id !== id);\n return Effect.succeed(undefined);\n });\n\n return {\n load,\n get,\n save,\n delete: remove,\n get items() {\n return items;\n },\n seed: (next: Entity[]) => {\n items = [...next];\n },\n failNext: (error: EntifixError) => {\n pendingFailure = error;\n },\n // Each method closes over its own state, so nothing is required from the\n // Effect environment; that is assignable to the interface's channel.\n } as unknown as InMemoryEntityRepository;\n};\n"],"names":["deserializeSingleEntity","EntifixConnError","EntityCollectionLink","EntityLink","extractMetaAccessors","serializeEntity","Effect","escapeRegex","value","replace","isFilterGroup","node","operator","storedValue","id","ids","matchesAny","actual","values","Array","isArray","some","element","includes","compareValues","left","right","Date","getTime","String","localeCompare","matchesNode","item","child","every","property","start","end","RegExp","test","never","applyFiltering","items","filtering","length","nodes","flatMap","entry","filter","applySorting","sorting","entries","record","Object","keys","map","Number","sort","priority","result","type","copyOf","entity","entityConstructor","succeed","assign","create","getPrototypeOf","makeInMemoryEntityRepository","seed","pendingFailure","guard","run","undefined","failure","fail","load","request","matched","page","pageSize","forEach","slice","pipe","copies","total","get","found","find","save","globalThis","crypto","randomUUID","tap","stored","index","findIndex","push","remove","entityOrId","delete","next","failNext","error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/doubles/entity-repository.ts"],"sourcesContent":["import type { EntityRepository } from '@entifix/business';\nimport {\n applyFiltering,\n applySorting,\n deserializeSingleEntity,\n EntifixConnError,\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n type EntityPage,\n extractMetaAccessors,\n serializeEntity,\n} from '@entifix/core';\nimport { Effect } from 'effect';\n\n/**\n * A copy of `entity` that shares no state with it.\n *\n * Through the entity's own mapping — serialize, then deserialize — because that\n * is what a real adapter does to every record on the way in and out: a caller\n * can never hold the instance the store keeps, and a member the mapping drops\n * (`hidden`, `readonly`) does not survive a save here either. A class that\n * declares no accessors has no mapping to go through, so it is copied member by\n * member instead; plain spec fixtures are the case this keeps working.\n */\nconst copyOf = <TEntity extends Entity>(\n entity: TEntity,\n): Effect.Effect<TEntity, EntifixError> => {\n const entityConstructor = entity.constructor as EntityConstructor<TEntity>;\n if (extractMetaAccessors(entityConstructor).length === 0) {\n return Effect.succeed(\n Object.assign(\n Object.create(Object.getPrototypeOf(entity) as object) as TEntity,\n entity,\n ),\n );\n }\n // `undefined` only comes back for `null`/`undefined` input, and\n // `serializeEntity` always hands over an object.\n return deserializeSingleEntity(\n entityConstructor,\n serializeEntity(entityConstructor, entity),\n ) as Effect.Effect<TEntity, EntifixError>;\n};\n\nexport interface InMemoryEntityRepository extends EntityRepository {\n /** Everything currently stored, in insertion order. */\n readonly items: Entity[];\n /** Replaces the contents, discarding whatever was there. */\n seed(items: Entity[]): void;\n /**\n * Makes the next call to any method fail with `error`, so error branches are\n * reachable without reaching for a mocking library.\n */\n failNext(error: EntifixError): void;\n}\n\n/**\n * In-memory {@link EntityRepository}: the default double for anything that\n * depends on persistence.\n *\n * Its semantics deliberately mirror `makeMongoRepository` — the same filtering\n * and sorting rules, which it takes from core's in-memory query evaluation\n * rather than keeping a copy of its own, same 1-based paging with a default page size of 10, same\n * \"entity not found\" failure, same id minting on create — and\n * `describeEntityRepositoryContract` holds both to that shared contract.\n */\nexport const makeInMemoryEntityRepository = (\n seed: Entity[] = [],\n): InMemoryEntityRepository => {\n let items = [...seed];\n let pendingFailure: EntifixError | undefined;\n\n /** Consumes a queued failure, if one was armed. */\n const guard = <TValue>(\n run: () => Effect.Effect<TValue, EntifixError>,\n ): Effect.Effect<TValue, EntifixError> => {\n if (pendingFailure !== undefined) {\n const failure = pendingFailure;\n pendingFailure = undefined;\n return Effect.fail(failure);\n }\n return run();\n };\n\n const load = <TEntity extends Entity>(request: EntityLoadRequest<TEntity>) =>\n guard(() => {\n const matched = applySorting(\n applyFiltering(items as TEntity[], request.filtering),\n request.sorting,\n );\n const page = request.page ?? 1;\n const pageSize = request.pageSize ?? 10;\n const start = (page - 1) * pageSize;\n\n return Effect.forEach(\n matched.slice(start, start + pageSize),\n copyOf,\n ).pipe(\n Effect.map(\n copies =>\n ({\n items: copies,\n total: matched.length,\n request,\n }) satisfies EntityPage<TEntity>,\n ),\n );\n });\n\n const get = <TEntity extends Entity>(id: EntityId) =>\n guard(() => {\n const found = items.find(item => item.id === id);\n return found === undefined\n ? Effect.fail(\n new EntifixConnError('Entity not found', undefined, { id }),\n )\n : copyOf(found as TEntity);\n });\n\n const save = <TEntity extends Entity>(entity: TEntity) =>\n guard(() => {\n // A create arrives without an id, so the store mints one — same rule the\n // Mongo adapter follows. The Web Crypto global rather than `node:crypto`,\n // because a browser bundle cannot resolve a `node:` import and an example\n // with no backend runs this repository in the page.\n entity.id = entity.id ?? globalThis.crypto.randomUUID();\n // The store keeps its own copy and hands back another, so neither the\n // caller's instance nor the returned one can change the record without a\n // second save — which a real adapter guarantees by construction.\n return copyOf(entity).pipe(\n Effect.tap(stored =>\n Effect.sync(() => {\n const index = items.findIndex(item => item.id === stored.id);\n if (index === -1) {\n items.push(stored);\n } else {\n items[index] = stored;\n }\n }),\n ),\n Effect.flatMap(copyOf),\n );\n });\n\n const remove = <TEntity extends Entity>(entityOrId: EntityId | TEntity) =>\n guard(() => {\n const id =\n entityOrId != null && typeof entityOrId === 'object'\n ? entityOrId.id\n : entityOrId;\n items = items.filter(item => item.id !== id);\n return Effect.succeed(undefined);\n });\n\n return {\n load,\n get,\n save,\n delete: remove,\n get items() {\n return items;\n },\n seed: (next: Entity[]) => {\n items = [...next];\n },\n failNext: (error: EntifixError) => {\n pendingFailure = error;\n },\n // Each method closes over its own state, so nothing is required from the\n // Effect environment; that is assignable to the interface's channel.\n } as unknown as InMemoryEntityRepository;\n};\n"],"names":["applyFiltering","applySorting","deserializeSingleEntity","EntifixConnError","extractMetaAccessors","serializeEntity","Effect","copyOf","entity","entityConstructor","length","succeed","Object","assign","create","getPrototypeOf","makeInMemoryEntityRepository","seed","items","pendingFailure","guard","run","undefined","failure","fail","load","request","matched","filtering","sorting","page","pageSize","start","forEach","slice","pipe","map","copies","total","get","id","found","find","item","save","globalThis","crypto","randomUUID","tap","stored","sync","index","findIndex","push","flatMap","remove","entityOrId","filter","delete","next","failNext","error"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-mongo.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-mongo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuMxC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACtD,6CAA6C;IAC7C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE;;;;OAIG;IACH,YAAY,IAAI;QACd,eAAe,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAM,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAM,KACpC,
|
|
1
|
+
{"version":3,"file":"fake-mongo.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-mongo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuMxC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACtD,6CAA6C;IAC7C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE;;;;OAIG;IACH,YAAY,IAAI;QACd,eAAe,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAM,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAM,KACpC,WAyhBF,CAAC"}
|
|
@@ -281,7 +281,9 @@ const matches = (doc, query)=>Object.entries(query).every(([field, condition])=>
|
|
|
281
281
|
const matched = documentsOf(name).filter((doc)=>matches(doc, query));
|
|
282
282
|
const sorted = Object.entries(sortSpec).reduceRight((items, [field, direction])=>[
|
|
283
283
|
...items
|
|
284
|
-
].sort((left, right)
|
|
284
|
+
].sort((left, right)=>// A dotted field sorts by the value it names, as the
|
|
285
|
+
// server does — one locale of a localized member, say.
|
|
286
|
+
compare(valuesAt(left, field)[0], valuesAt(right, field)[0]) * direction), matched);
|
|
285
287
|
return sorted.slice(skipCount, skipCount + limitCount).map((doc)=>project(doc, options));
|
|
286
288
|
})
|
|
287
289
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/drivers/fake-mongo.ts"],"sourcesContent":["/**\n * A fake of the mongodb driver, not of the repository port.\n *\n * It sits one level *below* `makeMongoRepository`, so the adapter's own code —\n * the filter translation, the projection, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would\n * skip all of it.\n *\n * Only the surface the adapter uses is implemented; anything else is a\n * deliberate `TypeError` rather than a silent no-op.\n */\n\n/** The subset of a Mongo query document the adapter produces. */\ntype QueryDocument = Record<string, unknown>;\ntype Document = Record<string, unknown>;\n\nconst isPlainObject = (value: unknown): value is Document =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst compare = (left: unknown, right: unknown): number => {\n if (left == null && right == null) return 0;\n if (left == null) return -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number')\n return left - right;\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n};\n\n/** Evaluates one `{ $op: value }` condition against a document field. */\nconst matchesCondition = (actual: unknown, condition: unknown): boolean => {\n if (!isPlainObject(condition)) {\n return actual === condition;\n }\n\n return Object.entries(condition).every(([operator, operand]) => {\n switch (operator) {\n case '$eq':\n return actual === operand;\n case '$ne':\n return actual !== operand;\n case '$gt':\n return compare(actual, operand) > 0;\n case '$gte':\n return compare(actual, operand) >= 0;\n case '$lt':\n return compare(actual, operand) < 0;\n case '$lte':\n return compare(actual, operand) <= 0;\n case '$in':\n return (operand as unknown[]).includes(actual);\n // Absence, not falsiness. A settlement ledger keys \"unpaid\" on a `runId`\n // that is not there at all, and `undefined` is what a document without\n // the field reads as — so `{ $exists: false }` must be a different test\n // from `{ $eq: undefined }` would be if the serializer ever wrote one.\n case '$exists':\n return (actual !== undefined) === (operand as boolean);\n case '$nin':\n return !(operand as unknown[]).includes(actual);\n case '$not':\n return !matchesCondition(actual, operand);\n case '$regex':\n return new RegExp(\n operand as string,\n (condition['$options'] as string) ?? '',\n ).test(String(actual));\n case '$options':\n // Handled together with `$regex`.\n return true;\n default:\n throw new TypeError(`fake mongo: unsupported operator \"${operator}\"`);\n }\n });\n};\n\n/**\n * Evaluates one node of an aggregation expression — the `$expr` half of a\n * query, which is a different language from the operators above: it compares\n * two *fields of the same document* rather than a field against a constant.\n *\n * Only the shape a conditional stock write sends is implemented — a\n * `$subtract` of two field paths compared with `$gte` — and anything else is a\n * `TypeError`. That is the same rule the rest of this file follows: a fake that\n * quietly evaluated an expression it does not understand would answer `true`\n * for a document the real server would have refused, which turns an\n * oversell-prevention test into a test that asserts nothing.\n */\nconst evaluateExpression = (doc: Document, node: unknown): unknown => {\n if (typeof node === 'string' && node.startsWith('$')) {\n return doc[node.slice(1)];\n }\n if (!isPlainObject(node)) {\n return node;\n }\n\n const entries = Object.entries(node);\n if (entries.length !== 1) {\n throw new TypeError(\n `fake mongo: $expr node must have exactly one operator, got ${entries.length}`,\n );\n }\n const [operator, operand] = entries[0];\n const operands = (operand as unknown[]).map(child =>\n evaluateExpression(doc, child),\n );\n\n switch (operator) {\n case '$subtract':\n return (operands[0] as number) - (operands[1] as number);\n case '$add':\n return (operands[0] as number) + (operands[1] as number);\n case '$gte':\n return compare(operands[0], operands[1]) >= 0;\n case '$gt':\n return compare(operands[0], operands[1]) > 0;\n case '$lte':\n return compare(operands[0], operands[1]) <= 0;\n case '$lt':\n return compare(operands[0], operands[1]) < 0;\n case '$eq':\n return operands[0] === operands[1];\n default:\n throw new TypeError(\n `fake mongo: unsupported $expr operator \"${operator}\"`,\n );\n }\n};\n\n/**\n * Every value a field path names, the way the server reads one.\n *\n * A dotted path descends, and **an array on the way down is walked\n * element-wise** — which is what makes `{'items.vendorId': x}` match an order\n * with a line for that vendor rather than nothing at all. Without it a query\n * into an embedded collection silently answers empty here while working against\n * a real server, which is worse than an unsupported-operator throw: the spec\n * passes and asserts the opposite of what it claims.\n *\n * The array itself is kept as a candidate beside its elements, because the\n * server matches both — `{ tags: 'a' }` matches `{ tags: ['a'] }` and\n * `{ tags: 'a' }` alike.\n */\nconst valuesAt = (doc: Document, field: string): unknown[] => {\n let current: unknown[] = [doc];\n for (const segment of field.split('.')) {\n const next: unknown[] = [];\n for (const value of current) {\n const container = Array.isArray(value) ? value : [value];\n for (const element of container) {\n if (isPlainObject(element)) {\n next.push(element[segment]);\n }\n }\n }\n current = next;\n }\n return current.flatMap(value =>\n Array.isArray(value) ? [value, ...value] : [value],\n );\n};\n\n/** Operators that exclude a document when a single value matches them. */\nconst NEGATIVE_OPERATORS = new Set(['$ne', '$nin', '$not']);\n\nconst isNegativeCondition = (condition: unknown): boolean =>\n isPlainObject(condition) &&\n Object.keys(condition).some(operator => NEGATIVE_OPERATORS.has(operator));\n\nconst matches = (doc: Document, query: QueryDocument): boolean =>\n Object.entries(query).every(([field, condition]) => {\n if (field === '$and') {\n return (condition as QueryDocument[]).every(child => matches(doc, child));\n }\n if (field === '$or') {\n return (condition as QueryDocument[]).some(child => matches(doc, child));\n }\n if (field === '$expr') {\n return evaluateExpression(doc, condition) === true;\n }\n const candidates = valuesAt(doc, field);\n if (candidates.length === 0) {\n return matchesCondition(undefined, condition);\n }\n // A positive condition matches when **any** value at the path satisfies it;\n // a negative one only when **every** value does. That asymmetry is the\n // server's: `{ 'items.vendorId': { $ne: 'a' } }` must exclude an order that\n // has a line for `a`, not include it because some other line differs.\n return isNegativeCondition(condition)\n ? candidates.every(value => matchesCondition(value, condition))\n : candidates.some(value => matchesCondition(value, condition));\n });\n\n/**\n * The error a real server raises when a write violates a unique index.\n *\n * The `code` matters and the message does not: callers recognise a duplicate\n * key by `error.code === 11000` (or the same code inside `writeErrors`), and a\n * fake that threw a plain `Error` would send a retry path that exists precisely\n * for this case down the \"unexpected failure\" branch instead.\n */\nclass FakeDuplicateKeyError extends Error {\n readonly code = 11000;\n\n constructor(collection: string, key: Document) {\n super(\n `fake mongo: E11000 duplicate key error collection: ${collection} dup key: ${JSON.stringify(key)}`,\n );\n this.name = 'MongoServerError';\n }\n}\n\nexport interface FakeMongoCollection {\n /** The documents currently stored, as the driver would return them. */\n readonly documents: Document[];\n}\n\nexport interface FakeMongoDb {\n /** Replaces a collection's contents. */\n seed(collection: string, documents: Document[]): void;\n /** Reads a collection's current contents. */\n read(collection: string): Document[];\n /**\n * Makes every subsequent driver call reject with `error`, so the adapter's\n * `EntifixConnError` mapping is reachable. Pass `undefined` to restore.\n */\n failWith(error: unknown): void;\n /**\n * Makes only `operation` reject, leaving the rest working — the way a read\n * that succeeds and a count that then fails reaches a distinct error branch.\n */\n failOn(operation: string, error: unknown): void;\n /** Every operation performed, for assertions about what the adapter did. */\n readonly operations: ReadonlyArray<{ collection: string; op: string }>;\n /**\n * Opens a session whose `withTransaction` rolls the whole store back when its\n * body throws. What a service hands to `client.startSession()`; see the\n * implementation for why it is atomicity and not isolation.\n */\n startSession(): {\n withTransaction(body: () => Promise<unknown>): Promise<unknown>;\n endSession(): Promise<void>;\n readonly hasEnded: boolean;\n };\n /** The object to hand to `makeMongoRepository` in place of a real `Db`. */\n readonly db: unknown;\n}\n\n/**\n * Builds an in-memory stand-in for a mongodb `Db`.\n *\n * `db` is typed `unknown` on purpose: casting it at the call site keeps this\n * package free of a hard dependency on the `mongodb` types while remaining\n * honest that it is not a real driver.\n */\nexport const makeFakeMongoDb = (\n seed: Record<string, Document[]> = {},\n): FakeMongoDb => {\n const collections = new Map<string, Document[]>(\n Object.entries(seed).map(([name, docs]) => [\n name,\n docs.map(d => ({ ...d })),\n ]),\n );\n const operations: Array<{ collection: string; op: string }> = [];\n let failure: unknown;\n const failuresByOperation = new Map<string, unknown>();\n\n const documentsOf = (name: string): Document[] => {\n if (!collections.has(name)) collections.set(name, []);\n return collections.get(name) as Document[];\n };\n\n /**\n * The unique indexes `createIndex` has been asked for, per collection, as the\n * field lists they key on.\n *\n * Recording them is not bookkeeping: `updateOne` with `upsert` is atomic per\n * *document*, so two concurrent upserts for the same key each fail to see the\n * other's uncommitted insert and each create a row. The index is what makes\n * the fold singular, and a fake that accepted `createIndex` as a no-op would\n * report a service as covered while the property the index exists for went\n * untested (ADR 0010).\n */\n const uniqueIndexes = new Map<string, string[][]>();\n\n const uniqueIndexesOf = (name: string): string[][] =>\n uniqueIndexes.get(name) ?? [];\n\n /**\n * Refuses `candidate` if it would duplicate an existing document under one of\n * the collection's unique indexes.\n *\n * `ignored` is the document being replaced by an update, which must not count\n * as its own duplicate. A key whose value is `undefined` on the candidate is\n * skipped: a real index treats a missing field as `null` and would collide,\n * but nothing in this repo indexes an optional member, and guessing wrong\n * here would fail writes the server accepts.\n */\n const assertUnique = (\n name: string,\n candidate: Document,\n ignored?: Document,\n ): void => {\n for (const fields of uniqueIndexesOf(name)) {\n if (fields.some(field => candidate[field] === undefined)) continue;\n const clash = documentsOf(name).find(\n doc =>\n doc !== ignored &&\n fields.every(field => doc[field] === candidate[field]),\n );\n if (clash !== undefined) {\n throw new FakeDuplicateKeyError(\n name,\n Object.fromEntries(fields.map(field => [field, candidate[field]])),\n );\n }\n }\n };\n\n const record = <TValue>(name: string, op: string, produce: () => TValue) => {\n operations.push({ collection: name, op });\n const scoped = failuresByOperation.get(op);\n if (scoped !== undefined) return Promise.reject(scoped);\n if (failure !== undefined) return Promise.reject(failure);\n // `try`/`catch` rather than `Promise.resolve(produce())`: a real driver\n // returns a promise and *rejects*, so a fake that threw synchronously would\n // need different handling at every call site than the thing it stands in for.\n try {\n return Promise.resolve(produce());\n } catch (error) {\n return Promise.reject(error);\n }\n };\n\n /** Applies `{ projection: { _id: 0 } }`, the only projection the adapter uses. */\n const project = (doc: Document, options?: { projection?: Document }) => {\n const copy = { ...doc };\n if (options?.projection && options.projection['_id'] === 0) {\n delete copy['_id'];\n }\n return copy;\n };\n\n const collection = (name: string) => ({\n find: (query: QueryDocument = {}, options?: { projection?: Document }) => {\n let sortSpec: Record<string, 1 | -1> = {};\n let skipCount = 0;\n let limitCount = Infinity;\n\n const cursor = {\n sort(spec: Record<string, 1 | -1>) {\n sortSpec = spec;\n return cursor;\n },\n skip(count: number) {\n skipCount = count;\n return cursor;\n },\n limit(count: number) {\n limitCount = count;\n return cursor;\n },\n toArray: () =>\n record(name, 'find', () => {\n const matched = documentsOf(name).filter(doc =>\n matches(doc, query),\n );\n const sorted = Object.entries(sortSpec).reduceRight(\n (items, [field, direction]) =>\n [...items].sort(\n (left, right) =>\n compare(left[field], right[field]) * direction,\n ),\n matched,\n );\n return sorted\n .slice(skipCount, skipCount + limitCount)\n .map(doc => project(doc, options));\n }),\n };\n return cursor;\n },\n\n findOne: (query: QueryDocument, options?: { projection?: Document }) =>\n record(name, 'findOne', () => {\n const found = documentsOf(name).find(doc => matches(doc, query));\n return found === undefined ? null : project(found, options);\n }),\n\n countDocuments: (query: QueryDocument = {}) =>\n record(\n name,\n 'countDocuments',\n () => documentsOf(name).filter(doc => matches(doc, query)).length,\n ),\n\n // Used by the services' own seeding, which the e2e `mock` profile runs for\n // real so both profiles serve the same catalog.\n insertMany: (documents: Document[]) =>\n record(name, 'insertMany', () => {\n const stored = documentsOf(name);\n for (const document of documents) {\n assertUnique(name, document);\n stored.push({ ...document });\n }\n return { insertedCount: documents.length };\n }),\n\n /**\n * A single insert — how an append-only ledger grows, and how a hold is\n * written beside the counter it moved.\n *\n * The options argument is accepted and ignored except for its `session`,\n * which the transaction wrapper already handles: a caller that passes one\n * is inside `withTransaction`, and this fake's rollback is a snapshot of\n * the whole store rather than anything per-operation.\n */\n insertOne: (document: Document) =>\n record(name, 'insertOne', () => {\n assertUnique(name, document);\n documentsOf(name).push({ ...document });\n return { acknowledged: true, insertedId: document['id'] ?? null };\n }),\n\n replaceOne: (\n query: QueryDocument,\n replacement: Document,\n options?: { upsert?: boolean },\n ) =>\n record(name, 'replaceOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) {\n if (!options?.upsert) return { matchedCount: 0, upsertedCount: 0 };\n documents.push({ ...replacement });\n return { matchedCount: 0, upsertedCount: 1 };\n }\n documents[index] = { ...replacement };\n return { matchedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * Partial update. `$set`, `$addToSet`, `$inc` and `$setOnInsert` are\n * implemented — the operators the adapters and the stock ledger use — and\n * **anything else throws**, because a fake that silently accepted `$unset`\n * without applying it would let a spec pass on an update that never\n * happened.\n *\n * `$addToSet` is what links an identifier onto `user-identity.identifiers`\n * without duplicating it on a repair run; supporting it here rather than\n * ignoring it is the difference between the e2e proving the link and merely\n * not noticing its absence.\n *\n * `$inc` with `upsert` is how a quantity moves. It matters that it is a\n * real increment and not a read-modify-write even here: the whole rule it\n * implements is that an absolute-value write loses updates, so a fake that\n * computed the new total from a value it had already read would model the\n * bug rather than the fix.\n *\n * `$push` appends, and a `$set` key of the form `field.$[name].member`\n * applies to every element of `field` that `options.arrayFilters` selects —\n * the pair a saga store records an outcome with and later flags one\n * compensated. Both are what keep an array update from being a\n * read-modify-write, which is the point of using them.\n */\n updateOne: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n $push?: Document;\n },\n options?: { upsert?: boolean; arrayFilters?: readonly Document[] },\n ) =>\n record(name, 'updateOne', () => {\n const supported = [\n '$set',\n '$addToSet',\n '$inc',\n '$setOnInsert',\n '$push',\n ];\n const unsupported = Object.keys(update).filter(\n operator => !supported.includes(operator),\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateOne does not implement ${unsupported.join(', ')}`,\n );\n }\n\n // A real server rejects an update whose operators both name one field,\n // and the message is the one this repro'd against: `$setOnInsert` that\n // also named a field `$inc` touches. Reproducing the refusal is what\n // keeps the fake from blessing an update Mongo would refuse.\n const conflicting = Object.keys(update.$setOnInsert ?? {}).filter(\n field => field in (update.$inc ?? {}) || field in (update.$set ?? {}),\n );\n if (conflicting.length > 0) {\n throw new Error(\n `fake-mongo: updateOne has conflicting paths in $setOnInsert and $inc/$set: ${conflicting.join(', ')}`,\n );\n }\n\n /** `$set`, `$addToSet`, `$inc` and `$push`, which apply on both branches. */\n const applyOperators = (base: Document): Document => {\n const plain = Object.entries(update.$set ?? {}).filter(\n ([field]) => !field.includes('.$['),\n );\n const updated = { ...base, ...Object.fromEntries(plain) };\n for (const [path, value] of Object.entries(update.$set ?? {})) {\n const filtered = /^([^.]+)\\.\\$\\[([^\\]]+)\\]\\.(.+)$/.exec(path);\n if (filtered === null) continue;\n const [, field, identifier, member] = filtered as unknown as [\n string,\n string,\n string,\n string,\n ];\n // The server refuses a filtered position with no filter naming it.\n const conditions = (options?.arrayFilters ?? []).flatMap(filter =>\n Object.entries(filter)\n .filter(([key]) => key.startsWith(`${identifier}.`))\n .map(\n ([key, condition]) =>\n [key.slice(identifier.length + 1), condition] as const,\n ),\n );\n if (conditions.length === 0) {\n throw new Error(\n `fake-mongo: no array filter found for identifier ${identifier}`,\n );\n }\n const current = updated[field];\n updated[field] = (Array.isArray(current) ? current : []).map(\n element =>\n isPlainObject(element) &&\n conditions.every(([key, condition]) =>\n matchesCondition(element[key], condition),\n )\n ? { ...element, [member]: value }\n : element,\n );\n }\n for (const [field, value] of Object.entries(update.$push ?? {})) {\n const current = updated[field];\n updated[field] = [\n ...(Array.isArray(current) ? current : []),\n value,\n ];\n }\n for (const [field, value] of Object.entries(update.$addToSet ?? {})) {\n const current = updated[field];\n const array = Array.isArray(current) ? current : [];\n if (!array.includes(value)) {\n updated[field] = [...array, value];\n }\n }\n for (const [field, delta] of Object.entries(update.$inc ?? {})) {\n // An absent field is created at the increment's value, which is\n // exactly why `$setOnInsert` must not also name it.\n const current = updated[field];\n updated[field] =\n (typeof current === 'number' ? current : 0) + (delta as number);\n }\n return updated;\n };\n\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n if (index === -1) {\n if (options?.upsert !== true) {\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 0 };\n }\n // The upserted document starts from the query's *equality* fields,\n // the way a real server seeds one — an operator condition contributes\n // nothing, which is why a `$expr`-guarded upsert would insert a row\n // missing the very field it filtered on.\n const seedFields = Object.fromEntries(\n Object.entries(query).filter(\n ([field, condition]) =>\n !field.startsWith('$') && !isPlainObject(condition),\n ),\n );\n const inserted = applyOperators({\n ...seedFields,\n ...(update.$setOnInsert ?? {}),\n });\n assertUnique(name, inserted);\n documents.push(inserted);\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 1 };\n }\n\n const updated = applyOperators(documents[index]);\n assertUnique(name, updated, documents[index]);\n documents[index] = updated;\n return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * `$set` across every matching document, and the count of what changed.\n *\n * ⚠️ **The count is what a caller branches on.** A settlement run claims its\n * ledger lines with this and reads `modifiedCount === 0` as \"another replica\n * took them all\" — so a fake returning a fixed `1`, or the match count\n * rather than the modified count, would make the losing replica behave like\n * the winning one and the conditional claim would test nothing.\n *\n * `$set` only. Anything else throws, for the reason `updateOne` gives:\n * silently accepting an operator it does not apply lets a spec pass on an\n * update that never happened.\n */\n updateMany: (query: QueryDocument, update: { $set?: Document }) =>\n record(name, 'updateMany', () => {\n const unsupported = Object.keys(update).filter(\n operator => operator !== '$set',\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateMany does not implement ${unsupported.join(', ')}`,\n );\n }\n\n const documents = documentsOf(name);\n let modifiedCount = 0;\n documents.forEach((doc, index) => {\n if (!matches(doc, query)) return;\n const updated = { ...doc, ...(update.$set ?? {}) };\n assertUnique(name, updated, doc);\n documents[index] = updated;\n modifiedCount += 1;\n });\n return { matchedCount: modifiedCount, modifiedCount };\n }),\n\n /**\n * Update one document and hand back the result, upserting when asked.\n *\n * ⚠️ **`returnDocument: 'after'` is the only mode implemented, and the\n * default here is `'after'` rather than the driver's `'before'`** — because\n * the callers that need this primitive at all need the merged document. A\n * two-message join upserts its own half and then asks \"is the pair complete\n * now?\", which is a question only the *after* state answers. `'before'`\n * throws rather than quietly returning the wrong half.\n *\n * Implemented over {@link updateOne} so the upsert seeding rule, the\n * conflicting-path refusal and the unique-index enforcement are the same\n * ones, rather than a second copy that could drift from them.\n */\n findOneAndUpdate: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n $push?: Document;\n },\n options?: { upsert?: boolean; returnDocument?: 'before' | 'after' },\n ) =>\n record(name, 'findOneAndUpdate', async () => {\n if (options?.returnDocument === 'before') {\n throw new Error(\n 'fake-mongo: findOneAndUpdate only implements returnDocument: \"after\"',\n );\n }\n // Where the document is *before* the write, because afterwards the\n // query may no longer find it: an upsert's operator conditions need not\n // hold of the row it just inserted — `{ folded: { $exists: false } }`\n // matches nothing once `folded` has been set. `documentsOf` hands back\n // the live array, which `updateOne` mutates in place or pushes onto, so\n // the position is enough to identify it either way.\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n const result = await collection(name).updateOne(query, update, {\n upsert: options?.upsert === true,\n });\n if (result.matchedCount === 0 && result.upsertedCount === 0) {\n return null;\n }\n\n return index === -1\n ? documents[documents.length - 1]\n : documents[index];\n }),\n\n deleteOne: (query: QueryDocument) =>\n record(name, 'deleteOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) return { deletedCount: 0 };\n documents.splice(index, 1);\n return { deletedCount: 1 };\n }),\n\n /**\n * Declares an index, and **enforces it when it is unique**.\n *\n * Idempotent, like the real one: a service that ensures its index per\n * request rather than at boot — because a tenant database only appears on\n * first write — calls this on every write.\n */\n createIndex: (spec: Document, options?: Document) =>\n record(name, 'createIndex', () => {\n if (options?.['unique'] === true) {\n const fields = Object.keys(spec);\n const existing = uniqueIndexes.get(name) ?? [];\n const already = existing.some(\n candidate =>\n candidate.length === fields.length &&\n candidate.every((field, at) => field === fields[at]),\n );\n if (!already) {\n uniqueIndexes.set(name, [...existing, fields]);\n }\n }\n return { spec, options };\n }),\n });\n\n /**\n * A session whose `withTransaction` really rolls back.\n *\n * ⚠️ **It is atomicity, not isolation, and the difference decides where a\n * test may live.** The store is a single-threaded object graph, so nothing\n * runs concurrently with the body and a \"race\" here resolves in the order it\n * was written: a concurrency property — two writers contending for the last\n * unit — is green under this fake no matter what the code does, and belongs\n * in a `*.live.spec.ts` against real Mongo (ADR 0010).\n *\n * What it *does* model is the abort path, which is worth having: a body that\n * throws leaves the store exactly as it found it, so a conditional write that\n * moves a counter and then fails to write its row cannot leave the counter\n * moved. That is a real bug shape, and one a fake without rollback would hide\n * by committing half of it.\n */\n const startSession = () => {\n let open = true;\n return {\n withTransaction: async (body: () => Promise<unknown>) => {\n const snapshot = new Map(\n [...collections].map(([name, docs]) => [\n name,\n docs.map(doc => ({ ...doc })),\n ]),\n );\n try {\n return await body();\n } catch (error) {\n collections.clear();\n for (const [name, docs] of snapshot) collections.set(name, docs);\n throw error;\n }\n },\n endSession: () => {\n open = false;\n return Promise.resolve();\n },\n get hasEnded() {\n return !open;\n },\n };\n };\n\n return {\n seed: (name, documents) => {\n collections.set(\n name,\n documents.map(doc => ({ ...doc })),\n );\n },\n read: name => documentsOf(name).map(doc => ({ ...doc })),\n failWith: error => {\n failure = error;\n },\n failOn: (operation, error) => {\n failuresByOperation.set(operation, error);\n },\n get operations() {\n return operations;\n },\n startSession,\n db: { collection },\n };\n};\n"],"names":["isPlainObject","value","Array","isArray","compare","left","right","Date","getTime","String","localeCompare","matchesCondition","actual","condition","Object","entries","every","operator","operand","includes","undefined","RegExp","test","TypeError","evaluateExpression","doc","node","startsWith","slice","length","operands","map","child","valuesAt","field","current","segment","split","next","container","element","push","flatMap","NEGATIVE_OPERATORS","Set","isNegativeCondition","keys","some","has","matches","query","candidates","FakeDuplicateKeyError","Error","collection","key","JSON","stringify","code","name","makeFakeMongoDb","seed","collections","Map","docs","d","operations","failure","failuresByOperation","documentsOf","set","get","uniqueIndexes","uniqueIndexesOf","assertUnique","candidate","ignored","fields","clash","find","fromEntries","record","op","produce","scoped","Promise","reject","resolve","error","project","options","copy","projection","sortSpec","skipCount","limitCount","Infinity","cursor","sort","spec","skip","count","limit","toArray","matched","filter","sorted","reduceRight","items","direction","findOne","found","countDocuments","insertMany","documents","stored","document","insertedCount","insertOne","acknowledged","insertedId","replaceOne","replacement","index","findIndex","upsert","matchedCount","upsertedCount","updateOne","update","supported","unsupported","join","conflicting","$setOnInsert","$inc","$set","applyOperators","base","plain","updated","path","filtered","exec","identifier","member","conditions","arrayFilters","$push","$addToSet","array","delta","modifiedCount","seedFields","inserted","updateMany","forEach","findOneAndUpdate","returnDocument","result","deleteOne","deletedCount","splice","createIndex","existing","already","at","startSession","open","withTransaction","body","snapshot","clear","endSession","hasEnded","read","failWith","failOn","operation","db"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/drivers/fake-mongo.ts"],"sourcesContent":["/**\n * A fake of the mongodb driver, not of the repository port.\n *\n * It sits one level *below* `makeMongoRepository`, so the adapter's own code —\n * the filter translation, the projection, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would\n * skip all of it.\n *\n * Only the surface the adapter uses is implemented; anything else is a\n * deliberate `TypeError` rather than a silent no-op.\n */\n\n/** The subset of a Mongo query document the adapter produces. */\ntype QueryDocument = Record<string, unknown>;\ntype Document = Record<string, unknown>;\n\nconst isPlainObject = (value: unknown): value is Document =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst compare = (left: unknown, right: unknown): number => {\n if (left == null && right == null) return 0;\n if (left == null) return -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number')\n return left - right;\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n};\n\n/** Evaluates one `{ $op: value }` condition against a document field. */\nconst matchesCondition = (actual: unknown, condition: unknown): boolean => {\n if (!isPlainObject(condition)) {\n return actual === condition;\n }\n\n return Object.entries(condition).every(([operator, operand]) => {\n switch (operator) {\n case '$eq':\n return actual === operand;\n case '$ne':\n return actual !== operand;\n case '$gt':\n return compare(actual, operand) > 0;\n case '$gte':\n return compare(actual, operand) >= 0;\n case '$lt':\n return compare(actual, operand) < 0;\n case '$lte':\n return compare(actual, operand) <= 0;\n case '$in':\n return (operand as unknown[]).includes(actual);\n // Absence, not falsiness. A settlement ledger keys \"unpaid\" on a `runId`\n // that is not there at all, and `undefined` is what a document without\n // the field reads as — so `{ $exists: false }` must be a different test\n // from `{ $eq: undefined }` would be if the serializer ever wrote one.\n case '$exists':\n return (actual !== undefined) === (operand as boolean);\n case '$nin':\n return !(operand as unknown[]).includes(actual);\n case '$not':\n return !matchesCondition(actual, operand);\n case '$regex':\n return new RegExp(\n operand as string,\n (condition['$options'] as string) ?? '',\n ).test(String(actual));\n case '$options':\n // Handled together with `$regex`.\n return true;\n default:\n throw new TypeError(`fake mongo: unsupported operator \"${operator}\"`);\n }\n });\n};\n\n/**\n * Evaluates one node of an aggregation expression — the `$expr` half of a\n * query, which is a different language from the operators above: it compares\n * two *fields of the same document* rather than a field against a constant.\n *\n * Only the shape a conditional stock write sends is implemented — a\n * `$subtract` of two field paths compared with `$gte` — and anything else is a\n * `TypeError`. That is the same rule the rest of this file follows: a fake that\n * quietly evaluated an expression it does not understand would answer `true`\n * for a document the real server would have refused, which turns an\n * oversell-prevention test into a test that asserts nothing.\n */\nconst evaluateExpression = (doc: Document, node: unknown): unknown => {\n if (typeof node === 'string' && node.startsWith('$')) {\n return doc[node.slice(1)];\n }\n if (!isPlainObject(node)) {\n return node;\n }\n\n const entries = Object.entries(node);\n if (entries.length !== 1) {\n throw new TypeError(\n `fake mongo: $expr node must have exactly one operator, got ${entries.length}`,\n );\n }\n const [operator, operand] = entries[0];\n const operands = (operand as unknown[]).map(child =>\n evaluateExpression(doc, child),\n );\n\n switch (operator) {\n case '$subtract':\n return (operands[0] as number) - (operands[1] as number);\n case '$add':\n return (operands[0] as number) + (operands[1] as number);\n case '$gte':\n return compare(operands[0], operands[1]) >= 0;\n case '$gt':\n return compare(operands[0], operands[1]) > 0;\n case '$lte':\n return compare(operands[0], operands[1]) <= 0;\n case '$lt':\n return compare(operands[0], operands[1]) < 0;\n case '$eq':\n return operands[0] === operands[1];\n default:\n throw new TypeError(\n `fake mongo: unsupported $expr operator \"${operator}\"`,\n );\n }\n};\n\n/**\n * Every value a field path names, the way the server reads one.\n *\n * A dotted path descends, and **an array on the way down is walked\n * element-wise** — which is what makes `{'items.vendorId': x}` match an order\n * with a line for that vendor rather than nothing at all. Without it a query\n * into an embedded collection silently answers empty here while working against\n * a real server, which is worse than an unsupported-operator throw: the spec\n * passes and asserts the opposite of what it claims.\n *\n * The array itself is kept as a candidate beside its elements, because the\n * server matches both — `{ tags: 'a' }` matches `{ tags: ['a'] }` and\n * `{ tags: 'a' }` alike.\n */\nconst valuesAt = (doc: Document, field: string): unknown[] => {\n let current: unknown[] = [doc];\n for (const segment of field.split('.')) {\n const next: unknown[] = [];\n for (const value of current) {\n const container = Array.isArray(value) ? value : [value];\n for (const element of container) {\n if (isPlainObject(element)) {\n next.push(element[segment]);\n }\n }\n }\n current = next;\n }\n return current.flatMap(value =>\n Array.isArray(value) ? [value, ...value] : [value],\n );\n};\n\n/** Operators that exclude a document when a single value matches them. */\nconst NEGATIVE_OPERATORS = new Set(['$ne', '$nin', '$not']);\n\nconst isNegativeCondition = (condition: unknown): boolean =>\n isPlainObject(condition) &&\n Object.keys(condition).some(operator => NEGATIVE_OPERATORS.has(operator));\n\nconst matches = (doc: Document, query: QueryDocument): boolean =>\n Object.entries(query).every(([field, condition]) => {\n if (field === '$and') {\n return (condition as QueryDocument[]).every(child => matches(doc, child));\n }\n if (field === '$or') {\n return (condition as QueryDocument[]).some(child => matches(doc, child));\n }\n if (field === '$expr') {\n return evaluateExpression(doc, condition) === true;\n }\n const candidates = valuesAt(doc, field);\n if (candidates.length === 0) {\n return matchesCondition(undefined, condition);\n }\n // A positive condition matches when **any** value at the path satisfies it;\n // a negative one only when **every** value does. That asymmetry is the\n // server's: `{ 'items.vendorId': { $ne: 'a' } }` must exclude an order that\n // has a line for `a`, not include it because some other line differs.\n return isNegativeCondition(condition)\n ? candidates.every(value => matchesCondition(value, condition))\n : candidates.some(value => matchesCondition(value, condition));\n });\n\n/**\n * The error a real server raises when a write violates a unique index.\n *\n * The `code` matters and the message does not: callers recognise a duplicate\n * key by `error.code === 11000` (or the same code inside `writeErrors`), and a\n * fake that threw a plain `Error` would send a retry path that exists precisely\n * for this case down the \"unexpected failure\" branch instead.\n */\nclass FakeDuplicateKeyError extends Error {\n readonly code = 11000;\n\n constructor(collection: string, key: Document) {\n super(\n `fake mongo: E11000 duplicate key error collection: ${collection} dup key: ${JSON.stringify(key)}`,\n );\n this.name = 'MongoServerError';\n }\n}\n\nexport interface FakeMongoCollection {\n /** The documents currently stored, as the driver would return them. */\n readonly documents: Document[];\n}\n\nexport interface FakeMongoDb {\n /** Replaces a collection's contents. */\n seed(collection: string, documents: Document[]): void;\n /** Reads a collection's current contents. */\n read(collection: string): Document[];\n /**\n * Makes every subsequent driver call reject with `error`, so the adapter's\n * `EntifixConnError` mapping is reachable. Pass `undefined` to restore.\n */\n failWith(error: unknown): void;\n /**\n * Makes only `operation` reject, leaving the rest working — the way a read\n * that succeeds and a count that then fails reaches a distinct error branch.\n */\n failOn(operation: string, error: unknown): void;\n /** Every operation performed, for assertions about what the adapter did. */\n readonly operations: ReadonlyArray<{ collection: string; op: string }>;\n /**\n * Opens a session whose `withTransaction` rolls the whole store back when its\n * body throws. What a service hands to `client.startSession()`; see the\n * implementation for why it is atomicity and not isolation.\n */\n startSession(): {\n withTransaction(body: () => Promise<unknown>): Promise<unknown>;\n endSession(): Promise<void>;\n readonly hasEnded: boolean;\n };\n /** The object to hand to `makeMongoRepository` in place of a real `Db`. */\n readonly db: unknown;\n}\n\n/**\n * Builds an in-memory stand-in for a mongodb `Db`.\n *\n * `db` is typed `unknown` on purpose: casting it at the call site keeps this\n * package free of a hard dependency on the `mongodb` types while remaining\n * honest that it is not a real driver.\n */\nexport const makeFakeMongoDb = (\n seed: Record<string, Document[]> = {},\n): FakeMongoDb => {\n const collections = new Map<string, Document[]>(\n Object.entries(seed).map(([name, docs]) => [\n name,\n docs.map(d => ({ ...d })),\n ]),\n );\n const operations: Array<{ collection: string; op: string }> = [];\n let failure: unknown;\n const failuresByOperation = new Map<string, unknown>();\n\n const documentsOf = (name: string): Document[] => {\n if (!collections.has(name)) collections.set(name, []);\n return collections.get(name) as Document[];\n };\n\n /**\n * The unique indexes `createIndex` has been asked for, per collection, as the\n * field lists they key on.\n *\n * Recording them is not bookkeeping: `updateOne` with `upsert` is atomic per\n * *document*, so two concurrent upserts for the same key each fail to see the\n * other's uncommitted insert and each create a row. The index is what makes\n * the fold singular, and a fake that accepted `createIndex` as a no-op would\n * report a service as covered while the property the index exists for went\n * untested (ADR 0010).\n */\n const uniqueIndexes = new Map<string, string[][]>();\n\n const uniqueIndexesOf = (name: string): string[][] =>\n uniqueIndexes.get(name) ?? [];\n\n /**\n * Refuses `candidate` if it would duplicate an existing document under one of\n * the collection's unique indexes.\n *\n * `ignored` is the document being replaced by an update, which must not count\n * as its own duplicate. A key whose value is `undefined` on the candidate is\n * skipped: a real index treats a missing field as `null` and would collide,\n * but nothing in this repo indexes an optional member, and guessing wrong\n * here would fail writes the server accepts.\n */\n const assertUnique = (\n name: string,\n candidate: Document,\n ignored?: Document,\n ): void => {\n for (const fields of uniqueIndexesOf(name)) {\n if (fields.some(field => candidate[field] === undefined)) continue;\n const clash = documentsOf(name).find(\n doc =>\n doc !== ignored &&\n fields.every(field => doc[field] === candidate[field]),\n );\n if (clash !== undefined) {\n throw new FakeDuplicateKeyError(\n name,\n Object.fromEntries(fields.map(field => [field, candidate[field]])),\n );\n }\n }\n };\n\n const record = <TValue>(name: string, op: string, produce: () => TValue) => {\n operations.push({ collection: name, op });\n const scoped = failuresByOperation.get(op);\n if (scoped !== undefined) return Promise.reject(scoped);\n if (failure !== undefined) return Promise.reject(failure);\n // `try`/`catch` rather than `Promise.resolve(produce())`: a real driver\n // returns a promise and *rejects*, so a fake that threw synchronously would\n // need different handling at every call site than the thing it stands in for.\n try {\n return Promise.resolve(produce());\n } catch (error) {\n return Promise.reject(error);\n }\n };\n\n /** Applies `{ projection: { _id: 0 } }`, the only projection the adapter uses. */\n const project = (doc: Document, options?: { projection?: Document }) => {\n const copy = { ...doc };\n if (options?.projection && options.projection['_id'] === 0) {\n delete copy['_id'];\n }\n return copy;\n };\n\n const collection = (name: string) => ({\n find: (query: QueryDocument = {}, options?: { projection?: Document }) => {\n let sortSpec: Record<string, 1 | -1> = {};\n let skipCount = 0;\n let limitCount = Infinity;\n\n const cursor = {\n sort(spec: Record<string, 1 | -1>) {\n sortSpec = spec;\n return cursor;\n },\n skip(count: number) {\n skipCount = count;\n return cursor;\n },\n limit(count: number) {\n limitCount = count;\n return cursor;\n },\n toArray: () =>\n record(name, 'find', () => {\n const matched = documentsOf(name).filter(doc =>\n matches(doc, query),\n );\n const sorted = Object.entries(sortSpec).reduceRight(\n (items, [field, direction]) =>\n [...items].sort(\n (left, right) =>\n // A dotted field sorts by the value it names, as the\n // server does — one locale of a localized member, say.\n compare(\n valuesAt(left, field)[0],\n valuesAt(right, field)[0],\n ) * direction,\n ),\n matched,\n );\n return sorted\n .slice(skipCount, skipCount + limitCount)\n .map(doc => project(doc, options));\n }),\n };\n return cursor;\n },\n\n findOne: (query: QueryDocument, options?: { projection?: Document }) =>\n record(name, 'findOne', () => {\n const found = documentsOf(name).find(doc => matches(doc, query));\n return found === undefined ? null : project(found, options);\n }),\n\n countDocuments: (query: QueryDocument = {}) =>\n record(\n name,\n 'countDocuments',\n () => documentsOf(name).filter(doc => matches(doc, query)).length,\n ),\n\n // Used by the services' own seeding, which the e2e `mock` profile runs for\n // real so both profiles serve the same catalog.\n insertMany: (documents: Document[]) =>\n record(name, 'insertMany', () => {\n const stored = documentsOf(name);\n for (const document of documents) {\n assertUnique(name, document);\n stored.push({ ...document });\n }\n return { insertedCount: documents.length };\n }),\n\n /**\n * A single insert — how an append-only ledger grows, and how a hold is\n * written beside the counter it moved.\n *\n * The options argument is accepted and ignored except for its `session`,\n * which the transaction wrapper already handles: a caller that passes one\n * is inside `withTransaction`, and this fake's rollback is a snapshot of\n * the whole store rather than anything per-operation.\n */\n insertOne: (document: Document) =>\n record(name, 'insertOne', () => {\n assertUnique(name, document);\n documentsOf(name).push({ ...document });\n return { acknowledged: true, insertedId: document['id'] ?? null };\n }),\n\n replaceOne: (\n query: QueryDocument,\n replacement: Document,\n options?: { upsert?: boolean },\n ) =>\n record(name, 'replaceOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) {\n if (!options?.upsert) return { matchedCount: 0, upsertedCount: 0 };\n documents.push({ ...replacement });\n return { matchedCount: 0, upsertedCount: 1 };\n }\n documents[index] = { ...replacement };\n return { matchedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * Partial update. `$set`, `$addToSet`, `$inc` and `$setOnInsert` are\n * implemented — the operators the adapters and the stock ledger use — and\n * **anything else throws**, because a fake that silently accepted `$unset`\n * without applying it would let a spec pass on an update that never\n * happened.\n *\n * `$addToSet` is what links an identifier onto `user-identity.identifiers`\n * without duplicating it on a repair run; supporting it here rather than\n * ignoring it is the difference between the e2e proving the link and merely\n * not noticing its absence.\n *\n * `$inc` with `upsert` is how a quantity moves. It matters that it is a\n * real increment and not a read-modify-write even here: the whole rule it\n * implements is that an absolute-value write loses updates, so a fake that\n * computed the new total from a value it had already read would model the\n * bug rather than the fix.\n *\n * `$push` appends, and a `$set` key of the form `field.$[name].member`\n * applies to every element of `field` that `options.arrayFilters` selects —\n * the pair a saga store records an outcome with and later flags one\n * compensated. Both are what keep an array update from being a\n * read-modify-write, which is the point of using them.\n */\n updateOne: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n $push?: Document;\n },\n options?: { upsert?: boolean; arrayFilters?: readonly Document[] },\n ) =>\n record(name, 'updateOne', () => {\n const supported = [\n '$set',\n '$addToSet',\n '$inc',\n '$setOnInsert',\n '$push',\n ];\n const unsupported = Object.keys(update).filter(\n operator => !supported.includes(operator),\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateOne does not implement ${unsupported.join(', ')}`,\n );\n }\n\n // A real server rejects an update whose operators both name one field,\n // and the message is the one this repro'd against: `$setOnInsert` that\n // also named a field `$inc` touches. Reproducing the refusal is what\n // keeps the fake from blessing an update Mongo would refuse.\n const conflicting = Object.keys(update.$setOnInsert ?? {}).filter(\n field => field in (update.$inc ?? {}) || field in (update.$set ?? {}),\n );\n if (conflicting.length > 0) {\n throw new Error(\n `fake-mongo: updateOne has conflicting paths in $setOnInsert and $inc/$set: ${conflicting.join(', ')}`,\n );\n }\n\n /** `$set`, `$addToSet`, `$inc` and `$push`, which apply on both branches. */\n const applyOperators = (base: Document): Document => {\n const plain = Object.entries(update.$set ?? {}).filter(\n ([field]) => !field.includes('.$['),\n );\n const updated = { ...base, ...Object.fromEntries(plain) };\n for (const [path, value] of Object.entries(update.$set ?? {})) {\n const filtered = /^([^.]+)\\.\\$\\[([^\\]]+)\\]\\.(.+)$/.exec(path);\n if (filtered === null) continue;\n const [, field, identifier, member] = filtered as unknown as [\n string,\n string,\n string,\n string,\n ];\n // The server refuses a filtered position with no filter naming it.\n const conditions = (options?.arrayFilters ?? []).flatMap(filter =>\n Object.entries(filter)\n .filter(([key]) => key.startsWith(`${identifier}.`))\n .map(\n ([key, condition]) =>\n [key.slice(identifier.length + 1), condition] as const,\n ),\n );\n if (conditions.length === 0) {\n throw new Error(\n `fake-mongo: no array filter found for identifier ${identifier}`,\n );\n }\n const current = updated[field];\n updated[field] = (Array.isArray(current) ? current : []).map(\n element =>\n isPlainObject(element) &&\n conditions.every(([key, condition]) =>\n matchesCondition(element[key], condition),\n )\n ? { ...element, [member]: value }\n : element,\n );\n }\n for (const [field, value] of Object.entries(update.$push ?? {})) {\n const current = updated[field];\n updated[field] = [\n ...(Array.isArray(current) ? current : []),\n value,\n ];\n }\n for (const [field, value] of Object.entries(update.$addToSet ?? {})) {\n const current = updated[field];\n const array = Array.isArray(current) ? current : [];\n if (!array.includes(value)) {\n updated[field] = [...array, value];\n }\n }\n for (const [field, delta] of Object.entries(update.$inc ?? {})) {\n // An absent field is created at the increment's value, which is\n // exactly why `$setOnInsert` must not also name it.\n const current = updated[field];\n updated[field] =\n (typeof current === 'number' ? current : 0) + (delta as number);\n }\n return updated;\n };\n\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n if (index === -1) {\n if (options?.upsert !== true) {\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 0 };\n }\n // The upserted document starts from the query's *equality* fields,\n // the way a real server seeds one — an operator condition contributes\n // nothing, which is why a `$expr`-guarded upsert would insert a row\n // missing the very field it filtered on.\n const seedFields = Object.fromEntries(\n Object.entries(query).filter(\n ([field, condition]) =>\n !field.startsWith('$') && !isPlainObject(condition),\n ),\n );\n const inserted = applyOperators({\n ...seedFields,\n ...(update.$setOnInsert ?? {}),\n });\n assertUnique(name, inserted);\n documents.push(inserted);\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 1 };\n }\n\n const updated = applyOperators(documents[index]);\n assertUnique(name, updated, documents[index]);\n documents[index] = updated;\n return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * `$set` across every matching document, and the count of what changed.\n *\n * ⚠️ **The count is what a caller branches on.** A settlement run claims its\n * ledger lines with this and reads `modifiedCount === 0` as \"another replica\n * took them all\" — so a fake returning a fixed `1`, or the match count\n * rather than the modified count, would make the losing replica behave like\n * the winning one and the conditional claim would test nothing.\n *\n * `$set` only. Anything else throws, for the reason `updateOne` gives:\n * silently accepting an operator it does not apply lets a spec pass on an\n * update that never happened.\n */\n updateMany: (query: QueryDocument, update: { $set?: Document }) =>\n record(name, 'updateMany', () => {\n const unsupported = Object.keys(update).filter(\n operator => operator !== '$set',\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateMany does not implement ${unsupported.join(', ')}`,\n );\n }\n\n const documents = documentsOf(name);\n let modifiedCount = 0;\n documents.forEach((doc, index) => {\n if (!matches(doc, query)) return;\n const updated = { ...doc, ...(update.$set ?? {}) };\n assertUnique(name, updated, doc);\n documents[index] = updated;\n modifiedCount += 1;\n });\n return { matchedCount: modifiedCount, modifiedCount };\n }),\n\n /**\n * Update one document and hand back the result, upserting when asked.\n *\n * ⚠️ **`returnDocument: 'after'` is the only mode implemented, and the\n * default here is `'after'` rather than the driver's `'before'`** — because\n * the callers that need this primitive at all need the merged document. A\n * two-message join upserts its own half and then asks \"is the pair complete\n * now?\", which is a question only the *after* state answers. `'before'`\n * throws rather than quietly returning the wrong half.\n *\n * Implemented over {@link updateOne} so the upsert seeding rule, the\n * conflicting-path refusal and the unique-index enforcement are the same\n * ones, rather than a second copy that could drift from them.\n */\n findOneAndUpdate: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n $push?: Document;\n },\n options?: { upsert?: boolean; returnDocument?: 'before' | 'after' },\n ) =>\n record(name, 'findOneAndUpdate', async () => {\n if (options?.returnDocument === 'before') {\n throw new Error(\n 'fake-mongo: findOneAndUpdate only implements returnDocument: \"after\"',\n );\n }\n // Where the document is *before* the write, because afterwards the\n // query may no longer find it: an upsert's operator conditions need not\n // hold of the row it just inserted — `{ folded: { $exists: false } }`\n // matches nothing once `folded` has been set. `documentsOf` hands back\n // the live array, which `updateOne` mutates in place or pushes onto, so\n // the position is enough to identify it either way.\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n const result = await collection(name).updateOne(query, update, {\n upsert: options?.upsert === true,\n });\n if (result.matchedCount === 0 && result.upsertedCount === 0) {\n return null;\n }\n\n return index === -1\n ? documents[documents.length - 1]\n : documents[index];\n }),\n\n deleteOne: (query: QueryDocument) =>\n record(name, 'deleteOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) return { deletedCount: 0 };\n documents.splice(index, 1);\n return { deletedCount: 1 };\n }),\n\n /**\n * Declares an index, and **enforces it when it is unique**.\n *\n * Idempotent, like the real one: a service that ensures its index per\n * request rather than at boot — because a tenant database only appears on\n * first write — calls this on every write.\n */\n createIndex: (spec: Document, options?: Document) =>\n record(name, 'createIndex', () => {\n if (options?.['unique'] === true) {\n const fields = Object.keys(spec);\n const existing = uniqueIndexes.get(name) ?? [];\n const already = existing.some(\n candidate =>\n candidate.length === fields.length &&\n candidate.every((field, at) => field === fields[at]),\n );\n if (!already) {\n uniqueIndexes.set(name, [...existing, fields]);\n }\n }\n return { spec, options };\n }),\n });\n\n /**\n * A session whose `withTransaction` really rolls back.\n *\n * ⚠️ **It is atomicity, not isolation, and the difference decides where a\n * test may live.** The store is a single-threaded object graph, so nothing\n * runs concurrently with the body and a \"race\" here resolves in the order it\n * was written: a concurrency property — two writers contending for the last\n * unit — is green under this fake no matter what the code does, and belongs\n * in a `*.live.spec.ts` against real Mongo (ADR 0010).\n *\n * What it *does* model is the abort path, which is worth having: a body that\n * throws leaves the store exactly as it found it, so a conditional write that\n * moves a counter and then fails to write its row cannot leave the counter\n * moved. That is a real bug shape, and one a fake without rollback would hide\n * by committing half of it.\n */\n const startSession = () => {\n let open = true;\n return {\n withTransaction: async (body: () => Promise<unknown>) => {\n const snapshot = new Map(\n [...collections].map(([name, docs]) => [\n name,\n docs.map(doc => ({ ...doc })),\n ]),\n );\n try {\n return await body();\n } catch (error) {\n collections.clear();\n for (const [name, docs] of snapshot) collections.set(name, docs);\n throw error;\n }\n },\n endSession: () => {\n open = false;\n return Promise.resolve();\n },\n get hasEnded() {\n return !open;\n },\n };\n };\n\n return {\n seed: (name, documents) => {\n collections.set(\n name,\n documents.map(doc => ({ ...doc })),\n );\n },\n read: name => documentsOf(name).map(doc => ({ ...doc })),\n failWith: error => {\n failure = error;\n },\n failOn: (operation, error) => {\n failuresByOperation.set(operation, error);\n },\n get operations() {\n return operations;\n },\n startSession,\n db: { collection },\n };\n};\n"],"names":["isPlainObject","value","Array","isArray","compare","left","right","Date","getTime","String","localeCompare","matchesCondition","actual","condition","Object","entries","every","operator","operand","includes","undefined","RegExp","test","TypeError","evaluateExpression","doc","node","startsWith","slice","length","operands","map","child","valuesAt","field","current","segment","split","next","container","element","push","flatMap","NEGATIVE_OPERATORS","Set","isNegativeCondition","keys","some","has","matches","query","candidates","FakeDuplicateKeyError","Error","collection","key","JSON","stringify","code","name","makeFakeMongoDb","seed","collections","Map","docs","d","operations","failure","failuresByOperation","documentsOf","set","get","uniqueIndexes","uniqueIndexesOf","assertUnique","candidate","ignored","fields","clash","find","fromEntries","record","op","produce","scoped","Promise","reject","resolve","error","project","options","copy","projection","sortSpec","skipCount","limitCount","Infinity","cursor","sort","spec","skip","count","limit","toArray","matched","filter","sorted","reduceRight","items","direction","findOne","found","countDocuments","insertMany","documents","stored","document","insertedCount","insertOne","acknowledged","insertedId","replaceOne","replacement","index","findIndex","upsert","matchedCount","upsertedCount","updateOne","update","supported","unsupported","join","conflicting","$setOnInsert","$inc","$set","applyOperators","base","plain","updated","path","filtered","exec","identifier","member","conditions","arrayFilters","$push","$addToSet","array","delta","modifiedCount","seedFields","inserted","updateMany","forEach","findOneAndUpdate","returnDocument","result","deleteOne","deletedCount","splice","createIndex","existing","already","at","startSession","open","withTransaction","body","snapshot","clear","endSession","hasEnded","read","failWith","failOn","operation","db"],"mappings":"AAAA;;;;;;;;;;CAUC,GAED,+DAA+D;AAI/D,MAAMA,gBAAgB,CAACC,QACrB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AAEhE,MAAMG,UAAU,CAACC,MAAeC;IAC9B,IAAID,QAAQ,QAAQC,SAAS,MAAM,OAAO;IAC1C,IAAID,QAAQ,MAAM,OAAO,CAAC;IAC1B,IAAIC,SAAS,MAAM,OAAO;IAC1B,IAAI,OAAOD,SAAS,YAAY,OAAOC,UAAU,UAC/C,OAAOD,OAAOC;IAChB,IAAID,gBAAgBE,QAAQD,iBAAiBC,MAAM;QACjD,OAAOF,KAAKG,OAAO,KAAKF,MAAME,OAAO;IACvC;IACA,OAAOC,OAAOJ,MAAMK,aAAa,CAACD,OAAOH;AAC3C;AAEA,uEAAuE,GACvE,MAAMK,mBAAmB,CAACC,QAAiBC;IACzC,IAAI,CAACb,cAAca,YAAY;QAC7B,OAAOD,WAAWC;IACpB;IAEA,OAAOC,OAAOC,OAAO,CAACF,WAAWG,KAAK,CAAC,CAAC,CAACC,UAAUC,QAAQ;QACzD,OAAQD;YACN,KAAK;gBACH,OAAOL,WAAWM;YACpB,KAAK;gBACH,OAAON,WAAWM;YACpB,KAAK;gBACH,OAAOd,QAAQQ,QAAQM,WAAW;YACpC,KAAK;gBACH,OAAOd,QAAQQ,QAAQM,YAAY;YACrC,KAAK;gBACH,OAAOd,QAAQQ,QAAQM,WAAW;YACpC,KAAK;gBACH,OAAOd,QAAQQ,QAAQM,YAAY;YACrC,KAAK;gBACH,OAAO,AAACA,QAAsBC,QAAQ,CAACP;YACzC,yEAAyE;YACzE,uEAAuE;YACvE,wEAAwE;YACxE,uEAAuE;YACvE,KAAK;gBACH,OAAO,AAACA,WAAWQ,cAAgBF;YACrC,KAAK;gBACH,OAAO,CAAC,AAACA,QAAsBC,QAAQ,CAACP;YAC1C,KAAK;gBACH,OAAO,CAACD,iBAAiBC,QAAQM;YACnC,KAAK;oBAGAL;gBAFH,OAAO,IAAIQ,OACTH,UACCL,sBAAAA,SAAS,CAAC,WAAW,YAArBA,sBAAoC,IACrCS,IAAI,CAACb,OAAOG;YAChB,KAAK;gBACH,kCAAkC;gBAClC,OAAO;YACT;gBACE,MAAM,IAAIW,UAAU,CAAC,kCAAkC,EAAEN,SAAS,CAAC,CAAC;QACxE;IACF;AACF;AAEA;;;;;;;;;;;CAWC,GACD,MAAMO,qBAAqB,CAACC,KAAeC;IACzC,IAAI,OAAOA,SAAS,YAAYA,KAAKC,UAAU,CAAC,MAAM;QACpD,OAAOF,GAAG,CAACC,KAAKE,KAAK,CAAC,GAAG;IAC3B;IACA,IAAI,CAAC5B,cAAc0B,OAAO;QACxB,OAAOA;IACT;IAEA,MAAMX,UAAUD,OAAOC,OAAO,CAACW;IAC/B,IAAIX,QAAQc,MAAM,KAAK,GAAG;QACxB,MAAM,IAAIN,UACR,CAAC,2DAA2D,EAAER,QAAQc,MAAM,EAAE;IAElF;IACA,MAAM,CAACZ,UAAUC,QAAQ,GAAGH,OAAO,CAAC,EAAE;IACtC,MAAMe,WAAW,AAACZ,QAAsBa,GAAG,CAACC,CAAAA,QAC1CR,mBAAmBC,KAAKO;IAG1B,OAAQf;QACN,KAAK;YACH,OAAO,AAACa,QAAQ,CAAC,EAAE,GAAeA,QAAQ,CAAC,EAAE;QAC/C,KAAK;YACH,OAAO,AAACA,QAAQ,CAAC,EAAE,GAAeA,QAAQ,CAAC,EAAE;QAC/C,KAAK;YACH,OAAO1B,QAAQ0B,QAAQ,CAAC,EAAE,EAAEA,QAAQ,CAAC,EAAE,KAAK;QAC9C,KAAK;YACH,OAAO1B,QAAQ0B,QAAQ,CAAC,EAAE,EAAEA,QAAQ,CAAC,EAAE,IAAI;QAC7C,KAAK;YACH,OAAO1B,QAAQ0B,QAAQ,CAAC,EAAE,EAAEA,QAAQ,CAAC,EAAE,KAAK;QAC9C,KAAK;YACH,OAAO1B,QAAQ0B,QAAQ,CAAC,EAAE,EAAEA,QAAQ,CAAC,EAAE,IAAI;QAC7C,KAAK;YACH,OAAOA,QAAQ,CAAC,EAAE,KAAKA,QAAQ,CAAC,EAAE;QACpC;YACE,MAAM,IAAIP,UACR,CAAC,wCAAwC,EAAEN,SAAS,CAAC,CAAC;IAE5D;AACF;AAEA;;;;;;;;;;;;;CAaC,GACD,MAAMgB,WAAW,CAACR,KAAeS;IAC/B,IAAIC,UAAqB;QAACV;KAAI;IAC9B,KAAK,MAAMW,WAAWF,MAAMG,KAAK,CAAC,KAAM;QACtC,MAAMC,OAAkB,EAAE;QAC1B,KAAK,MAAMrC,SAASkC,QAAS;YAC3B,MAAMI,YAAYrC,MAAMC,OAAO,CAACF,SAASA,QAAQ;gBAACA;aAAM;YACxD,KAAK,MAAMuC,WAAWD,UAAW;gBAC/B,IAAIvC,cAAcwC,UAAU;oBAC1BF,KAAKG,IAAI,CAACD,OAAO,CAACJ,QAAQ;gBAC5B;YACF;QACF;QACAD,UAAUG;IACZ;IACA,OAAOH,QAAQO,OAAO,CAACzC,CAAAA,QACrBC,MAAMC,OAAO,CAACF,SAAS;YAACA;eAAUA;SAAM,GAAG;YAACA;SAAM;AAEtD;AAEA,wEAAwE,GACxE,MAAM0C,qBAAqB,IAAIC,IAAI;IAAC;IAAO;IAAQ;CAAO;AAE1D,MAAMC,sBAAsB,CAAChC,YAC3Bb,cAAca,cACdC,OAAOgC,IAAI,CAACjC,WAAWkC,IAAI,CAAC9B,CAAAA,WAAY0B,mBAAmBK,GAAG,CAAC/B;AAEjE,MAAMgC,UAAU,CAACxB,KAAeyB,QAC9BpC,OAAOC,OAAO,CAACmC,OAAOlC,KAAK,CAAC,CAAC,CAACkB,OAAOrB,UAAU;QAC7C,IAAIqB,UAAU,QAAQ;YACpB,OAAO,AAACrB,UAA8BG,KAAK,CAACgB,CAAAA,QAASiB,QAAQxB,KAAKO;QACpE;QACA,IAAIE,UAAU,OAAO;YACnB,OAAO,AAACrB,UAA8BkC,IAAI,CAACf,CAAAA,QAASiB,QAAQxB,KAAKO;QACnE;QACA,IAAIE,UAAU,SAAS;YACrB,OAAOV,mBAAmBC,KAAKZ,eAAe;QAChD;QACA,MAAMsC,aAAalB,SAASR,KAAKS;QACjC,IAAIiB,WAAWtB,MAAM,KAAK,GAAG;YAC3B,OAAOlB,iBAAiBS,WAAWP;QACrC;QACA,4EAA4E;QAC5E,uEAAuE;QACvE,4EAA4E;QAC5E,sEAAsE;QACtE,OAAOgC,oBAAoBhC,aACvBsC,WAAWnC,KAAK,CAACf,CAAAA,QAASU,iBAAiBV,OAAOY,cAClDsC,WAAWJ,IAAI,CAAC9C,CAAAA,QAASU,iBAAiBV,OAAOY;IACvD;AAEF;;;;;;;CAOC,GACD,IAAA,AAAMuC,wBAAN,MAAMA,8BAA8BC;IAGlC,YAAYC,UAAkB,EAAEC,GAAa,CAAE;QAC7C,KAAK,CACH,CAAC,mDAAmD,EAAED,WAAW,UAAU,EAAEE,KAAKC,SAAS,CAACF,MAAM,QAJ7FG,OAAO;QAMd,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAsCA;;;;;;CAMC,GACD,OAAO,MAAMC,kBAAkB,CAC7BC,OAAmC,CAAC,CAAC;IAErC,MAAMC,cAAc,IAAIC,IACtBjD,OAAOC,OAAO,CAAC8C,MAAM9B,GAAG,CAAC,CAAC,CAAC4B,MAAMK,KAAK,GAAK;YACzCL;YACAK,KAAKjC,GAAG,CAACkC,CAAAA,IAAM,aAAKA;SACrB;IAEH,MAAMC,aAAwD,EAAE;IAChE,IAAIC;IACJ,MAAMC,sBAAsB,IAAIL;IAEhC,MAAMM,cAAc,CAACV;QACnB,IAAI,CAACG,YAAYd,GAAG,CAACW,OAAOG,YAAYQ,GAAG,CAACX,MAAM,EAAE;QACpD,OAAOG,YAAYS,GAAG,CAACZ;IACzB;IAEA;;;;;;;;;;GAUC,GACD,MAAMa,gBAAgB,IAAIT;IAE1B,MAAMU,kBAAkB,CAACd;YACvBa;gBAAAA,qBAAAA,cAAcD,GAAG,CAACZ,iBAAlBa,qBAA2B,EAAE;;IAE/B;;;;;;;;;GASC,GACD,MAAME,eAAe,CACnBf,MACAgB,WACAC;QAEA,KAAK,MAAMC,UAAUJ,gBAAgBd,MAAO;YAC1C,IAAIkB,OAAO9B,IAAI,CAACb,CAAAA,QAASyC,SAAS,CAACzC,MAAM,KAAKd,YAAY;YAC1D,MAAM0D,QAAQT,YAAYV,MAAMoB,IAAI,CAClCtD,CAAAA,MACEA,QAAQmD,WACRC,OAAO7D,KAAK,CAACkB,CAAAA,QAAST,GAAG,CAACS,MAAM,KAAKyC,SAAS,CAACzC,MAAM;YAEzD,IAAI4C,UAAU1D,WAAW;gBACvB,MAAM,IAAIgC,sBACRO,MACA7C,OAAOkE,WAAW,CAACH,OAAO9C,GAAG,CAACG,CAAAA,QAAS;wBAACA;wBAAOyC,SAAS,CAACzC,MAAM;qBAAC;YAEpE;QACF;IACF;IAEA,MAAM+C,SAAS,CAAStB,MAAcuB,IAAYC;QAChDjB,WAAWzB,IAAI,CAAC;YAAEa,YAAYK;YAAMuB;QAAG;QACvC,MAAME,SAAShB,oBAAoBG,GAAG,CAACW;QACvC,IAAIE,WAAWhE,WAAW,OAAOiE,QAAQC,MAAM,CAACF;QAChD,IAAIjB,YAAY/C,WAAW,OAAOiE,QAAQC,MAAM,CAACnB;QACjD,wEAAwE;QACxE,4EAA4E;QAC5E,8EAA8E;QAC9E,IAAI;YACF,OAAOkB,QAAQE,OAAO,CAACJ;QACzB,EAAE,OAAOK,OAAO;YACd,OAAOH,QAAQC,MAAM,CAACE;QACxB;IACF;IAEA,gFAAgF,GAChF,MAAMC,UAAU,CAAChE,KAAeiE;QAC9B,MAAMC,OAAO,aAAKlE;QAClB,IAAIiE,CAAAA,2BAAAA,QAASE,UAAU,KAAIF,QAAQE,UAAU,CAAC,MAAM,KAAK,GAAG;YAC1D,OAAOD,IAAI,CAAC,MAAM;QACpB;QACA,OAAOA;IACT;IAEA,MAAMrC,aAAa,CAACK,OAAkB,CAAA;YACpCoB,MAAM,CAAC7B,QAAuB,CAAC,CAAC,EAAEwC;gBAChC,IAAIG,WAAmC,CAAC;gBACxC,IAAIC,YAAY;gBAChB,IAAIC,aAAaC;gBAEjB,MAAMC,SAAS;oBACbC,MAAKC,IAA4B;wBAC/BN,WAAWM;wBACX,OAAOF;oBACT;oBACAG,MAAKC,KAAa;wBAChBP,YAAYO;wBACZ,OAAOJ;oBACT;oBACAK,OAAMD,KAAa;wBACjBN,aAAaM;wBACb,OAAOJ;oBACT;oBACAM,SAAS,IACPtB,OAAOtB,MAAM,QAAQ;4BACnB,MAAM6C,UAAUnC,YAAYV,MAAM8C,MAAM,CAAChF,CAAAA,MACvCwB,QAAQxB,KAAKyB;4BAEf,MAAMwD,SAAS5F,OAAOC,OAAO,CAAC8E,UAAUc,WAAW,CACjD,CAACC,OAAO,CAAC1E,OAAO2E,UAAU,GACxB;uCAAID;iCAAM,CAACV,IAAI,CACb,CAAC7F,MAAMC,QACL,qDAAqD;oCACrD,uDAAuD;oCACvDF,QACE6B,SAAS5B,MAAM6B,MAAM,CAAC,EAAE,EACxBD,SAAS3B,OAAO4B,MAAM,CAAC,EAAE,IACvB2E,YAEVL;4BAEF,OAAOE,OACJ9E,KAAK,CAACkE,WAAWA,YAAYC,YAC7BhE,GAAG,CAACN,CAAAA,MAAOgE,QAAQhE,KAAKiE;wBAC7B;gBACJ;gBACA,OAAOO;YACT;YAEAa,SAAS,CAAC5D,OAAsBwC,UAC9BT,OAAOtB,MAAM,WAAW;oBACtB,MAAMoD,QAAQ1C,YAAYV,MAAMoB,IAAI,CAACtD,CAAAA,MAAOwB,QAAQxB,KAAKyB;oBACzD,OAAO6D,UAAU3F,YAAY,OAAOqE,QAAQsB,OAAOrB;gBACrD;YAEFsB,gBAAgB,CAAC9D,QAAuB,CAAC,CAAC,GACxC+B,OACEtB,MACA,kBACA,IAAMU,YAAYV,MAAM8C,MAAM,CAAChF,CAAAA,MAAOwB,QAAQxB,KAAKyB,QAAQrB,MAAM;YAGrE,2EAA2E;YAC3E,gDAAgD;YAChDoF,YAAY,CAACC,YACXjC,OAAOtB,MAAM,cAAc;oBACzB,MAAMwD,SAAS9C,YAAYV;oBAC3B,KAAK,MAAMyD,YAAYF,UAAW;wBAChCxC,aAAaf,MAAMyD;wBACnBD,OAAO1E,IAAI,CAAC,aAAK2E;oBACnB;oBACA,OAAO;wBAAEC,eAAeH,UAAUrF,MAAM;oBAAC;gBAC3C;YAEF;;;;;;;;KAQC,GACDyF,WAAW,CAACF,WACVnC,OAAOtB,MAAM,aAAa;wBAGiByD;oBAFzC1C,aAAaf,MAAMyD;oBACnB/C,YAAYV,MAAMlB,IAAI,CAAC,aAAK2E;oBAC5B,OAAO;wBAAEG,cAAc;wBAAMC,UAAU,GAAEJ,eAAAA,QAAQ,CAAC,KAAK,YAAdA,eAAkB;oBAAK;gBAClE;YAEFK,YAAY,CACVvE,OACAwE,aACAhC,UAEAT,OAAOtB,MAAM,cAAc;oBACzB,MAAMuD,YAAY7C,YAAYV;oBAC9B,MAAMgE,QAAQT,UAAUU,SAAS,CAACnG,CAAAA,MAAOwB,QAAQxB,KAAKyB;oBACtD,IAAIyE,UAAU,CAAC,GAAG;wBAChB,IAAI,EAACjC,2BAAAA,QAASmC,MAAM,GAAE,OAAO;4BAAEC,cAAc;4BAAGC,eAAe;wBAAE;wBACjEb,UAAUzE,IAAI,CAAC,aAAKiF;wBACpB,OAAO;4BAAEI,cAAc;4BAAGC,eAAe;wBAAE;oBAC7C;oBACAb,SAAS,CAACS,MAAM,GAAG,aAAKD;oBACxB,OAAO;wBAAEI,cAAc;wBAAGC,eAAe;oBAAE;gBAC7C;YAEF;;;;;;;;;;;;;;;;;;;;;;;KAuBC,GACDC,WAAW,CACT9E,OACA+E,QAOAvC,UAEAT,OAAOtB,MAAM,aAAa;wBAqBQsE;oBApBhC,MAAMC,YAAY;wBAChB;wBACA;wBACA;wBACA;wBACA;qBACD;oBACD,MAAMC,cAAcrH,OAAOgC,IAAI,CAACmF,QAAQxB,MAAM,CAC5CxF,CAAAA,WAAY,CAACiH,UAAU/G,QAAQ,CAACF;oBAElC,IAAIkH,YAAYtG,MAAM,GAAG,GAAG;wBAC1B,MAAM,IAAIwB,MACR,CAAC,yCAAyC,EAAE8E,YAAYC,IAAI,CAAC,OAAO;oBAExE;oBAEA,uEAAuE;oBACvE,uEAAuE;oBACvE,qEAAqE;oBACrE,6DAA6D;oBAC7D,MAAMC,cAAcvH,OAAOgC,IAAI,EAACmF,uBAAAA,OAAOK,YAAY,YAAnBL,uBAAuB,CAAC,GAAGxB,MAAM,CAC/DvE,CAAAA;4BAAmB+F,cAAgCA;+BAA1C/F,WAAU+F,eAAAA,OAAOM,IAAI,YAAXN,eAAe,CAAC,MAAM/F,WAAU+F,eAAAA,OAAOO,IAAI,YAAXP,eAAe,CAAC;;oBAErE,IAAII,YAAYxG,MAAM,GAAG,GAAG;wBAC1B,MAAM,IAAIwB,MACR,CAAC,2EAA2E,EAAEgF,YAAYD,IAAI,CAAC,OAAO;oBAE1G;oBAEA,2EAA2E,GAC3E,MAAMK,iBAAiB,CAACC;4BACOT,cAIcA,eAkCCA,eAOAA,mBAOAA;wBApD5C,MAAMU,QAAQ7H,OAAOC,OAAO,EAACkH,eAAAA,OAAOO,IAAI,YAAXP,eAAe,CAAC,GAAGxB,MAAM,CACpD,CAAC,CAACvE,MAAM,GAAK,CAACA,MAAMf,QAAQ,CAAC;wBAE/B,MAAMyH,UAAU,aAAKF,MAAS5H,OAAOkE,WAAW,CAAC2D;wBACjD,KAAK,MAAM,CAACE,MAAM5I,MAAM,IAAIa,OAAOC,OAAO,EAACkH,gBAAAA,OAAOO,IAAI,YAAXP,gBAAe,CAAC,GAAI;;4BAC7D,MAAMa,WAAW,kCAAkCC,IAAI,CAACF;4BACxD,IAAIC,aAAa,MAAM;4BACvB,MAAM,GAAG5G,OAAO8G,YAAYC,OAAO,GAAGH;4BAMtC,mEAAmE;4BACnE,MAAMI,aAAa,SAACxD,2BAAAA,QAASyD,YAAY,mBAAI,EAAE,EAAEzG,OAAO,CAAC+D,CAAAA,SACvD3F,OAAOC,OAAO,CAAC0F,QACZA,MAAM,CAAC,CAAC,CAAClD,IAAI,GAAKA,IAAI5B,UAAU,CAAC,GAAGqH,WAAW,CAAC,CAAC,GACjDjH,GAAG,CACF,CAAC,CAACwB,KAAK1C,UAAU,GACf;wCAAC0C,IAAI3B,KAAK,CAACoH,WAAWnH,MAAM,GAAG;wCAAIhB;qCAAU;4BAGrD,IAAIqI,WAAWrH,MAAM,KAAK,GAAG;gCAC3B,MAAM,IAAIwB,MACR,CAAC,iDAAiD,EAAE2F,YAAY;4BAEpE;4BACA,MAAM7G,UAAUyG,OAAO,CAAC1G,MAAM;4BAC9B0G,OAAO,CAAC1G,MAAM,GAAG,AAAChC,CAAAA,MAAMC,OAAO,CAACgC,WAAWA,UAAU,EAAE,AAAD,EAAGJ,GAAG,CAC1DS,CAAAA,UACExC,cAAcwC,YACd0G,WAAWlI,KAAK,CAAC,CAAC,CAACuC,KAAK1C,UAAU,GAChCF,iBAAiB6B,OAAO,CAACe,IAAI,EAAE1C,cAE7B,aAAK2B;oCAAS,CAACyG,OAAO,EAAEhJ;qCACxBuC;wBAEV;wBACA,KAAK,MAAM,CAACN,OAAOjC,MAAM,IAAIa,OAAOC,OAAO,EAACkH,gBAAAA,OAAOmB,KAAK,YAAZnB,gBAAgB,CAAC,GAAI;4BAC/D,MAAM9F,UAAUyG,OAAO,CAAC1G,MAAM;4BAC9B0G,OAAO,CAAC1G,MAAM,GAAG;mCACXhC,MAAMC,OAAO,CAACgC,WAAWA,UAAU,EAAE;gCACzClC;6BACD;wBACH;wBACA,KAAK,MAAM,CAACiC,OAAOjC,MAAM,IAAIa,OAAOC,OAAO,EAACkH,oBAAAA,OAAOoB,SAAS,YAAhBpB,oBAAoB,CAAC,GAAI;4BACnE,MAAM9F,UAAUyG,OAAO,CAAC1G,MAAM;4BAC9B,MAAMoH,QAAQpJ,MAAMC,OAAO,CAACgC,WAAWA,UAAU,EAAE;4BACnD,IAAI,CAACmH,MAAMnI,QAAQ,CAAClB,QAAQ;gCAC1B2I,OAAO,CAAC1G,MAAM,GAAG;uCAAIoH;oCAAOrJ;iCAAM;4BACpC;wBACF;wBACA,KAAK,MAAM,CAACiC,OAAOqH,MAAM,IAAIzI,OAAOC,OAAO,EAACkH,eAAAA,OAAOM,IAAI,YAAXN,eAAe,CAAC,GAAI;4BAC9D,gEAAgE;4BAChE,oDAAoD;4BACpD,MAAM9F,UAAUyG,OAAO,CAAC1G,MAAM;4BAC9B0G,OAAO,CAAC1G,MAAM,GACZ,AAAC,CAAA,OAAOC,YAAY,WAAWA,UAAU,CAAA,IAAMoH;wBACnD;wBACA,OAAOX;oBACT;oBAEA,MAAM1B,YAAY7C,YAAYV;oBAC9B,MAAMgE,QAAQT,UAAUU,SAAS,CAACnG,CAAAA,MAAOwB,QAAQxB,KAAKyB;oBAEtD,IAAIyE,UAAU,CAAC,GAAG;4BAgBVM;wBAfN,IAAIvC,CAAAA,2BAAAA,QAASmC,MAAM,MAAK,MAAM;4BAC5B,OAAO;gCAAEC,cAAc;gCAAG0B,eAAe;gCAAGzB,eAAe;4BAAE;wBAC/D;wBACA,mEAAmE;wBACnE,sEAAsE;wBACtE,oEAAoE;wBACpE,yCAAyC;wBACzC,MAAM0B,aAAa3I,OAAOkE,WAAW,CACnClE,OAAOC,OAAO,CAACmC,OAAOuD,MAAM,CAC1B,CAAC,CAACvE,OAAOrB,UAAU,GACjB,CAACqB,MAAMP,UAAU,CAAC,QAAQ,CAAC3B,cAAca;wBAG/C,MAAM6I,WAAWjB,eAAe,aAC3BgB,aACCxB,wBAAAA,OAAOK,YAAY,YAAnBL,wBAAuB,CAAC;wBAE9BvD,aAAaf,MAAM+F;wBACnBxC,UAAUzE,IAAI,CAACiH;wBACf,OAAO;4BAAE5B,cAAc;4BAAG0B,eAAe;4BAAGzB,eAAe;wBAAE;oBAC/D;oBAEA,MAAMa,UAAUH,eAAevB,SAAS,CAACS,MAAM;oBAC/CjD,aAAaf,MAAMiF,SAAS1B,SAAS,CAACS,MAAM;oBAC5CT,SAAS,CAACS,MAAM,GAAGiB;oBACnB,OAAO;wBAAEd,cAAc;wBAAG0B,eAAe;wBAAGzB,eAAe;oBAAE;gBAC/D;YAEF;;;;;;;;;;;;KAYC,GACD4B,YAAY,CAACzG,OAAsB+E,SACjChD,OAAOtB,MAAM,cAAc;oBACzB,MAAMwE,cAAcrH,OAAOgC,IAAI,CAACmF,QAAQxB,MAAM,CAC5CxF,CAAAA,WAAYA,aAAa;oBAE3B,IAAIkH,YAAYtG,MAAM,GAAG,GAAG;wBAC1B,MAAM,IAAIwB,MACR,CAAC,0CAA0C,EAAE8E,YAAYC,IAAI,CAAC,OAAO;oBAEzE;oBAEA,MAAMlB,YAAY7C,YAAYV;oBAC9B,IAAI6F,gBAAgB;oBACpBtC,UAAU0C,OAAO,CAAC,CAACnI,KAAKkG;4BAEQM;wBAD9B,IAAI,CAAChF,QAAQxB,KAAKyB,QAAQ;wBAC1B,MAAM0F,UAAU,aAAKnH,MAASwG,eAAAA,OAAOO,IAAI,YAAXP,eAAe,CAAC;wBAC9CvD,aAAaf,MAAMiF,SAASnH;wBAC5ByF,SAAS,CAACS,MAAM,GAAGiB;wBACnBY,iBAAiB;oBACnB;oBACA,OAAO;wBAAE1B,cAAc0B;wBAAeA;oBAAc;gBACtD;YAEF;;;;;;;;;;;;;KAaC,GACDK,kBAAkB,CAChB3G,OACA+E,QAOAvC,UAEAT,OAAOtB,MAAM,oBAAoB;oBAC/B,IAAI+B,CAAAA,2BAAAA,QAASoE,cAAc,MAAK,UAAU;wBACxC,MAAM,IAAIzG,MACR;oBAEJ;oBACA,mEAAmE;oBACnE,wEAAwE;oBACxE,sEAAsE;oBACtE,uEAAuE;oBACvE,wEAAwE;oBACxE,oDAAoD;oBACpD,MAAM6D,YAAY7C,YAAYV;oBAC9B,MAAMgE,QAAQT,UAAUU,SAAS,CAACnG,CAAAA,MAAOwB,QAAQxB,KAAKyB;oBAEtD,MAAM6G,SAAS,MAAMzG,WAAWK,MAAMqE,SAAS,CAAC9E,OAAO+E,QAAQ;wBAC7DJ,QAAQnC,CAAAA,2BAAAA,QAASmC,MAAM,MAAK;oBAC9B;oBACA,IAAIkC,OAAOjC,YAAY,KAAK,KAAKiC,OAAOhC,aAAa,KAAK,GAAG;wBAC3D,OAAO;oBACT;oBAEA,OAAOJ,UAAU,CAAC,IACdT,SAAS,CAACA,UAAUrF,MAAM,GAAG,EAAE,GAC/BqF,SAAS,CAACS,MAAM;gBACtB;YAEFqC,WAAW,CAAC9G,QACV+B,OAAOtB,MAAM,aAAa;oBACxB,MAAMuD,YAAY7C,YAAYV;oBAC9B,MAAMgE,QAAQT,UAAUU,SAAS,CAACnG,CAAAA,MAAOwB,QAAQxB,KAAKyB;oBACtD,IAAIyE,UAAU,CAAC,GAAG,OAAO;wBAAEsC,cAAc;oBAAE;oBAC3C/C,UAAUgD,MAAM,CAACvC,OAAO;oBACxB,OAAO;wBAAEsC,cAAc;oBAAE;gBAC3B;YAEF;;;;;;KAMC,GACDE,aAAa,CAAChE,MAAgBT,UAC5BT,OAAOtB,MAAM,eAAe;oBAC1B,IAAI+B,CAAAA,2BAAAA,OAAS,CAAC,SAAS,MAAK,MAAM;4BAEflB;wBADjB,MAAMK,SAAS/D,OAAOgC,IAAI,CAACqD;wBAC3B,MAAMiE,YAAW5F,qBAAAA,cAAcD,GAAG,CAACZ,iBAAlBa,qBAA2B,EAAE;wBAC9C,MAAM6F,UAAUD,SAASrH,IAAI,CAC3B4B,CAAAA,YACEA,UAAU9C,MAAM,KAAKgD,OAAOhD,MAAM,IAClC8C,UAAU3D,KAAK,CAAC,CAACkB,OAAOoI,KAAOpI,UAAU2C,MAAM,CAACyF,GAAG;wBAEvD,IAAI,CAACD,SAAS;4BACZ7F,cAAcF,GAAG,CAACX,MAAM;mCAAIyG;gCAAUvF;6BAAO;wBAC/C;oBACF;oBACA,OAAO;wBAAEsB;wBAAMT;oBAAQ;gBACzB;QACJ,CAAA;IAEA;;;;;;;;;;;;;;;GAeC,GACD,MAAM6E,eAAe;QACnB,IAAIC,OAAO;QACX,OAAO;YACLC,iBAAiB,OAAOC;gBACtB,MAAMC,WAAW,IAAI5G,IACnB;uBAAID;iBAAY,CAAC/B,GAAG,CAAC,CAAC,CAAC4B,MAAMK,KAAK,GAAK;wBACrCL;wBACAK,KAAKjC,GAAG,CAACN,CAAAA,MAAQ,aAAKA;qBACvB;gBAEH,IAAI;oBACF,OAAO,MAAMiJ;gBACf,EAAE,OAAOlF,OAAO;oBACd1B,YAAY8G,KAAK;oBACjB,KAAK,MAAM,CAACjH,MAAMK,KAAK,IAAI2G,SAAU7G,YAAYQ,GAAG,CAACX,MAAMK;oBAC3D,MAAMwB;gBACR;YACF;YACAqF,YAAY;gBACVL,OAAO;gBACP,OAAOnF,QAAQE,OAAO;YACxB;YACA,IAAIuF,YAAW;gBACb,OAAO,CAACN;YACV;QACF;IACF;IAEA,OAAO;QACL3G,MAAM,CAACF,MAAMuD;YACXpD,YAAYQ,GAAG,CACbX,MACAuD,UAAUnF,GAAG,CAACN,CAAAA,MAAQ,aAAKA;QAE/B;QACAsJ,MAAMpH,CAAAA,OAAQU,YAAYV,MAAM5B,GAAG,CAACN,CAAAA,MAAQ,aAAKA;QACjDuJ,UAAUxF,CAAAA;YACRrB,UAAUqB;QACZ;QACAyF,QAAQ,CAACC,WAAW1F;YAClBpB,oBAAoBE,GAAG,CAAC4G,WAAW1F;QACrC;QACA,IAAItB,cAAa;YACf,OAAOA;QACT;QACAqG;QACAY,IAAI;YAAE7H;QAAW;IACnB;AACF,EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-sql.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACzC;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAC7E;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,aAAa,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrD;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAuBD,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,GAC5B,OAAM,aAAa,CAAC,eAAe,CAAM,KACxC,
|
|
1
|
+
{"version":3,"file":"fake-sql.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACzC;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAC7E;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,aAAa,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrD;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAuBD,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,GAC5B,OAAM,aAAa,CAAC,eAAe,CAAM,KACxC,aAsEF,CAAC"}
|
package/dist/drivers/fake-sql.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* parameters the adapter produced. Running the shared
|
|
12
12
|
* `describeEntityRepositoryContract` needs a real engine and belongs against real
|
|
13
13
|
* Postgres, the way the driver connection Layers are covered.
|
|
14
|
-
*/ import {
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
14
|
+
*/ import { Effect } from 'effect';
|
|
15
|
+
import { SqlError } from 'effect/sql';
|
|
16
|
+
import { Statement } from 'effect/sql';
|
|
17
17
|
const unsupported = (name)=>{
|
|
18
18
|
throw new TypeError(`fake sql: ${name} is not implemented`);
|
|
19
19
|
};
|
|
@@ -60,8 +60,10 @@ const unsupported = (name)=>{
|
|
|
60
60
|
const message = (_ref = scoped == null ? void 0 : scoped.message) != null ? _ref : failure;
|
|
61
61
|
if (message !== undefined) {
|
|
62
62
|
return Effect.fail(new SqlError.SqlError({
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
reason: new SqlError.UnknownError({
|
|
64
|
+
cause: new Error(message),
|
|
65
|
+
message
|
|
66
|
+
})
|
|
65
67
|
}));
|
|
66
68
|
}
|
|
67
69
|
return Effect.succeed(answer(sql));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/drivers/fake-sql.ts"],"sourcesContent":["/**\n * A fake of the `@effect/sql` driver, not of the repository port.\n *\n * It sits one level *below* `makeSqlRepository`, so the adapter's own code — the\n * filter translation, the clause assembly, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would skip\n * all of it. Same reasoning as {@link makeFakeMongoDb}.\n *\n * What it does **not** do is execute SQL: it hands back canned rows and records\n * the statement each call compiled to, so a spec asserts on the SQL and\n * parameters the adapter produced. Running the shared\n * `describeEntityRepositoryContract` needs a real engine and belongs against real\n * Postgres, the way the driver connection Layers are covered.\n */\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/drivers/fake-sql.ts"],"sourcesContent":["/**\n * A fake of the `@effect/sql` driver, not of the repository port.\n *\n * It sits one level *below* `makeSqlRepository`, so the adapter's own code — the\n * filter translation, the clause assembly, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would skip\n * all of it. Same reasoning as {@link makeFakeMongoDb}.\n *\n * What it does **not** do is execute SQL: it hands back canned rows and records\n * the statement each call compiled to, so a spec asserts on the SQL and\n * parameters the adapter produced. Running the shared\n * `describeEntityRepositoryContract` needs a real engine and belongs against real\n * Postgres, the way the driver connection Layers are covered.\n */\n\nimport { Effect } from 'effect';\nimport { SqlError } from 'effect/sql';\nimport { Statement } from 'effect/sql';\n\n/** One statement the adapter executed, already compiled. */\nexport interface FakeSqlExecution {\n /** The compiled SQL text, parameters replaced by `$1`, `$2`, … */\n readonly sql: string;\n readonly params: ReadonlyArray<unknown>;\n}\n\n/** Rows to answer with when a statement's SQL contains `match`. */\nexport interface FakeSqlResponse {\n readonly match: string;\n readonly rows: ReadonlyArray<Record<string, unknown>>;\n}\n\nexport interface FakeSqlClient {\n /**\n * Answers `rows` for any statement whose compiled SQL contains `match`. Later\n * registrations win, so a spec can override a default set up by a helper.\n */\n respondTo(match: string, rows: ReadonlyArray<Record<string, unknown>>): void;\n /**\n * Makes every subsequent execution fail, so the adapter's `EntifixConnError`\n * mapping is reachable.\n */\n failWith(message: string): void;\n /**\n * Makes only the statements containing `match` fail, leaving the rest working\n * — the way a select that succeeds and a count that then fails reaches a\n * distinct error branch.\n */\n failOn(match: string, message: string): void;\n /** Every statement executed, in order, for assertions about what ran. */\n readonly executions: ReadonlyArray<FakeSqlExecution>;\n /** The most recently executed statement. */\n readonly lastExecution: FakeSqlExecution | undefined;\n /**\n * The object to hand to `makeSqlRepository` in place of a real `SqlClient`.\n *\n * Typed `unknown` on purpose — casting at the call site keeps this fake honest\n * that it implements only the constructor surface the adapters use, not the\n * whole `SqlClient` (no transactions, no reactivity, no streaming).\n */\n readonly client: unknown;\n}\n\nconst unsupported = (name: string): never => {\n throw new TypeError(`fake sql: ${name} is not implemented`);\n};\n\n/**\n * The Postgres compiler's configuration, mirrored from `@effect/sql-pg` so the\n * fake emits exactly the dialect production does — `$n` placeholders and\n * double-quoted identifiers — without this package depending on the `pg` driver.\n *\n * `sql.updateValues` and the `PgJson` custom segment are deliberate `TypeError`s\n * rather than approximations: no adapter uses either, and a compiler that\n * half-emitted them would let a spec pass on SQL that real Postgres rejects.\n */\nconst pgCompiler = Statement.makeCompiler({\n dialect: 'pg',\n placeholder: index => `$${index}`,\n onIdentifier: Statement.defaultEscape('\"'),\n onRecordUpdate: () => unsupported('updateValues'),\n onCustom: () => unsupported('custom segments'),\n});\n\n/** Builds an in-memory stand-in for an `@effect/sql` client. */\nexport const makeFakeSqlClient = (\n seed: ReadonlyArray<FakeSqlResponse> = [],\n): FakeSqlClient => {\n const responses: FakeSqlResponse[] = [...seed];\n const executions: FakeSqlExecution[] = [];\n const failuresByMatch: Array<{ match: string; message: string }> = [];\n let failure: string | undefined;\n\n const answer = (sql: string) => {\n // Last registration wins, so an override does not have to remove the\n // default it replaces.\n for (let index = responses.length - 1; index >= 0; index -= 1) {\n const response = responses[index];\n if (response !== undefined && sql.includes(response.match)) {\n return response.rows;\n }\n }\n return [];\n };\n\n const execute = (sql: string, params: ReadonlyArray<unknown>) => {\n executions.push({ sql, params });\n\n const scoped = failuresByMatch.find(entry => sql.includes(entry.match));\n const message = scoped?.message ?? failure;\n if (message !== undefined) {\n return Effect.fail(\n new SqlError.SqlError({\n reason: new SqlError.UnknownError({\n cause: new Error(message),\n message,\n }),\n }),\n );\n }\n return Effect.succeed(answer(sql));\n };\n\n const connection = {\n execute: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeUnprepared: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeRaw: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeValues: () => unsupported('executeValues'),\n executeStream: () => unsupported('executeStream'),\n };\n\n return {\n respondTo: (match, rows) => {\n responses.push({ match, rows });\n },\n failWith: message => {\n failure = message;\n },\n failOn: (match, message) => {\n failuresByMatch.push({ match, message });\n },\n get executions() {\n return executions;\n },\n get lastExecution() {\n return executions[executions.length - 1];\n },\n client: Statement.make(\n Effect.succeed(connection) as never,\n pgCompiler,\n [],\n undefined,\n ),\n };\n};\n"],"names":["Effect","SqlError","Statement","unsupported","name","TypeError","pgCompiler","makeCompiler","dialect","placeholder","index","onIdentifier","defaultEscape","onRecordUpdate","onCustom","makeFakeSqlClient","seed","responses","executions","failuresByMatch","failure","answer","sql","length","response","undefined","includes","match","rows","execute","params","push","scoped","find","entry","message","fail","reason","UnknownError","cause","Error","succeed","connection","executeUnprepared","executeRaw","executeValues","executeStream","respondTo","failWith","failOn","lastExecution","client","make"],"mappings":"AAAA;;;;;;;;;;;;;CAaC,GAED,SAASA,MAAM,QAAQ,SAAS;AAChC,SAASC,QAAQ,QAAQ,aAAa;AACtC,SAASC,SAAS,QAAQ,aAAa;AA8CvC,MAAMC,cAAc,CAACC;IACnB,MAAM,IAAIC,UAAU,CAAC,UAAU,EAAED,KAAK,mBAAmB,CAAC;AAC5D;AAEA;;;;;;;;CAQC,GACD,MAAME,aAAaJ,UAAUK,YAAY,CAAC;IACxCC,SAAS;IACTC,aAAaC,CAAAA,QAAS,CAAC,CAAC,EAAEA,OAAO;IACjCC,cAAcT,UAAUU,aAAa,CAAC;IACtCC,gBAAgB,IAAMV,YAAY;IAClCW,UAAU,IAAMX,YAAY;AAC9B;AAEA,8DAA8D,GAC9D,OAAO,MAAMY,oBAAoB,CAC/BC,OAAuC,EAAE;IAEzC,MAAMC,YAA+B;WAAID;KAAK;IAC9C,MAAME,aAAiC,EAAE;IACzC,MAAMC,kBAA6D,EAAE;IACrE,IAAIC;IAEJ,MAAMC,SAAS,CAACC;QACd,qEAAqE;QACrE,uBAAuB;QACvB,IAAK,IAAIZ,QAAQO,UAAUM,MAAM,GAAG,GAAGb,SAAS,GAAGA,SAAS,EAAG;YAC7D,MAAMc,WAAWP,SAAS,CAACP,MAAM;YACjC,IAAIc,aAAaC,aAAaH,IAAII,QAAQ,CAACF,SAASG,KAAK,GAAG;gBAC1D,OAAOH,SAASI,IAAI;YACtB;QACF;QACA,OAAO,EAAE;IACX;IAEA,MAAMC,UAAU,CAACP,KAAaQ;;QAC5BZ,WAAWa,IAAI,CAAC;YAAET;YAAKQ;QAAO;QAE9B,MAAME,SAASb,gBAAgBc,IAAI,CAACC,CAAAA,QAASZ,IAAII,QAAQ,CAACQ,MAAMP,KAAK;QACrE,MAAMQ,kBAAUH,0BAAAA,OAAQG,OAAO,mBAAIf;QACnC,IAAIe,YAAYV,WAAW;YACzB,OAAOzB,OAAOoC,IAAI,CAChB,IAAInC,SAASA,QAAQ,CAAC;gBACpBoC,QAAQ,IAAIpC,SAASqC,YAAY,CAAC;oBAChCC,OAAO,IAAIC,MAAML;oBACjBA;gBACF;YACF;QAEJ;QACA,OAAOnC,OAAOyC,OAAO,CAACpB,OAAOC;IAC/B;IAEA,MAAMoB,aAAa;QACjBb,SAAS,CAACP,KAAaQ,SACrBD,QAAQP,KAAKQ;QACfa,mBAAmB,CAACrB,KAAaQ,SAC/BD,QAAQP,KAAKQ;QACfc,YAAY,CAACtB,KAAaQ,SACxBD,QAAQP,KAAKQ;QACfe,eAAe,IAAM1C,YAAY;QACjC2C,eAAe,IAAM3C,YAAY;IACnC;IAEA,OAAO;QACL4C,WAAW,CAACpB,OAAOC;YACjBX,UAAUc,IAAI,CAAC;gBAAEJ;gBAAOC;YAAK;QAC/B;QACAoB,UAAUb,CAAAA;YACRf,UAAUe;QACZ;QACAc,QAAQ,CAACtB,OAAOQ;YACdhB,gBAAgBY,IAAI,CAAC;gBAAEJ;gBAAOQ;YAAQ;QACxC;QACA,IAAIjB,cAAa;YACf,OAAOA;QACT;QACA,IAAIgC,iBAAgB;YAClB,OAAOhC,UAAU,CAACA,WAAWK,MAAM,GAAG,EAAE;QAC1C;QACA4B,QAAQjD,UAAUkD,IAAI,CACpBpD,OAAOyC,OAAO,CAACC,aACfpC,YACA,EAAE,EACFmB;IAEJ;AACF,EAAE"}
|
package/dist/effect/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/effect/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/effect/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAS,MAAM,EAAE,IAAI,EAAE,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAC;AAIjE;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,EAAE,MAAM,EAAE,QAAQ,EAC5C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAC3B,OAAO,CAAC,MAAM,CAAqD,CAAC;AAEvE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAU,MAAM,EAAE,MAAM,EAAE,QAAQ,EAC1D,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAC3B,OAAO,CAAC,MAAM,CAkBhB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,EAAE,MAAM,EAC1C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,0BAA0B,CAAC,EACjE,gBAAe,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAA4B,KAChF,OAAO,CAAC,MAAM,CAA6D,CAAC;AAE/E,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,GAAI,MAAM,EAAE,MAAM,EAC9C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,0BAA0B,CAAC,EACjE,gBAAe,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAA4B,KAChF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAC0B,CAAC;AAE/D,iEAAiE;AACjE,eAAO,MAAM,GAAG,GAAI,MAAM,EAAE,MAAM,EAChC,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,OAAO,CAAC,MAAM,CAA8B,CAAC;AAEhD,wEAAwE;AACxE,eAAO,MAAM,UAAU,GAAU,MAAM,EAAE,MAAM,EAC7C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,OAAO,CAAC,MAAM,CAiBhB,CAAC"}
|
package/dist/effect/run.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Exit } from 'effect';
|
|
1
|
+
import { Cause, Effect, Exit, Option } from 'effect';
|
|
2
2
|
import { stubConfigurationLayer } from '../doubles/configuration.js';
|
|
3
3
|
/**
|
|
4
4
|
* Runs an Effect that still requires `TContext`, providing it from `layer`.
|
|
@@ -18,11 +18,11 @@ import { stubConfigurationLayer } from '../doubles/configuration.js';
|
|
|
18
18
|
if (Exit.isSuccess(exit)) {
|
|
19
19
|
throw new Error(`Expected the effect to fail, but it succeeded with: ${JSON.stringify(exit.value)}`);
|
|
20
20
|
}
|
|
21
|
-
const failure = exit.cause;
|
|
22
|
-
if (failure
|
|
23
|
-
return failure.
|
|
21
|
+
const failure = Cause.findErrorOption(exit.cause);
|
|
22
|
+
if (Option.isSome(failure)) {
|
|
23
|
+
return failure.value;
|
|
24
24
|
}
|
|
25
|
-
throw new Error(`Expected a typed failure, but the effect died with: ${String(
|
|
25
|
+
throw new Error(`Expected a typed failure, but the effect died with: ${String(exit.cause)}`);
|
|
26
26
|
};
|
|
27
27
|
/**
|
|
28
28
|
* Runs a repository Effect.
|
|
@@ -39,8 +39,9 @@ import { stubConfigurationLayer } from '../doubles/configuration.js';
|
|
|
39
39
|
if (Exit.isSuccess(exit)) {
|
|
40
40
|
throw new Error(`Expected the effect to fail, but it succeeded with: ${JSON.stringify(exit.value)}`);
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const failure = Cause.findErrorOption(exit.cause);
|
|
43
|
+
if (Option.isSome(failure)) {
|
|
44
|
+
return failure.value;
|
|
44
45
|
}
|
|
45
46
|
throw new Error(`Expected a typed failure, but the effect died with: ${String(exit.cause)}`);
|
|
46
47
|
};
|
package/dist/effect/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/effect/run.ts"],"sourcesContent":["import type { ConfigurationRepositoryTag } from '@entifix/business';\nimport { Effect, Exit, type Layer } from 'effect';\n\nimport { stubConfigurationLayer } from '../doubles/configuration.js';\n\n/**\n * Runs an Effect that still requires `TContext`, providing it from `layer`.\n *\n * Use-cases are `Effect.gen` blocks yielding Context tags; every spec would\n * otherwise repeat the same `Effect.runPromise(uc.pipe(Effect.provide(…)))`\n * incantation.\n */\nexport const runUC = <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, layer));\n\n/**\n * Runs an Effect expected to fail and returns its typed error.\n *\n * Asserting on the error value beats asserting that a promise rejected: it is\n * the difference between \"something went wrong\" and \"it failed with the\n * `EntifixLockError` that the facade turns into a 409\".\n */\nexport const expectFailure = async <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(Effect.provide(effect, layer));\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n\n const failure = exit.cause;\n if (failure
|
|
1
|
+
{"version":3,"sources":["../../src/effect/run.ts"],"sourcesContent":["import type { ConfigurationRepositoryTag } from '@entifix/business';\nimport { Cause, Effect, Exit, type Layer, Option } from 'effect';\n\nimport { stubConfigurationLayer } from '../doubles/configuration.js';\n\n/**\n * Runs an Effect that still requires `TContext`, providing it from `layer`.\n *\n * Use-cases are `Effect.gen` blocks yielding Context tags; every spec would\n * otherwise repeat the same `Effect.runPromise(uc.pipe(Effect.provide(…)))`\n * incantation.\n */\nexport const runUC = <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, layer));\n\n/**\n * Runs an Effect expected to fail and returns its typed error.\n *\n * Asserting on the error value beats asserting that a promise rejected: it is\n * the difference between \"something went wrong\" and \"it failed with the\n * `EntifixLockError` that the facade turns into a 409\".\n */\nexport const expectFailure = async <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(Effect.provide(effect, layer));\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n\n const failure = Cause.findErrorOption(exit.cause);\n if (Option.isSome(failure)) {\n return failure.value;\n }\n throw new Error(\n `Expected a typed failure, but the effect died with: ${String(exit.cause)}`,\n );\n};\n\n/**\n * Runs a repository Effect.\n *\n * Every `EntityRepository` method declares `ConfigurationRepositoryTag` in its\n * requirement channel — the REST-backed adapters resolve their endpoints\n * through it — so even a repository that needs no configuration has to be given\n * one. `configuration` supplies it for the adapters that do.\n */\nexport const runRepository = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>,\n configuration: Layer.Layer<ConfigurationRepositoryTag> = stubConfigurationLayer(),\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, configuration));\n\n/** {@link runRepository}, for an Effect expected to fail. */\nexport const runRepositoryExit = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>,\n configuration: Layer.Layer<ConfigurationRepositoryTag> = stubConfigurationLayer(),\n): Promise<Exit.Exit<TValue, TError>> =>\n Effect.runPromiseExit(Effect.provide(effect, configuration));\n\n/** Runs an Effect that requires nothing, returning its value. */\nexport const run = <TValue, TError>(\n effect: Effect.Effect<TValue, TError>,\n): Promise<TValue> => Effect.runPromise(effect);\n\n/** Runs an Effect that requires nothing and returns its typed error. */\nexport const runFailure = async <TValue, TError>(\n effect: Effect.Effect<TValue, TError>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(effect);\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n const failure = Cause.findErrorOption(exit.cause);\n if (Option.isSome(failure)) {\n return failure.value;\n }\n throw new Error(\n `Expected a typed failure, but the effect died with: ${String(exit.cause)}`,\n );\n};\n"],"names":["Cause","Effect","Exit","Option","stubConfigurationLayer","runUC","effect","layer","runPromise","provide","expectFailure","exit","runPromiseExit","isSuccess","Error","JSON","stringify","value","failure","findErrorOption","cause","isSome","String","runRepository","configuration","runRepositoryExit","run","runFailure"],"mappings":"AACA,SAASA,KAAK,EAAEC,MAAM,EAAEC,IAAI,EAAcC,MAAM,QAAQ,SAAS;AAEjE,SAASC,sBAAsB,QAAQ,8BAA8B;AAErE;;;;;;CAMC,GACD,OAAO,MAAMC,QAAQ,CACnBC,QACAC,QACoBN,OAAOO,UAAU,CAACP,OAAOQ,OAAO,CAACH,QAAQC,QAAQ;AAEvE;;;;;;CAMC,GACD,OAAO,MAAMG,gBAAgB,OAC3BJ,QACAC;IAEA,MAAMI,OAAO,MAAMV,OAAOW,cAAc,CAACX,OAAOQ,OAAO,CAACH,QAAQC;IAEhE,IAAIL,KAAKW,SAAS,CAACF,OAAO;QACxB,MAAM,IAAIG,MACR,CAAC,oDAAoD,EAAEC,KAAKC,SAAS,CACnEL,KAAKM,KAAK,GACT;IAEP;IAEA,MAAMC,UAAUlB,MAAMmB,eAAe,CAACR,KAAKS,KAAK;IAChD,IAAIjB,OAAOkB,MAAM,CAACH,UAAU;QAC1B,OAAOA,QAAQD,KAAK;IACtB;IACA,MAAM,IAAIH,MACR,CAAC,oDAAoD,EAAEQ,OAAOX,KAAKS,KAAK,GAAG;AAE/E,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMG,gBAAgB,CAC3BjB,QACAkB,gBAAyDpB,wBAAwB,GAC7DH,OAAOO,UAAU,CAACP,OAAOQ,OAAO,CAACH,QAAQkB,gBAAgB;AAE/E,2DAA2D,GAC3D,OAAO,MAAMC,oBAAoB,CAC/BnB,QACAkB,gBAAyDpB,wBAAwB,GAEjFH,OAAOW,cAAc,CAACX,OAAOQ,OAAO,CAACH,QAAQkB,gBAAgB;AAE/D,+DAA+D,GAC/D,OAAO,MAAME,MAAM,CACjBpB,SACoBL,OAAOO,UAAU,CAACF,QAAQ;AAEhD,sEAAsE,GACtE,OAAO,MAAMqB,aAAa,OACxBrB;IAEA,MAAMK,OAAO,MAAMV,OAAOW,cAAc,CAACN;IAEzC,IAAIJ,KAAKW,SAAS,CAACF,OAAO;QACxB,MAAM,IAAIG,MACR,CAAC,oDAAoD,EAAEC,KAAKC,SAAS,CACnEL,KAAKM,KAAK,GACT;IAEP;IACA,MAAMC,UAAUlB,MAAMmB,eAAe,CAACR,KAAKS,KAAK;IAChD,IAAIjB,OAAOkB,MAAM,CAACH,UAAU;QAC1B,OAAOA,QAAQD,KAAK;IACtB;IACA,MAAM,IAAIH,MACR,CAAC,oDAAoD,EAAEQ,OAAOX,KAAKS,KAAK,GAAG;AAE/E,EAAE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/testing-unit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": {
|
|
@@ -23,31 +23,26 @@
|
|
|
23
23
|
"exports": {
|
|
24
24
|
"./package.json": "./package.json",
|
|
25
25
|
".": {
|
|
26
|
-
"@entifix/source": "./src/index.ts",
|
|
27
26
|
"types": "./dist/index.d.ts",
|
|
28
27
|
"import": "./dist/index.js",
|
|
29
28
|
"default": "./dist/index.js"
|
|
30
29
|
},
|
|
31
30
|
"./drivers": {
|
|
32
|
-
"@entifix/source": "./src/drivers/index.ts",
|
|
33
31
|
"types": "./dist/drivers/index.d.ts",
|
|
34
32
|
"import": "./dist/drivers/index.js",
|
|
35
33
|
"default": "./dist/drivers/index.js"
|
|
36
34
|
},
|
|
37
35
|
"./http": {
|
|
38
|
-
"@entifix/source": "./src/http/index.ts",
|
|
39
36
|
"types": "./dist/http/index.d.ts",
|
|
40
37
|
"import": "./dist/http/index.js",
|
|
41
38
|
"default": "./dist/http/index.js"
|
|
42
39
|
},
|
|
43
40
|
"./contracts": {
|
|
44
|
-
"@entifix/source": "./src/contracts/index.ts",
|
|
45
41
|
"types": "./dist/contracts/index.d.ts",
|
|
46
42
|
"import": "./dist/contracts/index.js",
|
|
47
43
|
"default": "./dist/contracts/index.js"
|
|
48
44
|
},
|
|
49
45
|
"./react": {
|
|
50
|
-
"@entifix/source": "./src/react/index.tsx",
|
|
51
46
|
"types": "./dist/react/index.d.ts",
|
|
52
47
|
"import": "./dist/react/index.js",
|
|
53
48
|
"default": "./dist/react/index.js"
|
|
@@ -76,25 +71,20 @@
|
|
|
76
71
|
}
|
|
77
72
|
},
|
|
78
73
|
"dependencies": {
|
|
79
|
-
"@effect/sql": "0.52.1",
|
|
80
74
|
"@swc/helpers": "0.5.23",
|
|
81
|
-
"@entifix/transactions": "0.
|
|
82
|
-
"@entifix/
|
|
83
|
-
"@entifix/
|
|
75
|
+
"@entifix/transactions": "0.3.0",
|
|
76
|
+
"@entifix/business": "0.3.0",
|
|
77
|
+
"@entifix/core": "0.3.0"
|
|
84
78
|
},
|
|
85
79
|
"peerDependencies": {
|
|
86
|
-
"@effect/vitest": ">=0.30 <1",
|
|
87
80
|
"@testing-library/react": "*",
|
|
88
|
-
"effect": "
|
|
81
|
+
"effect": "^4.0.5",
|
|
89
82
|
"msw": "*",
|
|
90
83
|
"react": "*",
|
|
91
84
|
"vitest": "*",
|
|
92
|
-
"@entifix/react-controls": "0.
|
|
85
|
+
"@entifix/react-controls": "0.3.0"
|
|
93
86
|
},
|
|
94
87
|
"peerDependenciesMeta": {
|
|
95
|
-
"@effect/vitest": {
|
|
96
|
-
"optional": true
|
|
97
|
-
},
|
|
98
88
|
"@entifix/react-controls": {
|
|
99
89
|
"optional": true
|
|
100
90
|
},
|
|
@@ -115,7 +105,6 @@
|
|
|
115
105
|
}
|
|
116
106
|
},
|
|
117
107
|
"devDependencies": {
|
|
118
|
-
"
|
|
119
|
-
"effect": "3.22.2"
|
|
108
|
+
"effect": "4.0.5"
|
|
120
109
|
}
|
|
121
110
|
}
|