@entifix/testing-unit 0.1.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/builders/entity-builder.d.ts +26 -0
- package/dist/builders/entity-builder.d.ts.map +1 -0
- package/dist/builders/entity-builder.js +30 -0
- package/dist/builders/entity-builder.js.map +1 -0
- package/dist/contracts/entity-repository.contract.d.ts +36 -0
- package/dist/contracts/entity-repository.contract.d.ts.map +1 -0
- package/dist/contracts/entity-repository.contract.js +273 -0
- package/dist/contracts/entity-repository.contract.js.map +1 -0
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +4 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/transaction-ports.contract.d.ts +44 -0
- package/dist/contracts/transaction-ports.contract.d.ts.map +1 -0
- package/dist/contracts/transaction-ports.contract.js +203 -0
- package/dist/contracts/transaction-ports.contract.js.map +1 -0
- package/dist/doubles/configuration.d.ts +36 -0
- package/dist/doubles/configuration.d.ts.map +1 -0
- package/dist/doubles/configuration.js +31 -0
- package/dist/doubles/configuration.js.map +1 -0
- package/dist/doubles/entity-repository.d.ts +24 -0
- package/dist/doubles/entity-repository.d.ts.map +1 -0
- package/dist/doubles/entity-repository.js +159 -0
- package/dist/doubles/entity-repository.js.map +1 -0
- package/dist/doubles/link-resolver.d.ts +17 -0
- package/dist/doubles/link-resolver.d.ts.map +1 -0
- package/dist/doubles/link-resolver.js +34 -0
- package/dist/doubles/link-resolver.js.map +1 -0
- package/dist/doubles/transaction-ports.d.ts +70 -0
- package/dist/doubles/transaction-ports.d.ts.map +1 -0
- package/dist/doubles/transaction-ports.js +145 -0
- package/dist/doubles/transaction-ports.js.map +1 -0
- package/dist/drivers/fake-amqp.d.ts +91 -0
- package/dist/drivers/fake-amqp.d.ts.map +1 -0
- package/dist/drivers/fake-amqp.js +164 -0
- package/dist/drivers/fake-amqp.js.map +1 -0
- package/dist/drivers/fake-mongo.d.ts +59 -0
- package/dist/drivers/fake-mongo.d.ts.map +1 -0
- package/dist/drivers/fake-mongo.js +603 -0
- package/dist/drivers/fake-mongo.js.map +1 -0
- package/dist/drivers/fake-redis.d.ts +32 -0
- package/dist/drivers/fake-redis.d.ts.map +1 -0
- package/dist/drivers/fake-redis.js +164 -0
- package/dist/drivers/fake-redis.js.map +1 -0
- package/dist/drivers/fake-sql.d.ts +58 -0
- package/dist/drivers/fake-sql.d.ts.map +1 -0
- package/dist/drivers/fake-sql.js +102 -0
- package/dist/drivers/fake-sql.js.map +1 -0
- package/dist/drivers/index.d.ts +5 -0
- package/dist/drivers/index.d.ts.map +1 -0
- package/dist/drivers/index.js +6 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/effect/run.d.ts +34 -0
- package/dist/effect/run.d.ts.map +1 -0
- package/dist/effect/run.js +48 -0
- package/dist/effect/run.js.map +1 -0
- package/dist/http/entity-handlers.d.ts +37 -0
- package/dist/http/entity-handlers.d.ts.map +1 -0
- package/dist/http/entity-handlers.js +92 -0
- package/dist/http/entity-handlers.js.map +1 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +4 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/server.d.ts +19 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +31 -0
- package/dist/http/server.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +4 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/render.d.ts +30 -0
- package/dist/react/render.d.ts.map +1 -0
- package/dist/react/render.js +38 -0
- package/dist/react/render.js.map +1 -0
- package/dist/react/ui-preferences.d.ts +17 -0
- package/dist/react/ui-preferences.d.ts.map +1 -0
- package/dist/react/ui-preferences.js +29 -0
- package/dist/react/ui-preferences.js.map +1 -0
- package/package.json +122 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 r10c Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @entifix/testing-unit
|
|
2
|
+
|
|
3
|
+
Shared test doubles, port contract suites, and HTTP stubbing for the workspace.
|
|
4
|
+
Test-only: it is private, has no build target, and resolves straight to source
|
|
5
|
+
through the `@r10c/source` condition. Depend on it as a `devDependency`.
|
|
6
|
+
|
|
7
|
+
## Vocabulary
|
|
8
|
+
|
|
9
|
+
Which kind of double to reach for, and where each lives:
|
|
10
|
+
|
|
11
|
+
| Kind | What it is | Where |
|
|
12
|
+
| --------------- | ---------------------------------------------------------- | ---------------------- |
|
|
13
|
+
| **Stub** | Canned answers, no assertions on it | inline, or `makeStub*` |
|
|
14
|
+
| **Fake** | Working in-memory implementation of a **driven port** | `.` (`makeInMemory*`) |
|
|
15
|
+
| **Driver fake** | Fake of a _third-party client_, one level below an adapter | `./drivers` |
|
|
16
|
+
| **Recording** | A fake that records what happened, asserted as state | `.` (`makeRecording*`) |
|
|
17
|
+
| **MSW** | The boundary for everything HTTP | `./http` |
|
|
18
|
+
|
|
19
|
+
Mocks with call assertions are a last resort, kept for cases where the
|
|
20
|
+
behaviour _is_ the interaction (event publication, lock ordering, rollback).
|
|
21
|
+
Prefer a recording fake — `expect(bus.published)` reads as state, a spy
|
|
22
|
+
protocol does not.
|
|
23
|
+
|
|
24
|
+
## Entry points
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { makeInMemoryEntityRepository, runRepository } from '@entifix/testing-unit';
|
|
28
|
+
import { makeFakeMongoDb } from '@entifix/testing-unit/drivers';
|
|
29
|
+
import { entityRestHandlers, setupEntifixServer } from '@entifix/testing-unit/http';
|
|
30
|
+
import { describeEntityRepositoryContract } from '@entifix/testing-unit/contracts';
|
|
31
|
+
import { renderWithAdapters } from '@entifix/testing-unit/react';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`.` carries no React and no msw, so TS-only packages pull neither.
|
|
35
|
+
|
|
36
|
+
## Why driver fakes rather than port fakes
|
|
37
|
+
|
|
38
|
+
A fake at the port level replaces the adapter, so none of the adapter's own code
|
|
39
|
+
runs — the filter translation, the `SET NX PX`, the envelope framing, the error
|
|
40
|
+
mapping all go unmeasured while coverage still reports the port as exercised.
|
|
41
|
+
The driver fakes sit one level lower, so the real adapter executes against them.
|
|
42
|
+
|
|
43
|
+
## Contract suites
|
|
44
|
+
|
|
45
|
+
Each driven port has one suite, run against **every** implementation:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
describeEntityRepositoryContract('in-memory fake', { makeRepository: seed => makeInMemoryEntityRepository(seed) });
|
|
49
|
+
describeEntityRepositoryContract('mongo adapter over a fake driver', { makeRepository: seed => makeMongoRepository(fakeDb, Widget) });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This is what keeps a fake from quietly becoming a more forgiving version of the
|
|
53
|
+
thing it stands in for. `@entifix/testing-integration` will later run
|
|
54
|
+
the same suites against real infrastructure.
|
|
55
|
+
|
|
56
|
+
## Known constraint
|
|
57
|
+
|
|
58
|
+
`@entifix/business` and `@entifix/transactions` **cannot** use this
|
|
59
|
+
package: it is built on their interfaces, so depending on it from them is a
|
|
60
|
+
cycle. Those two define their doubles locally. Everything else uses this.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Entity, EntityConstructor } from '@entifix/core';
|
|
2
|
+
export interface EntityBuilder<TEntity extends Entity> {
|
|
3
|
+
/** Produces one instance, overriding the defaults with `overrides`. */
|
|
4
|
+
(overrides?: Partial<TEntity>): TEntity;
|
|
5
|
+
/**
|
|
6
|
+
* Produces `count` instances. `overrides` may be a function of the index, so
|
|
7
|
+
* a list of distinguishable rows takes one line.
|
|
8
|
+
*/
|
|
9
|
+
many(count: number, overrides?: (index: number) => Partial<TEntity>): TEntity[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Builds test entities from a set of defaults.
|
|
13
|
+
*
|
|
14
|
+
* Entities are `#field` + accessor classes, so a literal will not do — every
|
|
15
|
+
* value has to go through its setter for `MetaEntity` introspection and
|
|
16
|
+
* serialization to see it. This keeps that assignment in one place instead of
|
|
17
|
+
* in a `makeWidget` helper redefined in every spec.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const aProduct = defineEntityBuilder(Product, { name: 'Widget', price: 10 });
|
|
21
|
+
* const cheap = aProduct({ price: 1 });
|
|
22
|
+
* const rows = aProduct.many(3, (index) => ({ id: `p-${index}` }));
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const defineEntityBuilder: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, defaults?: Partial<TEntity>) => EntityBuilder<TEntity>;
|
|
26
|
+
//# sourceMappingURL=entity-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-builder.d.ts","sourceRoot":"","sources":["../../src/builders/entity-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE/D,MAAM,WAAW,aAAa,CAAC,OAAO,SAAS,MAAM;IACnD,uEAAuE;IACvE,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACxC;;;OAGG;IACH,IAAI,CACF,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAC9C,OAAO,EAAE,CAAC;CACd;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,SAAS,MAAM,EACxD,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,WAAU,OAAO,CAAC,OAAO,CAAM,KAC9B,aAAa,CAAC,OAAO,CAiBvB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* Builds test entities from a set of defaults.
|
|
4
|
+
*
|
|
5
|
+
* Entities are `#field` + accessor classes, so a literal will not do — every
|
|
6
|
+
* value has to go through its setter for `MetaEntity` introspection and
|
|
7
|
+
* serialization to see it. This keeps that assignment in one place instead of
|
|
8
|
+
* in a `makeWidget` helper redefined in every spec.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const aProduct = defineEntityBuilder(Product, { name: 'Widget', price: 10 });
|
|
12
|
+
* const cheap = aProduct({ price: 1 });
|
|
13
|
+
* const rows = aProduct.many(3, (index) => ({ id: `p-${index}` }));
|
|
14
|
+
* ```
|
|
15
|
+
*/ export const defineEntityBuilder = (entityConstructor, defaults = {})=>{
|
|
16
|
+
const build = (overrides = {})=>{
|
|
17
|
+
const instance = new entityConstructor();
|
|
18
|
+
for (const [property, value] of Object.entries(_extends({}, defaults, overrides))){
|
|
19
|
+
instance[property] = value;
|
|
20
|
+
}
|
|
21
|
+
return instance;
|
|
22
|
+
};
|
|
23
|
+
const builder = build;
|
|
24
|
+
builder.many = (count, overrides = ()=>({}))=>Array.from({
|
|
25
|
+
length: count
|
|
26
|
+
}, (_unused, index)=>build(overrides(index)));
|
|
27
|
+
return builder;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=entity-builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/builders/entity-builder.ts"],"sourcesContent":["import type { Entity, EntityConstructor } from '@entifix/core';\n\nexport interface EntityBuilder<TEntity extends Entity> {\n /** Produces one instance, overriding the defaults with `overrides`. */\n (overrides?: Partial<TEntity>): TEntity;\n /**\n * Produces `count` instances. `overrides` may be a function of the index, so\n * a list of distinguishable rows takes one line.\n */\n many(\n count: number,\n overrides?: (index: number) => Partial<TEntity>,\n ): TEntity[];\n}\n\n/**\n * Builds test entities from a set of defaults.\n *\n * Entities are `#field` + accessor classes, so a literal will not do — every\n * value has to go through its setter for `MetaEntity` introspection and\n * serialization to see it. This keeps that assignment in one place instead of\n * in a `makeWidget` helper redefined in every spec.\n *\n * ```ts\n * const aProduct = defineEntityBuilder(Product, { name: 'Widget', price: 10 });\n * const cheap = aProduct({ price: 1 });\n * const rows = aProduct.many(3, (index) => ({ id: `p-${index}` }));\n * ```\n */\nexport const defineEntityBuilder = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n defaults: Partial<TEntity> = {},\n): EntityBuilder<TEntity> => {\n const build = (overrides: Partial<TEntity> = {}): TEntity => {\n const instance = new entityConstructor();\n for (const [property, value] of Object.entries({\n ...defaults,\n ...overrides,\n })) {\n (instance as Record<string, unknown>)[property] = value;\n }\n return instance;\n };\n\n const builder = build as EntityBuilder<TEntity>;\n builder.many = (count, overrides = () => ({})) =>\n Array.from({ length: count }, (_unused, index) => build(overrides(index)));\n\n return builder;\n};\n"],"names":["defineEntityBuilder","entityConstructor","defaults","build","overrides","instance","property","value","Object","entries","builder","many","count","Array","from","length","_unused","index"],"mappings":";AAeA;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMA,sBAAsB,CACjCC,mBACAC,WAA6B,CAAC,CAAC;IAE/B,MAAMC,QAAQ,CAACC,YAA8B,CAAC,CAAC;QAC7C,MAAMC,WAAW,IAAIJ;QACrB,KAAK,MAAM,CAACK,UAAUC,MAAM,IAAIC,OAAOC,OAAO,CAAC,aAC1CP,UACAE,YACD;YACDC,QAAoC,CAACC,SAAS,GAAGC;QACpD;QACA,OAAOF;IACT;IAEA,MAAMK,UAAUP;IAChBO,QAAQC,IAAI,GAAG,CAACC,OAAOR,YAAY,IAAO,CAAA,CAAC,CAAA,CAAE,GAC3CS,MAAMC,IAAI,CAAC;YAAEC,QAAQH;QAAM,GAAG,CAACI,SAASC,QAAUd,MAAMC,UAAUa;IAEpE,OAAOP;AACT,EAAE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EntityRepository } from '@entifix/business';
|
|
2
|
+
import { type Entity, type EntityId } from '@entifix/core';
|
|
3
|
+
/**
|
|
4
|
+
* The entity every implementation of the contract is exercised against. It is
|
|
5
|
+
* declared here rather than by each caller so the suite controls exactly which
|
|
6
|
+
* fields exist.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ContractWidget implements Entity {
|
|
9
|
+
#private;
|
|
10
|
+
get id(): EntityId;
|
|
11
|
+
set id(value: EntityId);
|
|
12
|
+
get name(): string | undefined;
|
|
13
|
+
set name(value: string | undefined);
|
|
14
|
+
get size(): number | undefined;
|
|
15
|
+
set size(value: number | undefined);
|
|
16
|
+
}
|
|
17
|
+
export declare const makeContractWidget: (id: EntityId, name: string, size: number) => ContractWidget;
|
|
18
|
+
export interface EntityRepositoryContractOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Builds a repository over `seed`. Called fresh for each test, so no state
|
|
21
|
+
* leaks between them.
|
|
22
|
+
*/
|
|
23
|
+
makeRepository(seed: ContractWidget[]): EntityRepository | Promise<EntityRepository>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The behaviour every {@link EntityRepository} must exhibit, regardless of what
|
|
27
|
+
* backs it.
|
|
28
|
+
*
|
|
29
|
+
* Run it against the in-memory fake *and* against each real adapter. That is
|
|
30
|
+
* what stops a fake from drifting into a more forgiving version of the thing it
|
|
31
|
+
* stands in for — the failure mode where a suite is green and production is
|
|
32
|
+
* not. `entifix-ts-testing-integration` will run this same suite against real
|
|
33
|
+
* infrastructure later.
|
|
34
|
+
*/
|
|
35
|
+
export declare const describeEntityRepositoryContract: (name: string, { makeRepository }: EntityRepositoryContractOptions) => void;
|
|
36
|
+
//# sourceMappingURL=entity-repository.contract.d.ts.map
|
|
@@ -0,0 +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;;IAK3C,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;CACF;AAED,eAAO,MAAM,kBAAkB,GAC7B,IAAI,QAAQ,EACZ,MAAM,MAAM,EACZ,MAAM,MAAM,KACX,cAMF,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;;;;;;;;;GASG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,MAAM,EACZ,oBAAoB,+BAA+B,KAClD,IAsMF,CAAC"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { _ as _class_private_field_loose_base } from "@swc/helpers/_/_class_private_field_loose_base";
|
|
2
|
+
import { _ as _class_private_field_loose_key } from "@swc/helpers/_/_class_private_field_loose_key";
|
|
3
|
+
import { _ as _apply_decs_2203_r } from "@swc/helpers/_/_apply_decs_2203_r";
|
|
4
|
+
var _dec, _initClass, _dec1, _dec2, _dec3, _initProto;
|
|
5
|
+
import { accessor, entity } from '@entifix/core';
|
|
6
|
+
import { Exit } from 'effect';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { runRepository, runRepositoryExit } from '../effect/run.js';
|
|
9
|
+
_dec = entity({
|
|
10
|
+
key: 'contract-widget'
|
|
11
|
+
}), _dec1 = accessor({
|
|
12
|
+
type: 'id'
|
|
13
|
+
}), _dec2 = accessor({
|
|
14
|
+
type: 'string'
|
|
15
|
+
}), _dec3 = accessor({
|
|
16
|
+
type: 'number'
|
|
17
|
+
});
|
|
18
|
+
let _ContractWidget;
|
|
19
|
+
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");
|
|
20
|
+
let ContractWidget = class ContractWidget {
|
|
21
|
+
get id() {
|
|
22
|
+
return _class_private_field_loose_base(this, _id)[_id];
|
|
23
|
+
}
|
|
24
|
+
set id(value) {
|
|
25
|
+
_class_private_field_loose_base(this, _id)[_id] = value;
|
|
26
|
+
}
|
|
27
|
+
get name() {
|
|
28
|
+
return _class_private_field_loose_base(this, _name)[_name];
|
|
29
|
+
}
|
|
30
|
+
set name(value) {
|
|
31
|
+
_class_private_field_loose_base(this, _name)[_name] = value;
|
|
32
|
+
}
|
|
33
|
+
get size() {
|
|
34
|
+
return _class_private_field_loose_base(this, _size)[_size];
|
|
35
|
+
}
|
|
36
|
+
set size(value) {
|
|
37
|
+
_class_private_field_loose_base(this, _size)[_size] = value;
|
|
38
|
+
}
|
|
39
|
+
constructor(){
|
|
40
|
+
Object.defineProperty(this, _id, {
|
|
41
|
+
writable: true,
|
|
42
|
+
value: void 0
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, _name, {
|
|
45
|
+
writable: true,
|
|
46
|
+
value: void 0
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(this, _size, {
|
|
49
|
+
writable: true,
|
|
50
|
+
value: void 0
|
|
51
|
+
});
|
|
52
|
+
_initProto(this);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
({ e: [_initProto], c: [_ContractWidget, _initClass] } = _apply_decs_2203_r(ContractWidget, [
|
|
56
|
+
[
|
|
57
|
+
_dec1,
|
|
58
|
+
3,
|
|
59
|
+
"id"
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
_dec2,
|
|
63
|
+
3,
|
|
64
|
+
"name"
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
_dec3,
|
|
68
|
+
3,
|
|
69
|
+
"size"
|
|
70
|
+
]
|
|
71
|
+
], [
|
|
72
|
+
_dec
|
|
73
|
+
]));
|
|
74
|
+
_initClass();
|
|
75
|
+
export const makeContractWidget = (id, name, size)=>{
|
|
76
|
+
const widget = new _ContractWidget();
|
|
77
|
+
widget.id = id;
|
|
78
|
+
widget.name = name;
|
|
79
|
+
widget.size = size;
|
|
80
|
+
return widget;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The behaviour every {@link EntityRepository} must exhibit, regardless of what
|
|
84
|
+
* backs it.
|
|
85
|
+
*
|
|
86
|
+
* Run it against the in-memory fake *and* against each real adapter. That is
|
|
87
|
+
* what stops a fake from drifting into a more forgiving version of the thing it
|
|
88
|
+
* stands in for — the failure mode where a suite is green and production is
|
|
89
|
+
* not. `entifix-ts-testing-integration` will run this same suite against real
|
|
90
|
+
* infrastructure later.
|
|
91
|
+
*/ export const describeEntityRepositoryContract = (name, { makeRepository })=>{
|
|
92
|
+
describe(`EntityRepository contract: ${name}`, ()=>{
|
|
93
|
+
const seeded = ()=>[
|
|
94
|
+
makeContractWidget('w-1', 'Alpha', 10),
|
|
95
|
+
makeContractWidget('w-2', 'Beta', 20),
|
|
96
|
+
makeContractWidget('w-3', 'Gamma', 30)
|
|
97
|
+
];
|
|
98
|
+
const withRepository = async (use, seed = seeded())=>{
|
|
99
|
+
await use(await makeRepository(seed));
|
|
100
|
+
};
|
|
101
|
+
it('loads every stored entity with its total', async ()=>{
|
|
102
|
+
await withRepository(async (repository)=>{
|
|
103
|
+
const page = await runRepository(repository.load({}));
|
|
104
|
+
expect(page.total).toBe(3);
|
|
105
|
+
expect(page.items.map((item)=>item.name)).toEqual([
|
|
106
|
+
'Alpha',
|
|
107
|
+
'Beta',
|
|
108
|
+
'Gamma'
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it('echoes the request it served on the page', async ()=>{
|
|
113
|
+
await withRepository(async (repository)=>{
|
|
114
|
+
const request = {
|
|
115
|
+
page: 1,
|
|
116
|
+
pageSize: 2
|
|
117
|
+
};
|
|
118
|
+
const page = await runRepository(repository.load(request));
|
|
119
|
+
expect(page.request).toEqual(request);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
it('pages with a 1-based page number', async ()=>{
|
|
123
|
+
await withRepository(async (repository)=>{
|
|
124
|
+
var _page_items_;
|
|
125
|
+
const page = await runRepository(repository.load({
|
|
126
|
+
page: 2,
|
|
127
|
+
pageSize: 2
|
|
128
|
+
}));
|
|
129
|
+
expect(page.items).toHaveLength(1);
|
|
130
|
+
expect((_page_items_ = page.items[0]) == null ? void 0 : _page_items_.name).toBe('Gamma');
|
|
131
|
+
// The total counts matches, not the page.
|
|
132
|
+
expect(page.total).toBe(3);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
it('filters by an equality operator', async ()=>{
|
|
136
|
+
await withRepository(async (repository)=>{
|
|
137
|
+
var _page_items_;
|
|
138
|
+
const page = await runRepository(repository.load({
|
|
139
|
+
filtering: [
|
|
140
|
+
{
|
|
141
|
+
property: 'name',
|
|
142
|
+
operator: 'eq',
|
|
143
|
+
value: 'Beta'
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}));
|
|
147
|
+
expect(page.items).toHaveLength(1);
|
|
148
|
+
expect((_page_items_ = page.items[0]) == null ? void 0 : _page_items_.id).toBe('w-2');
|
|
149
|
+
expect(page.total).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
it('filters by a range operator', async ()=>{
|
|
153
|
+
await withRepository(async (repository)=>{
|
|
154
|
+
const page = await runRepository(repository.load({
|
|
155
|
+
filtering: [
|
|
156
|
+
{
|
|
157
|
+
property: 'size',
|
|
158
|
+
operator: 'between',
|
|
159
|
+
start: 15,
|
|
160
|
+
end: 35
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}));
|
|
164
|
+
expect(page.items.map((item)=>item.id)).toEqual([
|
|
165
|
+
'w-2',
|
|
166
|
+
'w-3'
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
it('matches `like` case-insensitively on a substring', async ()=>{
|
|
171
|
+
await withRepository(async (repository)=>{
|
|
172
|
+
const page = await runRepository(repository.load({
|
|
173
|
+
filtering: [
|
|
174
|
+
{
|
|
175
|
+
property: 'name',
|
|
176
|
+
operator: 'like',
|
|
177
|
+
value: 'et'
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}));
|
|
181
|
+
expect(page.items.map((item)=>item.id)).toEqual([
|
|
182
|
+
'w-2'
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
it('sorts descending when asked to', async ()=>{
|
|
187
|
+
await withRepository(async (repository)=>{
|
|
188
|
+
const page = await runRepository(repository.load({
|
|
189
|
+
sorting: [
|
|
190
|
+
{
|
|
191
|
+
0: {
|
|
192
|
+
property: 'size',
|
|
193
|
+
type: 'desc'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}));
|
|
198
|
+
expect(page.items.map((item)=>item.size)).toEqual([
|
|
199
|
+
30,
|
|
200
|
+
20,
|
|
201
|
+
10
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
it('reads a single entity by id', async ()=>{
|
|
206
|
+
await withRepository(async (repository)=>{
|
|
207
|
+
const found = await runRepository(repository.get('w-2'));
|
|
208
|
+
expect(found.id).toBe('w-2');
|
|
209
|
+
expect(found.name).toBe('Beta');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
it('fails rather than resolving undefined for an unknown id', async ()=>{
|
|
213
|
+
await withRepository(async (repository)=>{
|
|
214
|
+
const exit = await runRepositoryExit(repository.get('missing'));
|
|
215
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
it('mints an id when saving an entity that has none', async ()=>{
|
|
219
|
+
await withRepository(async (repository)=>{
|
|
220
|
+
const created = await runRepository(repository.save(makeContractWidget(undefined, 'Delta', 40)));
|
|
221
|
+
expect(created.id).toBeDefined();
|
|
222
|
+
const page = await runRepository(repository.load({}));
|
|
223
|
+
expect(page.total).toBe(4);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
it('overwrites in place when saving an entity that already has an id', async ()=>{
|
|
227
|
+
await withRepository(async (repository)=>{
|
|
228
|
+
await runRepository(repository.save(makeContractWidget('w-2', 'Renamed', 25)));
|
|
229
|
+
const found = await runRepository(repository.get('w-2'));
|
|
230
|
+
expect(found.name).toBe('Renamed');
|
|
231
|
+
const page = await runRepository(repository.load({}));
|
|
232
|
+
expect(page.total).toBe(3);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
it('deletes by id', async ()=>{
|
|
236
|
+
await withRepository(async (repository)=>{
|
|
237
|
+
await runRepository(repository.delete('w-1'));
|
|
238
|
+
const page = await runRepository(repository.load({}));
|
|
239
|
+
expect(page.items.map((item)=>item.id)).toEqual([
|
|
240
|
+
'w-2',
|
|
241
|
+
'w-3'
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
it('deletes by entity', async ()=>{
|
|
246
|
+
await withRepository(async (repository)=>{
|
|
247
|
+
await runRepository(repository.delete(makeContractWidget('w-3', 'Gamma', 30)));
|
|
248
|
+
const page = await runRepository(repository.load({}));
|
|
249
|
+
expect(page.items.map((item)=>item.id)).toEqual([
|
|
250
|
+
'w-1',
|
|
251
|
+
'w-2'
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
it('treats deleting an unknown id as a no-op', async ()=>{
|
|
256
|
+
await withRepository(async (repository)=>{
|
|
257
|
+
await runRepository(repository.delete('missing'));
|
|
258
|
+
const page = await runRepository(repository.load({}));
|
|
259
|
+
expect(page.total).toBe(3);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
it('loads an empty page from an empty store', async ()=>{
|
|
263
|
+
await withRepository(async (repository)=>{
|
|
264
|
+
const page = await runRepository(repository.load({}));
|
|
265
|
+
expect(page.items).toEqual([]);
|
|
266
|
+
expect(page.total).toBe(0);
|
|
267
|
+
}, []);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
export { _ContractWidget as ContractWidget };
|
|
272
|
+
|
|
273
|
+
//# sourceMappingURL=entity-repository.contract.js.map
|
|
@@ -0,0 +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\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\nexport const makeContractWidget = (\n id: EntityId,\n name: string,\n size: number,\n): ContractWidget => {\n const widget = new ContractWidget();\n widget.id = id;\n widget.name = name;\n widget.size = size;\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 behaviour every {@link EntityRepository} must exhibit, regardless of what\n * backs it.\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 { makeRepository }: EntityRepositoryContractOptions,\n): void => {\n describe(`EntityRepository 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('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('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('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 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"],"names":["accessor","entity","Exit","describe","expect","it","runRepository","runRepositoryExit","key","type","ContractWidget","id","value","name","size","makeContractWidget","widget","describeEntityRepositoryContract","makeRepository","seeded","withRepository","use","seed","repository","page","load","total","toBe","items","map","item","toEqual","request","pageSize","toHaveLength","filtering","property","operator","start","end","sorting","found","get","exit","isFailure","created","save","undefined","toBeDefined","delete"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/contracts/index.ts"],"sourcesContent":["export * from './entity-repository.contract.js';\nexport * from './transaction-ports.contract.js';\n"],"names":[],"mappings":"AAAA,cAAc,kCAAkC;AAChD,cAAc,kCAAkC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { DomainEvent } from '@entifix/core';
|
|
2
|
+
import type { EventBus, LockService, SequenceService, TransactionInbox } from '@entifix/transactions';
|
|
3
|
+
/**
|
|
4
|
+
* What every {@link LockService} must guarantee: exclusivity while held,
|
|
5
|
+
* availability after release, and a release that only frees the caller's own
|
|
6
|
+
* lock. The last one is why `release` takes a handle rather than a key — a lock
|
|
7
|
+
* that lapsed and was re-acquired by someone else must not be freed by the
|
|
8
|
+
* previous holder.
|
|
9
|
+
*/
|
|
10
|
+
export declare const describeLockServiceContract: (name: string, makeLockService: () => LockService | Promise<LockService>) => void;
|
|
11
|
+
/**
|
|
12
|
+
* What every {@link SequenceService} must guarantee: strictly increasing values
|
|
13
|
+
* per name, never repeating, with names independent of one another. This is the
|
|
14
|
+
* property incremental codes rest on.
|
|
15
|
+
*/
|
|
16
|
+
export declare const describeSequenceServiceContract: (name: string, makeSequenceService: () => SequenceService | Promise<SequenceService>) => void;
|
|
17
|
+
export interface EventBusContractHarness {
|
|
18
|
+
bus: EventBus;
|
|
19
|
+
/** Delivers `event` to whatever subscribed, as the broker would. */
|
|
20
|
+
deliver(event: DomainEvent): Promise<void>;
|
|
21
|
+
/** Everything the bus published, decoded back into events. */
|
|
22
|
+
published(): DomainEvent[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What every {@link EventBus} must guarantee: what goes out comes back
|
|
26
|
+
* unchanged, subscribers receive deliveries, and a subscriber receives only what
|
|
27
|
+
* its pattern matches. The transports differ wildly (an array here, a topic
|
|
28
|
+
* exchange in RabbitMQ) — the observable contract does not.
|
|
29
|
+
*/
|
|
30
|
+
export declare const describeEventBusContract: (name: string, makeHarness: () => EventBusContractHarness | Promise<EventBusContractHarness>) => void;
|
|
31
|
+
/**
|
|
32
|
+
* What every {@link TransactionInbox} must guarantee: a first claim is granted,
|
|
33
|
+
* every repeat of it is refused, and one consumer's claim says nothing about
|
|
34
|
+
* another's.
|
|
35
|
+
*
|
|
36
|
+
* That last one is the property with teeth. Two consumers legitimately process
|
|
37
|
+
* the same event — the saga tracker's fold and the SSE hub both bind
|
|
38
|
+
* `transaction.*` — so an implementation keyed on `eventId` alone passes the
|
|
39
|
+
* first two cases and silently starves every consumer but the first. It looks
|
|
40
|
+
* like exactly-once from inside one consumer, which is why it is asserted here
|
|
41
|
+
* rather than left to each adapter's own spec.
|
|
42
|
+
*/
|
|
43
|
+
export declare const describeTransactionInboxContract: (name: string, makeInbox: (consumer: string) => TransactionInbox | Promise<TransactionInbox>) => void;
|
|
44
|
+
//# sourceMappingURL=transaction-ports.contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction-ports.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/transaction-ports.contract.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,eAAe,EAEf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAO/B;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACtC,MAAM,MAAM,EACZ,iBAAiB,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,KACxD,IAqDF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,GAC1C,MAAM,MAAM,EACZ,qBAAqB,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,KACpE,IA6BF,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,QAAQ,CAAC;IACd,oEAAoE;IACpE,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8DAA8D;IAC9D,SAAS,IAAI,WAAW,EAAE,CAAC;CAC5B;AAwCD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GACnC,MAAM,MAAM,EACZ,aAAa,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,KAC5E,IAkDF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,MAAM,EACZ,WAAW,CAAC,QAAQ,EAAE,MAAM,KAAK,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAC5E,IAyCF,CAAC"}
|