@starci/jest-preset 2.4.1 → 2.5.1
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/CHANGELOG.md +9 -0
- package/README.md +24 -3
- package/dist/entity-manager.d.ts +3 -1
- package/dist/entity-manager.js +9 -1
- package/dist/inbox.d.ts +26 -2
- package/dist/inbox.js +32 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/query-builder.d.ts +56 -0
- package/dist/query-builder.js +51 -0
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.1 - 2026-10-29
|
|
4
|
+
|
|
5
|
+
- Fixed: `fakeTransaction` records a `query` that calls a migration's database function the way `callDbFunction` writes it (`SELECT * FROM "<schema>"."<fn>"(...)`, any schema but `pg_catalog`) as a write, so a rolled-back inbox claim or event send appears in `rolledBackWrites`; any other `SELECT` stays a read.
|
|
6
|
+
- Added: `fakeInbox().claimRow(source, eventId, tx, { entity, row })`, the double of the lite `EntityInbox` claim: it refuses a missing or blank id (`DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED`), claims the trimmed id and, for the winning claim only, records the row `row(id)` builds in `receipts` (with the manager and `inTransaction`) and answers `receipt-<n>`; a repeat, or a pair marked by `receiptExists(source, eventId)`, answers undefined. `failNext("claimRow", error)` scripts a failure.
|
|
7
|
+
|
|
8
|
+
## 2.5.0 - 2026-10-29
|
|
9
|
+
|
|
10
|
+
- Added: `fakeQueryBuilder(answers)` and `queueQueryBuilders(em, answers)` (also at `@starci/jest-preset/query-builder`), the double of `createQueryBuilder(Entity, "alias")` beside `mockEntityManager`: every chained call is recorded and returned to the chain, the terminals resolve the spec's answers. Lite persistence reads through entities and that builder (ruling `lite-persistence-typeorm-entities`), so the double moves here from the apps.
|
|
11
|
+
|
|
3
12
|
## 2.4.1 - 2026-10-28
|
|
4
13
|
|
|
5
14
|
- Changed: `@starci/ai-router` joins the ESM-only dependencies jest compiles to CommonJS, so a spec that builds an AppModule registering the AI pool loads it.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @starci/jest-preset
|
|
2
2
|
|
|
3
|
-
The jest config of a Nest repository and the one shared unit-test double kit (`mock<T>()`, `mockEntityManager`, `fakeTransaction`, `fakeCache`, `fakeLock`, `recordingEventBus`, `recordingQueueOutbox`, `fakeInbox`, `builder`, `FakeClock`, `fakeIds` and the Outcome matchers). Install it from the npm registry at the exact version in the StarCi runtime's `knowledge/stack/pins.yaml` (see [`packages/README.md`](../README.md)).
|
|
3
|
+
The jest config of a Nest repository and the one shared unit-test double kit (`mock<T>()`, `mockEntityManager`, `fakeTransaction`, `fakeQueryBuilder`, `queueQueryBuilders`, `fakeCache`, `fakeLock`, `recordingEventBus`, `recordingQueueOutbox`, `fakeInbox`, `builder`, `FakeClock`, `fakeIds` and the Outcome matchers). Install it from the npm registry at the exact version in the StarCi runtime's `knowledge/stack/pins.yaml` (see [`packages/README.md`](../README.md)).
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
// jest.config.js (a managed file: `starci app sync` renders it, `starci app check` compares it; do not edit)
|
|
@@ -72,7 +72,7 @@ const service = moduleRef.get(CommissionService)
|
|
|
72
72
|
No `new` of the service, no `imports`, no `overrideProvider`, no `jest.mock`, no casts, no `Date.now()` and no `process.env`. The
|
|
73
73
|
full rules and the dependency-to-double-to-cases table are `contexts/code/be/test.md` (the BE-TEST rules). Every
|
|
74
74
|
export below is on the package root (`@starci/jest-preset`); the sub-paths `./mock`, `./clock`, `./entity-manager`, `./ids`,
|
|
75
|
-
`./matchers`, `./cache`, `./lock`, `./event-bus`, `./queue`, `./inbox` and `./builders` exist as well.
|
|
75
|
+
`./query-builder`, `./matchers`, `./cache`, `./lock`, `./event-bus`, `./queue`, `./inbox` and `./builders` exist as well.
|
|
76
76
|
|
|
77
77
|
## `mockEntityManager` and `fakeTransaction`
|
|
78
78
|
|
|
@@ -103,7 +103,24 @@ expect(tx.committedWrites).toHaveLength(1) // after a throwing body: tx.r
|
|
|
103
103
|
The stubbable methods are `findOne`, `findOneBy`, `findOneOrFail`, `findOneByOrFail`, `find`, `findBy`, `findAndCount`,
|
|
104
104
|
`findAndCountBy`, `count`, `countBy`, `exists`, `existsBy`, `save`, `remove`, `softRemove`, `recover`, `insert`, `update`, `upsert`,
|
|
105
105
|
`delete`, `softDelete`, `restore`, `increment`, `decrement` and `query`. `fakeTransaction(em)` returns `{ em, outcomes, commits,
|
|
106
|
-
rollbacks, committedWrites, rolledBackWrites }`.
|
|
106
|
+
rollbacks, committedWrites, rolledBackWrites }`. A write is a write method, a `query` of a DML statement, or a `query` that calls a
|
|
107
|
+
migration's database function the way `callDbFunction` does (`SELECT * FROM "public"."inbox_claim_event"($1, $2)`, any schema but
|
|
108
|
+
`pg_catalog`); any other `SELECT` is a read.
|
|
109
|
+
|
|
110
|
+
## `fakeQueryBuilder` and `queueQueryBuilders`
|
|
111
|
+
|
|
112
|
+
The double of `createQueryBuilder(Entity, "alias")`, the builder a lite service reads with. Every chained method records
|
|
113
|
+
`{ method, args }` and returns the builder; a terminal (`execute`, `getMany`, `getOne`, `getRawMany`, `getRawOne`, `getCount`,
|
|
114
|
+
`getManyAndCount`) resolves the answer the spec gave it, and a function answer is called when the terminal runs.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { mockEntityManager, queueQueryBuilders } from "@starci/jest-preset"
|
|
118
|
+
|
|
119
|
+
const em = mockEntityManager()
|
|
120
|
+
const [open] = queueQueryBuilders(em, [{ getMany: [order] }]) // one fake per createQueryBuilder call, in order
|
|
121
|
+
await service.listOpen(owner)
|
|
122
|
+
expect(open.callsOf("where")).toEqual([["o.ownerId = :owner", { owner }]])
|
|
123
|
+
```
|
|
107
124
|
|
|
108
125
|
## `fakeCache(clock)`, `fakeLock(clock)`, `recordingEventBus()` and `recordingQueueOutbox()`
|
|
109
126
|
|
|
@@ -141,6 +158,10 @@ await inbox.claim("sepay", "e-1") // true the first time, false for the s
|
|
|
141
158
|
await inbox.release("sepay", "e-1") // gives the claim back: the next claim answers true
|
|
142
159
|
inbox.seen("sepay", "e-2") // a redelivery: the next claim of the pair answers false
|
|
143
160
|
inbox.claims; inbox.claimed; inbox.released; inbox.failNext("claim", new Error("db down")); inbox.clear()
|
|
161
|
+
// claimRow: the EntityInbox claim that inserts the receipt row only for the winning claim and answers its id
|
|
162
|
+
await inbox.claimRow("webhook.sepay", "e-3", tx, { entity: SepayInboxEntity, row: (deliveryId) => ({ deliveryId }) }) // "receipt-1"
|
|
163
|
+
inbox.receipts // every inserted receipt: id, source, eventId, entity, row, tx, inTransaction
|
|
164
|
+
inbox.receiptExists("webhook.sepay", "e-4") // the receipt row is already there: the winning claim inserts nothing, answers undefined
|
|
144
165
|
|
|
145
166
|
const clientInbox = fakeInbox(clock) // the driven clock stamps client claims for retention tests
|
|
146
167
|
await clientInbox.runClient({ principalId: "p-1", operation: "checkout.place", idempotencyKey: "key-1", request: { quantity: 2 } }, tx, async () => ({ kind: "ok", value: { orderId: "o-1" } }))
|
package/dist/entity-manager.d.ts
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
*
|
|
15
15
|
* `fakeTransaction(em)` makes `em.transaction(work)` (and `em.transaction(isolation, work)`) run `work` with a scoped view
|
|
16
16
|
* of the same double and record how it ended (`commit` when `work` resolves, `rollback` when it throws and the error is
|
|
17
|
-
* rethrown) and which writes it made, so a spec can assert that a rolled-back transaction committed nothing.
|
|
17
|
+
* rethrown) and which writes it made, so a spec can assert that a rolled-back transaction committed nothing. A write is a
|
|
18
|
+
* write method (`save`, `insert`, ...), a `query` of a DML statement, or a `query` calling a migration's database function
|
|
19
|
+
* the way `callDbFunction` does (`SELECT * FROM "public"."inbox_claim_event"($1, $2)`); any other `SELECT` is a read.
|
|
18
20
|
*/
|
|
19
21
|
import type { DeleteResult, EntityManager, InsertResult, UpdateResult } from "typeorm";
|
|
20
22
|
import type { MockOf } from "./mock.js";
|
package/dist/entity-manager.js
CHANGED
|
@@ -12,6 +12,14 @@ const WRITE_METHODS = new Set([
|
|
|
12
12
|
"increment", "decrement", "clear",
|
|
13
13
|
]);
|
|
14
14
|
const WRITE_SQL = /^\s*(insert|update|delete|upsert|merge|truncate)\b/i;
|
|
15
|
+
/**
|
|
16
|
+
* A call of a migration's database function in the shape `callDbFunction` writes (`SELECT * FROM "<schema>"."<fn>"(...)`), in
|
|
17
|
+
* any schema but the pg_catalog built-ins: such a function holds a write TypeORM cannot express (an inbox claim, an event send),
|
|
18
|
+
* so the transaction that makes the call records it as a write.
|
|
19
|
+
*/
|
|
20
|
+
const DB_FUNCTION_SQL = /^\s*select\s+\*\s+from\s+"(?!pg_catalog")[^"]+"\."[^"]+"\s*\(/i;
|
|
21
|
+
/** Whether a `query` writes: a DML statement, or a call of a migration's database function. */
|
|
22
|
+
const isWriteSql = (sql) => WRITE_SQL.test(String(sql)) || DB_FUNCTION_SQL.test(String(sql));
|
|
15
23
|
const describeTarget = (target) => typeof target === "function" ? target.name || "<anonymous class>" : JSON.stringify(String(target)).slice(0, 80);
|
|
16
24
|
const isPair = (value) => Array.isArray(value) && value.length === 2 && (typeof value[0] === "function" || typeof value[0] === "string");
|
|
17
25
|
/** `{ method: [target, result] | [[target, result], ...] }` as `Map<method, Array<{ target, result, used }>>`. */
|
|
@@ -74,7 +82,7 @@ function createEntityManagerKit(makeFn) {
|
|
|
74
82
|
if (!WRITE_METHODS.has(property) && property !== "query")
|
|
75
83
|
return value;
|
|
76
84
|
return (...args) => {
|
|
77
|
-
if (property !== "query" ||
|
|
85
|
+
if (property !== "query" || isWriteSql(args[0]))
|
|
78
86
|
run.writes.push({ method: property, args });
|
|
79
87
|
return value(...args);
|
|
80
88
|
};
|
package/dist/inbox.d.ts
CHANGED
|
@@ -4,7 +4,20 @@ export interface InboxEntry {
|
|
|
4
4
|
readonly eventId: string;
|
|
5
5
|
}
|
|
6
6
|
/** The inbox operations `failNext` can make reject. */
|
|
7
|
-
export type InboxOperation = "claim" | "release" | "runClient" | "sweepClients";
|
|
7
|
+
export type InboxOperation = "claim" | "claimRow" | "release" | "runClient" | "sweepClients";
|
|
8
|
+
/** The receipt `claimRow` inserts for a winning claim: its entity, and its row built from the normalized delivery id. */
|
|
9
|
+
export interface InboxReceiptRequest<Row> {
|
|
10
|
+
readonly entity: unknown;
|
|
11
|
+
readonly row: (eventId: string) => Row;
|
|
12
|
+
}
|
|
13
|
+
/** One receipt row `claimRow` inserted, with the id it answered and the manager passed by its caller. */
|
|
14
|
+
export interface InboxReceipt extends InboxEntry {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly entity: unknown;
|
|
17
|
+
readonly row: unknown;
|
|
18
|
+
readonly tx: unknown;
|
|
19
|
+
readonly inTransaction: boolean;
|
|
20
|
+
}
|
|
8
21
|
/** The client operation namespace, exact key and JSON request whose outcome the inbox stores. */
|
|
9
22
|
export interface InboxClientOperation {
|
|
10
23
|
readonly principalId: string;
|
|
@@ -27,6 +40,13 @@ export interface InboxSweep {
|
|
|
27
40
|
export interface FakeInbox {
|
|
28
41
|
/** True for the first claim of a (source, eventId) pair, false for every later one until it is released. */
|
|
29
42
|
claim(source: string, eventId: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* The `EntityInbox` claim: refuses a missing or blank delivery id, claims the trimmed id, and only for the winning claim
|
|
45
|
+
* inserts the receipt row and answers its id (`receipt-<n>`); a repeat, or a receipt that already exists, answers undefined.
|
|
46
|
+
*/
|
|
47
|
+
claimRow<Row>(source: string, eventId: string | undefined, tx: unknown, receipt: InboxReceiptRequest<Row>): Promise<string | undefined>;
|
|
48
|
+
/** Marks the receipt row of a pair as already inserted: its next winning `claimRow` inserts nothing and answers undefined. */
|
|
49
|
+
receiptExists(source: string, eventId: string): void;
|
|
30
50
|
/** Gives an ordinary delivery claim back while preserving client receipts and their outcomes. */
|
|
31
51
|
release(source: string, eventId: string): Promise<void>;
|
|
32
52
|
/** Runs once for the client namespace and key, replays the JSON outcome and refuses a different body. */
|
|
@@ -47,6 +67,8 @@ export interface FakeInbox {
|
|
|
47
67
|
readonly clientEntries: ReadonlyArray<InboxClientEntry>;
|
|
48
68
|
/** Every sweep attempt with its cutoff and transaction, including a scripted failure. */
|
|
49
69
|
readonly sweeps: ReadonlyArray<InboxSweep>;
|
|
70
|
+
/** Every receipt row `claimRow` inserted, in order, with its id, row and manager. */
|
|
71
|
+
readonly receipts: ReadonlyArray<InboxReceipt>;
|
|
50
72
|
clear(): void;
|
|
51
73
|
}
|
|
52
74
|
/**
|
|
@@ -57,7 +79,9 @@ export interface FakeInbox {
|
|
|
57
79
|
* real unique key; `release(source, eventId)` gives an ordinary delivery claim back while preserving client outcomes.
|
|
58
80
|
* `claims` is every claim in order, `claimed` the pairs that are held now, `released` every release, `seen(source, eventId)` marks a
|
|
59
81
|
* pair as already delivered (a redelivery), and `failNext(operation, error)` makes the next call of that operation reject once, so
|
|
60
|
-
* a spec drives the failure path.
|
|
82
|
+
* a spec drives the failure path. `claimRow(source, eventId, tx, { entity, row })` is the `EntityInbox` claim: it refuses a
|
|
83
|
+
* missing or blank id with `DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED`, claims the trimmed id and, for the winning claim only, records
|
|
84
|
+
* the receipt row `row(id)` builds in `receipts` and answers its id; `receiptExists(source, eventId)` makes that insert a no-op.
|
|
61
85
|
*/
|
|
62
86
|
/** The inbox double. Pass a driven clock to test retention; omitted clocks stamp client claims at the Unix epoch. */
|
|
63
87
|
export declare function fakeInbox(clock?: {
|
package/dist/inbox.js
CHANGED
|
@@ -35,7 +35,9 @@ const snapshot = (value, ancestors = new Set()) => {
|
|
|
35
35
|
* real unique key; `release(source, eventId)` gives an ordinary delivery claim back while preserving client outcomes.
|
|
36
36
|
* `claims` is every claim in order, `claimed` the pairs that are held now, `released` every release, `seen(source, eventId)` marks a
|
|
37
37
|
* pair as already delivered (a redelivery), and `failNext(operation, error)` makes the next call of that operation reject once, so
|
|
38
|
-
* a spec drives the failure path.
|
|
38
|
+
* a spec drives the failure path. `claimRow(source, eventId, tx, { entity, row })` is the `EntityInbox` claim: it refuses a
|
|
39
|
+
* missing or blank id with `DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED`, claims the trimmed id and, for the winning claim only, records
|
|
40
|
+
* the receipt row `row(id)` builds in `receipts` and answers its id; `receiptExists(source, eventId)` makes that insert a no-op.
|
|
39
41
|
*/
|
|
40
42
|
/** The inbox double. Pass a driven clock to test retention; omitted clocks stamp client claims at the Unix epoch. */
|
|
41
43
|
function fakeInbox(clock) {
|
|
@@ -45,6 +47,8 @@ function fakeInbox(clock) {
|
|
|
45
47
|
const clients = new Map();
|
|
46
48
|
const clientEntries = [];
|
|
47
49
|
const sweeps = [];
|
|
50
|
+
const receipts = [];
|
|
51
|
+
const existingReceipts = new Set();
|
|
48
52
|
const failures = new Map();
|
|
49
53
|
const keyOf = (source, eventId) => `${source}\u0000${eventId}`;
|
|
50
54
|
const failIfScripted = (operation) => {
|
|
@@ -106,6 +110,27 @@ function fakeInbox(clock) {
|
|
|
106
110
|
held.add(key);
|
|
107
111
|
return true;
|
|
108
112
|
},
|
|
113
|
+
async claimRow(source, eventId, tx, receipt) {
|
|
114
|
+
failIfScripted("claimRow");
|
|
115
|
+
const normalized = eventId?.trim();
|
|
116
|
+
if (!normalized)
|
|
117
|
+
throw inboxError("DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED");
|
|
118
|
+
claims.push({ source, eventId: normalized });
|
|
119
|
+
const key = keyOf(source, normalized);
|
|
120
|
+
if (held.has(key))
|
|
121
|
+
return undefined;
|
|
122
|
+
held.add(key);
|
|
123
|
+
if (existingReceipts.has(key))
|
|
124
|
+
return undefined;
|
|
125
|
+
existingReceipts.add(key);
|
|
126
|
+
const id = `receipt-${String(receipts.length + 1)}`;
|
|
127
|
+
receipts.push({ id, source, eventId: normalized, entity: receipt.entity, row: receipt.row(normalized), tx, inTransaction: (0, entity_manager_ts_1.isTransactionManager)(tx) });
|
|
128
|
+
return id;
|
|
129
|
+
},
|
|
130
|
+
/** Marks the receipt row of a pair as already inserted, so its next winning `claimRow` answers undefined. */
|
|
131
|
+
receiptExists(source, eventId) {
|
|
132
|
+
existingReceipts.add(keyOf(source, eventId));
|
|
133
|
+
},
|
|
109
134
|
async release(source, eventId) {
|
|
110
135
|
failIfScripted("release");
|
|
111
136
|
released.push({ source, eventId });
|
|
@@ -160,7 +185,13 @@ function fakeInbox(clock) {
|
|
|
160
185
|
get sweeps() {
|
|
161
186
|
return [...sweeps];
|
|
162
187
|
},
|
|
188
|
+
/** Every receipt row `claimRow` inserted, in order. */
|
|
189
|
+
get receipts() {
|
|
190
|
+
return [...receipts];
|
|
191
|
+
},
|
|
163
192
|
clear() {
|
|
193
|
+
receipts.length = 0;
|
|
194
|
+
existingReceipts.clear();
|
|
164
195
|
held.clear();
|
|
165
196
|
claims.length = 0;
|
|
166
197
|
released.length = 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Config } from "jest";
|
|
2
2
|
import { mock, createMock } from "./mock.js";
|
|
3
3
|
import { mockEntityManager, fakeTransaction } from "./entity-manager.js";
|
|
4
|
+
import { fakeQueryBuilder, queueQueryBuilders } from "./query-builder.js";
|
|
4
5
|
import { FakeClock } from "./clock.js";
|
|
5
6
|
import { fakeCache } from "./cache.js";
|
|
6
7
|
import { fakeLock } from "./lock.js";
|
|
@@ -11,6 +12,7 @@ import { builder } from "./builders.js";
|
|
|
11
12
|
import { fakeIds, FakeIds } from "./ids.js";
|
|
12
13
|
export type { MockOf } from "./mock.js";
|
|
13
14
|
export type { MockEntityManager, FakeTransaction, EntityClass } from "./entity-manager.js";
|
|
15
|
+
export type { FakeQueryBuilder, QueryBuilderAnswers, QueryBuilderCall, QueryBuilderHost } from "./query-builder.js";
|
|
14
16
|
export type { FakeCache } from "./cache.js";
|
|
15
17
|
export type { FakeLock } from "./lock.js";
|
|
16
18
|
export type { RecordingEventBus } from "./event-bus.js";
|
|
@@ -111,4 +113,4 @@ declare const WORLD_RUNNER: string;
|
|
|
111
113
|
declare function hasCoverageSubjects(root: string, glob: string): boolean;
|
|
112
114
|
/** The whole jest config: projects `unit`, `integration`, `e2e` and `contract`. It takes the coverage scope the managed repository config renders; without it it refuses. */
|
|
113
115
|
declare function starciJestConfig(options: StarciJestOptions): StarciJestConfig;
|
|
114
|
-
export { starciJestConfig, TEST_KIND_FOLDERS, MODULE_NAME_MAPPER, mock, createMock, mockEntityManager, fakeTransaction, FakeClock, fakeCache, fakeLock, recordingEventBus, recordingQueueOutbox, fakeInbox, builder, fakeIds, FakeIds, collectCoverageFrom, rootGlob, sonarExclusions, hasCoverageSubjects, COVERAGE_EXCLUDES, COVERAGE_THRESHOLD, UNIT_COMPILER_OPTIONS, WORLD_RUNNER, };
|
|
116
|
+
export { starciJestConfig, TEST_KIND_FOLDERS, MODULE_NAME_MAPPER, mock, createMock, mockEntityManager, fakeTransaction, fakeQueryBuilder, queueQueryBuilders, FakeClock, fakeCache, fakeLock, recordingEventBus, recordingQueueOutbox, fakeInbox, builder, fakeIds, FakeIds, collectCoverageFrom, rootGlob, sonarExclusions, hasCoverageSubjects, COVERAGE_EXCLUDES, COVERAGE_THRESHOLD, UNIT_COMPILER_OPTIONS, WORLD_RUNNER, };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WORLD_RUNNER = exports.UNIT_COMPILER_OPTIONS = exports.COVERAGE_THRESHOLD = exports.COVERAGE_EXCLUDES = exports.FakeIds = exports.fakeIds = exports.builder = exports.fakeInbox = exports.recordingQueueOutbox = exports.recordingEventBus = exports.fakeLock = exports.fakeCache = exports.FakeClock = exports.fakeTransaction = exports.mockEntityManager = exports.createMock = exports.mock = exports.MODULE_NAME_MAPPER = exports.TEST_KIND_FOLDERS = void 0;
|
|
3
|
+
exports.WORLD_RUNNER = exports.UNIT_COMPILER_OPTIONS = exports.COVERAGE_THRESHOLD = exports.COVERAGE_EXCLUDES = exports.FakeIds = exports.fakeIds = exports.builder = exports.fakeInbox = exports.recordingQueueOutbox = exports.recordingEventBus = exports.fakeLock = exports.fakeCache = exports.FakeClock = exports.queueQueryBuilders = exports.fakeQueryBuilder = exports.fakeTransaction = exports.mockEntityManager = exports.createMock = exports.mock = exports.MODULE_NAME_MAPPER = exports.TEST_KIND_FOLDERS = void 0;
|
|
4
4
|
exports.starciJestConfig = starciJestConfig;
|
|
5
5
|
exports.collectCoverageFrom = collectCoverageFrom;
|
|
6
6
|
exports.rootGlob = rootGlob;
|
|
@@ -14,6 +14,9 @@ Object.defineProperty(exports, "createMock", { enumerable: true, get: function (
|
|
|
14
14
|
const entity_manager_ts_1 = require("./entity-manager.js");
|
|
15
15
|
Object.defineProperty(exports, "mockEntityManager", { enumerable: true, get: function () { return entity_manager_ts_1.mockEntityManager; } });
|
|
16
16
|
Object.defineProperty(exports, "fakeTransaction", { enumerable: true, get: function () { return entity_manager_ts_1.fakeTransaction; } });
|
|
17
|
+
const query_builder_ts_1 = require("./query-builder.js");
|
|
18
|
+
Object.defineProperty(exports, "fakeQueryBuilder", { enumerable: true, get: function () { return query_builder_ts_1.fakeQueryBuilder; } });
|
|
19
|
+
Object.defineProperty(exports, "queueQueryBuilders", { enumerable: true, get: function () { return query_builder_ts_1.queueQueryBuilders; } });
|
|
17
20
|
const clock_ts_1 = require("./clock.js");
|
|
18
21
|
Object.defineProperty(exports, "FakeClock", { enumerable: true, get: function () { return clock_ts_1.FakeClock; } });
|
|
19
22
|
const cache_ts_1 = require("./cache.js");
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fakeQueryBuilder()` and `queueQueryBuilders()` -- the double of a typeorm `SelectQueryBuilder` (and its insert, update
|
|
3
|
+
* and delete siblings) that `mockEntityManager` hands out from `createQueryBuilder(Entity, "alias")`.
|
|
4
|
+
*
|
|
5
|
+
* A lite service reads with `createQueryBuilder(Entity, "alias")` and named parameters (persistence.md BE-PERSISTENCE-4).
|
|
6
|
+
* The fake is a Proxy: every chained method (`where`, `andWhere`, `innerJoin`, `orderBy`, `take`, `setLock`, `returning`,
|
|
7
|
+
* ...) records `{ method, args }` and returns the builder; a terminal method (`execute`, `getMany`, `getOne`,
|
|
8
|
+
* `getRawMany`, `getRawOne`, `getCount`, `getManyAndCount`) records too and resolves the answer the spec gave it (a function
|
|
9
|
+
* answer is called when the terminal runs). A spec asserts the statement it expects through `calls` or `callsOf(method)`.
|
|
10
|
+
*
|
|
11
|
+
* `queueQueryBuilders(em, answers)` queues one fake per answer on `em.createQueryBuilder` of a `mockEntityManager()`, in
|
|
12
|
+
* call order, and returns the fakes so the spec can read each one's calls.
|
|
13
|
+
*/
|
|
14
|
+
/** One chained call recorded on a fake query builder. */
|
|
15
|
+
export interface QueryBuilderCall {
|
|
16
|
+
/** The builder method that was called. */
|
|
17
|
+
readonly method: string;
|
|
18
|
+
/** The arguments it was called with. */
|
|
19
|
+
readonly args: ReadonlyArray<unknown>;
|
|
20
|
+
}
|
|
21
|
+
/** What the terminal methods of a fake query builder answer; a function is called when the terminal runs and its result returned. */
|
|
22
|
+
export interface QueryBuilderAnswers {
|
|
23
|
+
/** The answer of `execute()`. */
|
|
24
|
+
readonly execute?: unknown;
|
|
25
|
+
/** The answer of `getMany()`. */
|
|
26
|
+
readonly getMany?: ReadonlyArray<unknown> | (() => unknown);
|
|
27
|
+
/** The answer of `getOne()`. */
|
|
28
|
+
readonly getOne?: unknown;
|
|
29
|
+
/** The answer of `getRawMany()`. */
|
|
30
|
+
readonly getRawMany?: ReadonlyArray<unknown> | (() => unknown);
|
|
31
|
+
/** The answer of `getRawOne()`. */
|
|
32
|
+
readonly getRawOne?: unknown;
|
|
33
|
+
/** The answer of `getCount()`. */
|
|
34
|
+
readonly getCount?: number | (() => unknown);
|
|
35
|
+
/** The answer of `getManyAndCount()`. */
|
|
36
|
+
readonly getManyAndCount?: readonly [ReadonlyArray<unknown>, number] | (() => unknown);
|
|
37
|
+
}
|
|
38
|
+
/** A chainable stand-in for a typeorm query builder that records every chained call. */
|
|
39
|
+
export interface FakeQueryBuilder {
|
|
40
|
+
/** The builder to hand back from `entityManager.createQueryBuilder`. */
|
|
41
|
+
readonly builder: object;
|
|
42
|
+
/** Every chained call, in order. */
|
|
43
|
+
readonly calls: ReadonlyArray<QueryBuilderCall>;
|
|
44
|
+
/** The argument lists of every call of one builder method, in order. */
|
|
45
|
+
callsOf(method: string): ReadonlyArray<ReadonlyArray<unknown>>;
|
|
46
|
+
}
|
|
47
|
+
/** Builds a chainable query-builder double: every method returns the builder, the terminal methods resolve `answers`. */
|
|
48
|
+
export declare const fakeQueryBuilder: (answers?: QueryBuilderAnswers) => FakeQueryBuilder;
|
|
49
|
+
/** The part of a `mockEntityManager()` this helper queues on: its `createQueryBuilder` jest mock. */
|
|
50
|
+
export interface QueryBuilderHost {
|
|
51
|
+
readonly createQueryBuilder: {
|
|
52
|
+
mockReturnValueOnce(value: never): unknown;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Queues one fake query builder per answer on `manager.createQueryBuilder`, handed out in call order, and returns them. */
|
|
56
|
+
export declare const queueQueryBuilders: (manager: QueryBuilderHost, answers: ReadonlyArray<QueryBuilderAnswers>) => ReadonlyArray<FakeQueryBuilder>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `fakeQueryBuilder()` and `queueQueryBuilders()` -- the double of a typeorm `SelectQueryBuilder` (and its insert, update
|
|
4
|
+
* and delete siblings) that `mockEntityManager` hands out from `createQueryBuilder(Entity, "alias")`.
|
|
5
|
+
*
|
|
6
|
+
* A lite service reads with `createQueryBuilder(Entity, "alias")` and named parameters (persistence.md BE-PERSISTENCE-4).
|
|
7
|
+
* The fake is a Proxy: every chained method (`where`, `andWhere`, `innerJoin`, `orderBy`, `take`, `setLock`, `returning`,
|
|
8
|
+
* ...) records `{ method, args }` and returns the builder; a terminal method (`execute`, `getMany`, `getOne`,
|
|
9
|
+
* `getRawMany`, `getRawOne`, `getCount`, `getManyAndCount`) records too and resolves the answer the spec gave it (a function
|
|
10
|
+
* answer is called when the terminal runs). A spec asserts the statement it expects through `calls` or `callsOf(method)`.
|
|
11
|
+
*
|
|
12
|
+
* `queueQueryBuilders(em, answers)` queues one fake per answer on `em.createQueryBuilder` of a `mockEntityManager()`, in
|
|
13
|
+
* call order, and returns the fakes so the spec can read each one's calls.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.queueQueryBuilders = exports.fakeQueryBuilder = void 0;
|
|
17
|
+
/** The methods that end a chain and resolve the spec's answer. */
|
|
18
|
+
const TERMINALS = ["execute", "getMany", "getOne", "getRawMany", "getRawOne", "getCount", "getManyAndCount"];
|
|
19
|
+
const isTerminal = (method) => TERMINALS.some((name) => name === method);
|
|
20
|
+
/** Builds a chainable query-builder double: every method returns the builder, the terminal methods resolve `answers`. */
|
|
21
|
+
const fakeQueryBuilder = (answers = {}) => {
|
|
22
|
+
const calls = [];
|
|
23
|
+
const builder = new Proxy({}, {
|
|
24
|
+
get: (_target, property) => {
|
|
25
|
+
// `then` stays undefined so an awaited builder is not mistaken for a promise.
|
|
26
|
+
if (typeof property !== "string" || property === "then")
|
|
27
|
+
return undefined;
|
|
28
|
+
return (...args) => {
|
|
29
|
+
calls.push({ method: property, args });
|
|
30
|
+
if (!isTerminal(property))
|
|
31
|
+
return builder;
|
|
32
|
+
const answer = answers[property];
|
|
33
|
+
return Promise.resolve(typeof answer === "function" ? answer() : answer);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
builder,
|
|
39
|
+
calls,
|
|
40
|
+
callsOf: (method) => calls.filter((call) => call.method === method).map((call) => call.args),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
exports.fakeQueryBuilder = fakeQueryBuilder;
|
|
44
|
+
/** Queues one fake query builder per answer on `manager.createQueryBuilder`, handed out in call order, and returns them. */
|
|
45
|
+
const queueQueryBuilders = (manager, answers) => {
|
|
46
|
+
const fakes = answers.map((answer) => (0, exports.fakeQueryBuilder)(answer));
|
|
47
|
+
for (const fake of fakes)
|
|
48
|
+
manager.createQueryBuilder.mockReturnValueOnce(fake.builder);
|
|
49
|
+
return fakes;
|
|
50
|
+
};
|
|
51
|
+
exports.queueQueryBuilders = queueQueryBuilders;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/jest-preset",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "The StarCi jest config for Nest repositories (unit, integration, e2e and contract projects, ts-jest with diagnostics off, per-file 100 coverage of *.service.ts only) and the one shared unit-test double kit: typed mock<T>(), mockEntityManager(), fakeTransaction(), fakeCache(), fakeLock(), recordingEventBus(), recordingQueueOutbox(), fakeInbox(), FakeClock, fakeIds(), builder() and the Outcome matchers toBeRefused/toSucceedWith.",
|
|
3
|
+
"version": "2.5.1",
|
|
4
|
+
"description": "The StarCi jest config for Nest repositories (unit, integration, e2e and contract projects, ts-jest with diagnostics off, per-file 100 coverage of *.service.ts only) and the one shared unit-test double kit: typed mock<T>(), mockEntityManager(), fakeTransaction(), fakeQueryBuilder(), queueQueryBuilders(), fakeCache(), fakeLock(), recordingEventBus(), recordingQueueOutbox(), fakeInbox(), FakeClock, fakeIds(), builder() and the Outcome matchers toBeRefused/toSucceedWith.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "starci183",
|
|
7
7
|
"homepage": "https://github.com/starci183/starci-harness/tree/main/packages/jest-preset#readme",
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"types": "./dist/entity-manager.d.ts",
|
|
35
35
|
"default": "./dist/entity-manager.js"
|
|
36
36
|
},
|
|
37
|
+
"./query-builder": {
|
|
38
|
+
"types": "./dist/query-builder.d.ts",
|
|
39
|
+
"default": "./dist/query-builder.js"
|
|
40
|
+
},
|
|
37
41
|
"./ids": {
|
|
38
42
|
"types": "./dist/ids.d.ts",
|
|
39
43
|
"default": "./dist/ids.js"
|