@endora-commerce/test-kit 0.100.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 +108 -0
- package/dist/database/index.d.ts +17 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +17 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/lease.d.ts +115 -0
- package/dist/database/lease.d.ts.map +1 -0
- package/dist/database/lease.js +204 -0
- package/dist/database/lease.js.map +1 -0
- package/dist/database/provision.d.ts +135 -0
- package/dist/database/provision.d.ts.map +1 -0
- package/dist/database/provision.js +457 -0
- package/dist/database/provision.js.map +1 -0
- package/dist/database/run-isolation.d.ts +356 -0
- package/dist/database/run-isolation.d.ts.map +1 -0
- package/dist/database/run-isolation.js +490 -0
- package/dist/database/run-isolation.js.map +1 -0
- package/dist/server/compose-test-server.d.ts +251 -0
- package/dist/server/compose-test-server.d.ts.map +1 -0
- package/dist/server/compose-test-server.js +359 -0
- package/dist/server/compose-test-server.js.map +1 -0
- package/dist/server/composition.d.ts +112 -0
- package/dist/server/composition.d.ts.map +1 -0
- package/dist/server/composition.js +32 -0
- package/dist/server/composition.js.map +1 -0
- package/dist/server/index.d.ts +13 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +12 -0
- package/dist/server/index.js.map +1 -0
- package/dist/support/entity-index.d.ts +91 -0
- package/dist/support/entity-index.d.ts.map +1 -0
- package/dist/support/entity-index.js +56 -0
- package/dist/support/entity-index.js.map +1 -0
- package/dist/support/index.d.ts +134 -0
- package/dist/support/index.d.ts.map +1 -0
- package/dist/support/index.js +153 -0
- package/dist/support/index.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PlatformComposition` — what a caller hands the kit (feature 109, contract
|
|
3
|
+
* R2.1).
|
|
4
|
+
*
|
|
5
|
+
* ## The inversion, and why it is the whole feature
|
|
6
|
+
*
|
|
7
|
+
* `backend/test/helpers/test-server.ts` composes a platform by *finding* one:
|
|
8
|
+
* it imports the generated module list, the application's ORM configuration,
|
|
9
|
+
* the resolved manifest registry, the overlay loader and the package loader —
|
|
10
|
+
* seven bindings, every one of them a fact about **this repository's tree**
|
|
11
|
+
* (§2.1). A package cannot have any of them, so a harness that reaches for them
|
|
12
|
+
* cannot be a package, and a module's server-bound test therefore had nowhere
|
|
13
|
+
* to run but `backend/test`.
|
|
14
|
+
*
|
|
15
|
+
* So the kit is *handed* its composition and never builds one. That is the same
|
|
16
|
+
* inversion the platform already applies to every other host value: a
|
|
17
|
+
* composition root registers `redis`, `eventBus` and `processRunsWorkers`
|
|
18
|
+
* above the compose call because no module defaults them (AGENTS.md
|
|
19
|
+
* § Composition item 8), and the composition itself is one more of those.
|
|
20
|
+
*
|
|
21
|
+
* ## What the kit deliberately will not do (R2.2)
|
|
22
|
+
*
|
|
23
|
+
* It does not read `process.env.DEPLOYMENT`, walk `node_modules` or read a
|
|
24
|
+
* generated artefact. Every one of those is a fact about the caller's process —
|
|
25
|
+
* D-104's predicate — and a kit that answered them would answer them
|
|
26
|
+
* differently from the platform that composes for real. Discovery lives with
|
|
27
|
+
* the host, which is where `loadOverlayModuleEntries` and
|
|
28
|
+
* `loadPackageModuleEntries` already are; a second copy in the kit would be a
|
|
29
|
+
* second answer waiting to disagree.
|
|
30
|
+
*/
|
|
31
|
+
import type { MikroORM } from '@mikro-orm/postgresql';
|
|
32
|
+
import type { ModuleManifest } from '@endora-commerce/contracts';
|
|
33
|
+
import type { composeModules } from '@endora-commerce/platform/composition';
|
|
34
|
+
import type { TestSupportContribution } from '../support/index.js';
|
|
35
|
+
/**
|
|
36
|
+
* One composed module, named through `composeModules`' own signature.
|
|
37
|
+
*
|
|
38
|
+
* `ModuleEntry` is not one of the 27 symbols `./composition` carries and this
|
|
39
|
+
* feature widens no barrel (R3.1), so the type is taken from the function that
|
|
40
|
+
* *is* published rather than by publishing a second symbol to spell it. It
|
|
41
|
+
* stays correct by construction: a field added to the entry is a field here in
|
|
42
|
+
* the same compile.
|
|
43
|
+
*/
|
|
44
|
+
type ModuleEntry = Parameters<typeof composeModules>[0][number];
|
|
45
|
+
/**
|
|
46
|
+
* How this composition opens and closes its ORM.
|
|
47
|
+
*
|
|
48
|
+
* A pair rather than an instance, for the reason the harness's own
|
|
49
|
+
* `initOrm`/`closeOrm` are a pair: the ORM configuration captures
|
|
50
|
+
* `DATABASE_URL` at import, so *when* it is opened is part of what the caller
|
|
51
|
+
* decides. The application supplies its own; a module package's test supplies
|
|
52
|
+
* one built over the entity classes its installed set declares.
|
|
53
|
+
*/
|
|
54
|
+
export interface TestOrmLifecycle {
|
|
55
|
+
open(): Promise<MikroORM>;
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One entry of the loaded manifest registry, as the kit reads it.
|
|
60
|
+
*
|
|
61
|
+
* **Structural on purpose.** The application's own entry type carries a file
|
|
62
|
+
* path, an origin, install hooks and the operator commands a module declares —
|
|
63
|
+
* none of which the composition seam needs, and all of which are host-owned
|
|
64
|
+
* (`backend/src/lifecycle/registered-manifests.ts`, which a package may not
|
|
65
|
+
* name). Declaring the one member the kit reads keeps the application's array
|
|
66
|
+
* assignable without the kit knowing what else is on it.
|
|
67
|
+
*/
|
|
68
|
+
export interface ComposedManifestEntry {
|
|
69
|
+
readonly manifest: ModuleManifest;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The four things only the caller knows.
|
|
73
|
+
*
|
|
74
|
+
* Each member is one of §2.1's rows, and the table there is the normative
|
|
75
|
+
* statement of what belongs here: a fifth member would have to be something a
|
|
76
|
+
* *stranger's* test could not answer, and there is nothing left in that class.
|
|
77
|
+
*/
|
|
78
|
+
export interface PlatformComposition {
|
|
79
|
+
/**
|
|
80
|
+
* The module entries to compose — this repository's `MODULES` plus its
|
|
81
|
+
* overlay and its discovered packages, or a third party's own set.
|
|
82
|
+
*
|
|
83
|
+
* A composition lacking a module that declares `activation.nonDeactivatable`
|
|
84
|
+
* is refused by `composeModules` before the first module registers (issue
|
|
85
|
+
* #258), and the kit adds no second check and swallows no refusal (R2.4): a
|
|
86
|
+
* stranger composing an incomplete set gets the platform's own sentence,
|
|
87
|
+
* naming the module, the reason its manifest gives and the remedy.
|
|
88
|
+
*/
|
|
89
|
+
readonly modules: readonly ModuleEntry[];
|
|
90
|
+
/** An opener and a closer, per {@link TestOrmLifecycle}. */
|
|
91
|
+
readonly orm: TestOrmLifecycle;
|
|
92
|
+
/**
|
|
93
|
+
* The loaded manifest registry the lifecycle needs.
|
|
94
|
+
*
|
|
95
|
+
* It is what the required-module set, the activation declarations and the
|
|
96
|
+
* seeded enabled set are all derived from — three derivations from one input,
|
|
97
|
+
* on every composition, so a caller that withdraws a module changes all three
|
|
98
|
+
* in the same run and there is no list anywhere (D-100).
|
|
99
|
+
*/
|
|
100
|
+
readonly manifests: readonly ComposedManifestEntry[];
|
|
101
|
+
/**
|
|
102
|
+
* The test-support contributions of **exactly the modules in `modules`**,
|
|
103
|
+
* collected by the caller (contract §4).
|
|
104
|
+
*
|
|
105
|
+
* The kit does not discover them for the same reason it does not discover
|
|
106
|
+
* modules. In Phase 1b only `registrations` is applied; `volatileTables` and
|
|
107
|
+
* `seed` are declared and not yet collected — see `../support/index.ts`.
|
|
108
|
+
*/
|
|
109
|
+
readonly testSupport?: readonly TestSupportContribution[];
|
|
110
|
+
}
|
|
111
|
+
export {};
|
|
112
|
+
//# sourceMappingURL=composition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../../src/server/composition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEnE;;;;;;;;GAQG;AACH,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;CACnC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;OASG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IACzC,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAC;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACrD;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;CAC3D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PlatformComposition` — what a caller hands the kit (feature 109, contract
|
|
3
|
+
* R2.1).
|
|
4
|
+
*
|
|
5
|
+
* ## The inversion, and why it is the whole feature
|
|
6
|
+
*
|
|
7
|
+
* `backend/test/helpers/test-server.ts` composes a platform by *finding* one:
|
|
8
|
+
* it imports the generated module list, the application's ORM configuration,
|
|
9
|
+
* the resolved manifest registry, the overlay loader and the package loader —
|
|
10
|
+
* seven bindings, every one of them a fact about **this repository's tree**
|
|
11
|
+
* (§2.1). A package cannot have any of them, so a harness that reaches for them
|
|
12
|
+
* cannot be a package, and a module's server-bound test therefore had nowhere
|
|
13
|
+
* to run but `backend/test`.
|
|
14
|
+
*
|
|
15
|
+
* So the kit is *handed* its composition and never builds one. That is the same
|
|
16
|
+
* inversion the platform already applies to every other host value: a
|
|
17
|
+
* composition root registers `redis`, `eventBus` and `processRunsWorkers`
|
|
18
|
+
* above the compose call because no module defaults them (AGENTS.md
|
|
19
|
+
* § Composition item 8), and the composition itself is one more of those.
|
|
20
|
+
*
|
|
21
|
+
* ## What the kit deliberately will not do (R2.2)
|
|
22
|
+
*
|
|
23
|
+
* It does not read `process.env.DEPLOYMENT`, walk `node_modules` or read a
|
|
24
|
+
* generated artefact. Every one of those is a fact about the caller's process —
|
|
25
|
+
* D-104's predicate — and a kit that answered them would answer them
|
|
26
|
+
* differently from the platform that composes for real. Discovery lives with
|
|
27
|
+
* the host, which is where `loadOverlayModuleEntries` and
|
|
28
|
+
* `loadPackageModuleEntries` already are; a second copy in the kit would be a
|
|
29
|
+
* second answer waiting to disagree.
|
|
30
|
+
*/
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=composition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/server/composition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `./server` — compose a platform for a test, and take it down again
|
|
3
|
+
* (feature 109, R1.1).
|
|
4
|
+
*
|
|
5
|
+
* `composeTestServer` and `teardownTestServer`, and the two types they take and
|
|
6
|
+
* return. Everything the caller supplies is `PlatformComposition`
|
|
7
|
+
* (`./composition.ts`) plus the four hooks the boot order makes load-bearing;
|
|
8
|
+
* everything the kit owns is on `TestServerHandle`, and **not one field of it
|
|
9
|
+
* names a module**.
|
|
10
|
+
*/
|
|
11
|
+
export { composeTestServer, teardownTestServer, type ComposedPlatformContext, type ComposeTestServerOptions, type TestPlatformContext, type TestServerHandle, } from './compose-test-server.js';
|
|
12
|
+
export type { ComposedManifestEntry, PlatformComposition, TestOrmLifecycle, } from './composition.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAElC,YAAY,EACV,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `./server` — compose a platform for a test, and take it down again
|
|
3
|
+
* (feature 109, R1.1).
|
|
4
|
+
*
|
|
5
|
+
* `composeTestServer` and `teardownTestServer`, and the two types they take and
|
|
6
|
+
* return. Everything the caller supplies is `PlatformComposition`
|
|
7
|
+
* (`./composition.ts`) plus the four hooks the boot order makes load-bearing;
|
|
8
|
+
* everything the kit owns is on `TestServerHandle`, and **not one field of it
|
|
9
|
+
* names a module**.
|
|
10
|
+
*/
|
|
11
|
+
export { composeTestServer, teardownTestServer, } from './compose-test-server.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,iBAAiB,EACjB,kBAAkB,GAKnB,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entity index — its **type** and its **lookup**, and no module's name
|
|
3
|
+
* (feature 109, T065; contract R2.2, FR-001).
|
|
4
|
+
*
|
|
5
|
+
* ## The one fact this file is built around not knowing
|
|
6
|
+
*
|
|
7
|
+
* A server-bound test writes rows, and to write a row it needs the entity
|
|
8
|
+
* **class the ORM registered** — not a structurally identical copy read out of a
|
|
9
|
+
* package's source, which is a class the ORM never discovered (D-160.6.1). A
|
|
10
|
+
* module package publishes one `entities` array and **no entity class by name**
|
|
11
|
+
* (D-168), so the class is picked out of that array by name, and the array comes
|
|
12
|
+
* off the package's own `./backend`.
|
|
13
|
+
*
|
|
14
|
+
* Which packages are there is where this stops being a package's business.
|
|
15
|
+
* *Which modules one deployment installed* is the single fact a kit that may
|
|
16
|
+
* name no module is forbidden to know (R2.2, FR-001), and it is why
|
|
17
|
+
* `backend/test/helpers/package-entities.ts` is permanently host-owned and may
|
|
18
|
+
* not move here — `specs/084-small-f4-package-layout/contracts/module-package-layout.md`
|
|
19
|
+
* R10's closing paragraph, which asks in as many words that this not be
|
|
20
|
+
* re-opened on import volume.
|
|
21
|
+
*
|
|
22
|
+
* So the population arrives as an **argument**, exactly as
|
|
23
|
+
* `PlatformComposition.modules` does, and it is rendered by the host's own
|
|
24
|
+
* generator — `@endora-commerce/cli/lib/entity-index-artefact.js`, run by
|
|
25
|
+
* `endora generate` in an instance and by `composer:generate` in this
|
|
26
|
+
* repository. One derivation, two populations
|
|
27
|
+
* (`contracts/instance-repository.md` R3.5).
|
|
28
|
+
*
|
|
29
|
+
* ## Why the lookup delegates rather than re-implements
|
|
30
|
+
*
|
|
31
|
+
* `entityNamed` is `@endora-commerce/platform/packages`', and two
|
|
32
|
+
* implementations of one lookup are two answers waiting to disagree about what a
|
|
33
|
+
* missing name does. What this file adds is the **module** dimension the
|
|
34
|
+
* platform's lookup has no opinion about: the index is keyed by module id, so a
|
|
35
|
+
* name that is absent has two different causes — the module is not installed, or
|
|
36
|
+
* the module publishes no such class — and they have different remedies. A host
|
|
37
|
+
* that gets `Cannot read properties of undefined` for the first has been told
|
|
38
|
+
* nothing.
|
|
39
|
+
*/
|
|
40
|
+
import type { EntityClass } from '@mikro-orm/core';
|
|
41
|
+
import { type EntityRowTypeIsRequired } from '@endora-commerce/platform/packages';
|
|
42
|
+
/**
|
|
43
|
+
* What a published `entities` array holds.
|
|
44
|
+
*
|
|
45
|
+
* A construct signature rather than `unknown`, for the platform lookup's own
|
|
46
|
+
* reason: it refuses an argument that is not an array of classes at all, which
|
|
47
|
+
* is the shape a package that lost its array answers with — the host reads a
|
|
48
|
+
* missing `entities` export as `[]` and reports nothing.
|
|
49
|
+
*/
|
|
50
|
+
type PublishedEntityClass = abstract new (...args: never[]) => object;
|
|
51
|
+
/**
|
|
52
|
+
* The index's type, and the whole of what the kit may say about its content.
|
|
53
|
+
*
|
|
54
|
+
* Keyed by **module id** — `endora.id`, the identity of record (D-142) — and
|
|
55
|
+
* never by package name: a test names the module it is testing, and the package
|
|
56
|
+
* name is a fact about how that module was delivered.
|
|
57
|
+
*
|
|
58
|
+
* The kit declares this shape and never a member of it. A union of the module
|
|
59
|
+
* ids that happen to exist today would be the list R2.2 forbids, and it would
|
|
60
|
+
* make a stranger's own module unnameable in the index their own host rendered.
|
|
61
|
+
*/
|
|
62
|
+
export type InstalledEntityIndex = Readonly<Record<string, readonly PublishedEntityClass[]>>;
|
|
63
|
+
/**
|
|
64
|
+
* One entity class out of an installed module's published array, with the row
|
|
65
|
+
* type the caller supplies.
|
|
66
|
+
*
|
|
67
|
+
* The row type is **required** and its default is unsatisfiable, which is the
|
|
68
|
+
* platform lookup's decision and is inherited here rather than restated: with
|
|
69
|
+
* `T` free, `em.create` checks the payload against `object` and accepts
|
|
70
|
+
* everything, which is worse than the constructor-union collapse the mechanism
|
|
71
|
+
* exists for. A caller takes it from an `import type` of the module's own
|
|
72
|
+
* `./test-support`, which publishes the row types `export type`-only for exactly
|
|
73
|
+
* this (R10 property 3).
|
|
74
|
+
*/
|
|
75
|
+
export declare function entityNamedIn<T extends object = EntityRowTypeIsRequired>(index: InstalledEntityIndex, moduleId: string, name: string): EntityClass<T>;
|
|
76
|
+
/**
|
|
77
|
+
* A module id the index does not carry.
|
|
78
|
+
*
|
|
79
|
+
* Its own class rather than a bare `Error` because the two causes of "no such
|
|
80
|
+
* entity" have different remedies and a caller may legitimately branch on which
|
|
81
|
+
* it got: this one means *this host composed a different set of modules*, and
|
|
82
|
+
* the platform lookup's means *that module publishes no such class*.
|
|
83
|
+
*/
|
|
84
|
+
export declare class ModuleNotInstalledError extends Error {
|
|
85
|
+
readonly moduleId: string;
|
|
86
|
+
readonly installed: readonly string[];
|
|
87
|
+
readonly name = "ModuleNotInstalledError";
|
|
88
|
+
constructor(moduleId: string, installed: readonly string[]);
|
|
89
|
+
}
|
|
90
|
+
export {};
|
|
91
|
+
//# sourceMappingURL=entity-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-index.d.ts","sourceRoot":"","sources":["../../src/support/entity-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAe,KAAK,uBAAuB,EAAE,MAAM,oCAAoC,CAAC;AAE/F;;;;;;;GAOG;AACH,KAAK,oBAAoB,GAAG,QAAQ,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,MAAM,CAAC;AAEtE;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,oBAAoB,EAAE,CAAC,CAAC,CAAC;AAE7F;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,uBAAuB,EACtE,KAAK,EAAE,oBAAoB,EAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,WAAW,CAAC,CAAC,CAAC,CAiBhB;AASD;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAI9C,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE;IAJvC,SAAkB,IAAI,6BAA6B;gBAGxC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,MAAM,EAAE;CAUxC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { entityNamed } from '@endora-commerce/platform/packages';
|
|
2
|
+
/**
|
|
3
|
+
* One entity class out of an installed module's published array, with the row
|
|
4
|
+
* type the caller supplies.
|
|
5
|
+
*
|
|
6
|
+
* The row type is **required** and its default is unsatisfiable, which is the
|
|
7
|
+
* platform lookup's decision and is inherited here rather than restated: with
|
|
8
|
+
* `T` free, `em.create` checks the payload against `object` and accepts
|
|
9
|
+
* everything, which is worse than the constructor-union collapse the mechanism
|
|
10
|
+
* exists for. A caller takes it from an `import type` of the module's own
|
|
11
|
+
* `./test-support`, which publishes the row types `export type`-only for exactly
|
|
12
|
+
* this (R10 property 3).
|
|
13
|
+
*/
|
|
14
|
+
export function entityNamedIn(index, moduleId, name) {
|
|
15
|
+
// `Object.hasOwn`, not a truthiness test: the index is a plain object a
|
|
16
|
+
// generator rendered, so `index['toString']` answers with a function off
|
|
17
|
+
// `Object.prototype` — a truthy value where an array belongs, and on
|
|
18
|
+
// `constructor` a value that would reach the lookup and fail somewhere else.
|
|
19
|
+
if (!Object.hasOwn(index, moduleId)) {
|
|
20
|
+
throw new ModuleNotInstalledError(moduleId, installedModuleIds(index));
|
|
21
|
+
}
|
|
22
|
+
const published = index[moduleId];
|
|
23
|
+
if (!Array.isArray(published)) {
|
|
24
|
+
throw new ModuleNotInstalledError(moduleId, installedModuleIds(index));
|
|
25
|
+
}
|
|
26
|
+
return entityNamed(published, name, `the installed module '${moduleId}'`);
|
|
27
|
+
}
|
|
28
|
+
/** Every module id the index carries, sorted, so a refusal reads the same way twice. */
|
|
29
|
+
function installedModuleIds(index) {
|
|
30
|
+
return Object.keys(index)
|
|
31
|
+
.filter((key) => Object.hasOwn(index, key))
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A module id the index does not carry.
|
|
36
|
+
*
|
|
37
|
+
* Its own class rather than a bare `Error` because the two causes of "no such
|
|
38
|
+
* entity" have different remedies and a caller may legitimately branch on which
|
|
39
|
+
* it got: this one means *this host composed a different set of modules*, and
|
|
40
|
+
* the platform lookup's means *that module publishes no such class*.
|
|
41
|
+
*/
|
|
42
|
+
export class ModuleNotInstalledError extends Error {
|
|
43
|
+
moduleId;
|
|
44
|
+
installed;
|
|
45
|
+
name = 'ModuleNotInstalledError';
|
|
46
|
+
constructor(moduleId, installed) {
|
|
47
|
+
super(`the entity index carries no module '${moduleId}', so no entity class of it can be ` +
|
|
48
|
+
`named. This host installed: ${installed.length === 0 ? '(no module at all)' : installed.join(', ')}. ` +
|
|
49
|
+
`The index is rendered from the packages this host installed, so either install ` +
|
|
50
|
+
`'${moduleId}' and re-run the generator, or name a module that is in the list — a ` +
|
|
51
|
+
`stale index is a test asking the ORM about a table nothing mapped.`);
|
|
52
|
+
this.moduleId = moduleId;
|
|
53
|
+
this.installed = installed;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=entity-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-index.js","sourceRoot":"","sources":["../../src/support/entity-index.ts"],"names":[],"mappings":"AAyCA,OAAO,EAAE,WAAW,EAAgC,MAAM,oCAAoC,CAAC;AAyB/F;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAC3B,KAA2B,EAC3B,QAAgB,EAChB,IAAY;IAEZ,wEAAwE;IACxE,yEAAyE;IACzE,qEAAqE;IACrE,6EAA6E;IAC7E,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,uBAAuB,CAAC,QAAQ,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,uBAAuB,CAAC,QAAQ,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,WAAW,CAChB,SAA4C,EAC5C,IAAI,EACJ,yBAAyB,QAAQ,GAAG,CACrC,CAAC;AACJ,CAAC;AAED,wFAAwF;AACxF,SAAS,kBAAkB,CAAC,KAA2B;IACrD,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;SACtB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;SAC1C,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAIrC;IACA;IAJO,IAAI,GAAG,yBAAyB,CAAC;IAEnD,YACW,QAAgB,EAChB,SAA4B;QAErC,KAAK,CACH,uCAAuC,QAAQ,qCAAqC;YAClF,+BAA+B,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YACvG,iFAAiF;YACjF,IAAI,QAAQ,uEAAuE;YACnF,oEAAoE,CACvE,CAAC;QATO,aAAQ,GAAR,QAAQ,CAAQ;QAChB,cAAS,GAAT,SAAS,CAAmB;IASvC,CAAC;CACF"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `./support` — what a module contributes to a test composition (feature 109,
|
|
3
|
+
* contract §4).
|
|
4
|
+
*
|
|
5
|
+
* A module package that needs test support declares it at
|
|
6
|
+
* `src/test-support/index.ts` and publishes it as `./test-support`; the caller
|
|
7
|
+
* collects the declarations of **the modules it composed** and hands them to
|
|
8
|
+
* `composeTestServer` as `PlatformComposition.testSupport`. The kit never
|
|
9
|
+
* discovers them: discovery walks `node_modules`, which is a fact about the
|
|
10
|
+
* caller's process (R2.2).
|
|
11
|
+
*
|
|
12
|
+
* ## What is here in Phase 1b, and what is not
|
|
13
|
+
*
|
|
14
|
+
* The **declaration shape** and the **registration** member, because
|
|
15
|
+
* `PlatformComposition` takes contributions and a type it could not name would
|
|
16
|
+
* make that member `unknown`. The three collectors — the truncate ordering
|
|
17
|
+
* derived from the foreign-key graph (R4.2), the per-module seed decomposition
|
|
18
|
+
* (R4.6) and the fixture re-export — are Phase 3, and the members they read are
|
|
19
|
+
* declared here now so that a module author writing a `./test-support` layer
|
|
20
|
+
* writes one declaration rather than two.
|
|
21
|
+
*
|
|
22
|
+
* `volatileTables` and `seed` are therefore **declared and not yet collected**,
|
|
23
|
+
* which is stated rather than left to be discovered: `composeTestServer`
|
|
24
|
+
* applies `registrations` and reads neither, and a module that declares tables
|
|
25
|
+
* today gets no truncate from them. The alternative — leaving them out of the
|
|
26
|
+
* type until the collector exists — would make every module that ships a
|
|
27
|
+
* contribution in Phase 3 edit a declaration it had already written.
|
|
28
|
+
*
|
|
29
|
+
* ## The entity index is here too, and it is the same inversion
|
|
30
|
+
*
|
|
31
|
+
* `./entity-index.js` carries the **type** of a host's entity index and the
|
|
32
|
+
* **lookup** over it, and no module's name (T065). It is on this subpath rather
|
|
33
|
+
* than a new one because it is the same kind of thing as everything above: a
|
|
34
|
+
* shape the kit declares and a population the caller supplies. Its own header
|
|
35
|
+
* is where the reasoning is.
|
|
36
|
+
*/
|
|
37
|
+
export { entityNamedIn, ModuleNotInstalledError, type InstalledEntityIndex, } from './entity-index.js';
|
|
38
|
+
/**
|
|
39
|
+
* One module's test support.
|
|
40
|
+
*
|
|
41
|
+
* Four optional members and nothing else (R4.1). Every one of them is that
|
|
42
|
+
* module's own policy: the taxonomy fetcher that refuses and the PIM client
|
|
43
|
+
* that throws exist to make a test which reaches the network fail loudly, and
|
|
44
|
+
* that is `product_feeds`' and `pim_ergonode`' decision rather than a harness's.
|
|
45
|
+
*/
|
|
46
|
+
export interface TestSupportContribution {
|
|
47
|
+
/**
|
|
48
|
+
* The module this contribution belongs to. It is the module's own manifest
|
|
49
|
+
* id, and the kit uses it for one thing: naming the module in a refusal.
|
|
50
|
+
*/
|
|
51
|
+
readonly moduleId: string;
|
|
52
|
+
/**
|
|
53
|
+
* Container registrations this module's tests substitute by default, keyed by
|
|
54
|
+
* the **registration name its owner registers** — the same key
|
|
55
|
+
* `composedModules.contribute` takes.
|
|
56
|
+
*
|
|
57
|
+
* A module may not contribute over a name it does not own (R4.4), for the
|
|
58
|
+
* reason D-176 and issue #203 give about decoration: a substitution rewrites
|
|
59
|
+
* what every consumer of that name resolves. A test that needs to substitute
|
|
60
|
+
* *another* module's collaborator does it at the call site, in its own file,
|
|
61
|
+
* through `composeTestServer`'s own `contribute` option — where the coupling
|
|
62
|
+
* is visible in the test that wanted it.
|
|
63
|
+
*/
|
|
64
|
+
readonly registrations?: Readonly<Record<string, unknown>>;
|
|
65
|
+
/**
|
|
66
|
+
* The tables this module owns whose rows must not survive between tests.
|
|
67
|
+
*
|
|
68
|
+
* A **set, not a sequence** (R4.2): the order they are emptied in is derived
|
|
69
|
+
* from the foreign-key graph, and a set the derivation cannot order is a
|
|
70
|
+
* failure naming the cycle. A hand-written order is a fact about the schema
|
|
71
|
+
* that the schema can state for itself.
|
|
72
|
+
*/
|
|
73
|
+
readonly volatileTables?: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* Rows this module contributes to a named seed.
|
|
76
|
+
*
|
|
77
|
+
* Additive and order-independent (R4.6): a seed whose result depends on the
|
|
78
|
+
* order two modules contributed in is a defect, not a configuration.
|
|
79
|
+
*/
|
|
80
|
+
readonly seed?: Readonly<Record<string, (em: unknown) => Promise<void>>>;
|
|
81
|
+
/** Anything else this module's tests build, re-exported for its own use. */
|
|
82
|
+
readonly fixtures?: Readonly<Record<string, unknown>>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Every registration the given contributions substitute, merged.
|
|
86
|
+
*
|
|
87
|
+
* Refuses two modules claiming one name rather than letting the last one win:
|
|
88
|
+
* a substitution the reader cannot attribute is the shape D-176 refuses for
|
|
89
|
+
* decoration, and "whichever was collected last" is not an answer anybody can
|
|
90
|
+
* act on.
|
|
91
|
+
*/
|
|
92
|
+
export declare function mergeTestSupportRegistrations(contributions: readonly TestSupportContribution[]): Readonly<Record<string, unknown>>;
|
|
93
|
+
/** Two modules substituting one registration name. */
|
|
94
|
+
export declare class ConflictingTestSupportError extends Error {
|
|
95
|
+
readonly registrationName: string;
|
|
96
|
+
readonly firstModuleId: string;
|
|
97
|
+
readonly secondModuleId: string;
|
|
98
|
+
constructor(registrationName: string, firstModuleId: string, secondModuleId: string);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Every table the given contributions declare volatile, deduped and sorted.
|
|
102
|
+
*
|
|
103
|
+
* **Sorted rather than ordered** (R4.2). The declaration is a set, and the
|
|
104
|
+
* ordering the contract asks the foreign-key graph to derive is not needed by
|
|
105
|
+
* the one consumer there is: the caller empties them with a single
|
|
106
|
+
* `truncate table … cascade`, which is one statement over the whole set and
|
|
107
|
+
* takes no order — PostgreSQL resolves the dependencies itself, which is why the
|
|
108
|
+
* harness's hand-written children-first list has always been a comment rather
|
|
109
|
+
* than a mechanism. A sorted set makes the result a function of the composition
|
|
110
|
+
* and not of the order the contributions were collected in, which is the
|
|
111
|
+
* property R4.2 is actually protecting. The day a consumer empties them one
|
|
112
|
+
* statement at a time, the derivation the contract describes is what it has to
|
|
113
|
+
* take, and this function is where it goes.
|
|
114
|
+
*
|
|
115
|
+
* Two refusals, both about a declaration rather than about a statement: two
|
|
116
|
+
* modules claiming one table, and a name that is not an identifier. The second
|
|
117
|
+
* matters because the collected set is interpolated into SQL by its caller, so a
|
|
118
|
+
* quote or a semicolon has to be refused where it is written down.
|
|
119
|
+
*/
|
|
120
|
+
export declare function collectVolatileTables(contributions: readonly TestSupportContribution[]): readonly string[];
|
|
121
|
+
/** Two modules declaring one table volatile. */
|
|
122
|
+
export declare class ConflictingVolatileTableError extends Error {
|
|
123
|
+
readonly tableName: string;
|
|
124
|
+
readonly firstModuleId: string;
|
|
125
|
+
readonly secondModuleId: string;
|
|
126
|
+
constructor(tableName: string, firstModuleId: string, secondModuleId: string);
|
|
127
|
+
}
|
|
128
|
+
/** A declared volatile table whose name is not an identifier. */
|
|
129
|
+
export declare class UndeclarableVolatileTableError extends Error {
|
|
130
|
+
readonly moduleId: string;
|
|
131
|
+
readonly tableName: string;
|
|
132
|
+
constructor(moduleId: string, tableName: string);
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/support/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzE,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,EAAE,SAAS,uBAAuB,EAAE,GAChD,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAcnC;AAED,sDAAsD;AACtD,qBAAa,2BAA4B,SAAQ,KAAK;IAElD,QAAQ,CAAC,gBAAgB,EAAE,MAAM;IACjC,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM;gBAFtB,gBAAgB,EAAE,MAAM,EACxB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM;CAWlC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,SAAS,uBAAuB,EAAE,GAChD,SAAS,MAAM,EAAE,CAiBnB;AAQD,gDAAgD;AAChD,qBAAa,6BAA8B,SAAQ,KAAK;IAEpD,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM;gBAFtB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM;CAWlC;AAED,iEAAiE;AACjE,qBAAa,8BAA+B,SAAQ,KAAK;IAErD,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM;gBADjB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM;CAW7B"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `./support` — what a module contributes to a test composition (feature 109,
|
|
3
|
+
* contract §4).
|
|
4
|
+
*
|
|
5
|
+
* A module package that needs test support declares it at
|
|
6
|
+
* `src/test-support/index.ts` and publishes it as `./test-support`; the caller
|
|
7
|
+
* collects the declarations of **the modules it composed** and hands them to
|
|
8
|
+
* `composeTestServer` as `PlatformComposition.testSupport`. The kit never
|
|
9
|
+
* discovers them: discovery walks `node_modules`, which is a fact about the
|
|
10
|
+
* caller's process (R2.2).
|
|
11
|
+
*
|
|
12
|
+
* ## What is here in Phase 1b, and what is not
|
|
13
|
+
*
|
|
14
|
+
* The **declaration shape** and the **registration** member, because
|
|
15
|
+
* `PlatformComposition` takes contributions and a type it could not name would
|
|
16
|
+
* make that member `unknown`. The three collectors — the truncate ordering
|
|
17
|
+
* derived from the foreign-key graph (R4.2), the per-module seed decomposition
|
|
18
|
+
* (R4.6) and the fixture re-export — are Phase 3, and the members they read are
|
|
19
|
+
* declared here now so that a module author writing a `./test-support` layer
|
|
20
|
+
* writes one declaration rather than two.
|
|
21
|
+
*
|
|
22
|
+
* `volatileTables` and `seed` are therefore **declared and not yet collected**,
|
|
23
|
+
* which is stated rather than left to be discovered: `composeTestServer`
|
|
24
|
+
* applies `registrations` and reads neither, and a module that declares tables
|
|
25
|
+
* today gets no truncate from them. The alternative — leaving them out of the
|
|
26
|
+
* type until the collector exists — would make every module that ships a
|
|
27
|
+
* contribution in Phase 3 edit a declaration it had already written.
|
|
28
|
+
*
|
|
29
|
+
* ## The entity index is here too, and it is the same inversion
|
|
30
|
+
*
|
|
31
|
+
* `./entity-index.js` carries the **type** of a host's entity index and the
|
|
32
|
+
* **lookup** over it, and no module's name (T065). It is on this subpath rather
|
|
33
|
+
* than a new one because it is the same kind of thing as everything above: a
|
|
34
|
+
* shape the kit declares and a population the caller supplies. Its own header
|
|
35
|
+
* is where the reasoning is.
|
|
36
|
+
*/
|
|
37
|
+
export { entityNamedIn, ModuleNotInstalledError, } from './entity-index.js';
|
|
38
|
+
/**
|
|
39
|
+
* Every registration the given contributions substitute, merged.
|
|
40
|
+
*
|
|
41
|
+
* Refuses two modules claiming one name rather than letting the last one win:
|
|
42
|
+
* a substitution the reader cannot attribute is the shape D-176 refuses for
|
|
43
|
+
* decoration, and "whichever was collected last" is not an answer anybody can
|
|
44
|
+
* act on.
|
|
45
|
+
*/
|
|
46
|
+
export function mergeTestSupportRegistrations(contributions) {
|
|
47
|
+
const merged = {};
|
|
48
|
+
const claimedBy = new Map();
|
|
49
|
+
for (const contribution of contributions) {
|
|
50
|
+
for (const [name, value] of Object.entries(contribution.registrations ?? {})) {
|
|
51
|
+
const owner = claimedBy.get(name);
|
|
52
|
+
if (owner !== undefined) {
|
|
53
|
+
throw new ConflictingTestSupportError(name, owner, contribution.moduleId);
|
|
54
|
+
}
|
|
55
|
+
claimedBy.set(name, contribution.moduleId);
|
|
56
|
+
merged[name] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
|
61
|
+
/** Two modules substituting one registration name. */
|
|
62
|
+
export class ConflictingTestSupportError extends Error {
|
|
63
|
+
registrationName;
|
|
64
|
+
firstModuleId;
|
|
65
|
+
secondModuleId;
|
|
66
|
+
constructor(registrationName, firstModuleId, secondModuleId) {
|
|
67
|
+
super(`test support for '${registrationName}' is contributed by both '${firstModuleId}' and ` +
|
|
68
|
+
`'${secondModuleId}'. A registration has one owner, and a substitution rewrites what ` +
|
|
69
|
+
`every consumer of that name resolves — so only the module that registers it may ` +
|
|
70
|
+
`substitute it by default. A test that needs the other module's substitution asks for ` +
|
|
71
|
+
`it at its own call site.`);
|
|
72
|
+
this.registrationName = registrationName;
|
|
73
|
+
this.firstModuleId = firstModuleId;
|
|
74
|
+
this.secondModuleId = secondModuleId;
|
|
75
|
+
this.name = 'ConflictingTestSupportError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Every table the given contributions declare volatile, deduped and sorted.
|
|
80
|
+
*
|
|
81
|
+
* **Sorted rather than ordered** (R4.2). The declaration is a set, and the
|
|
82
|
+
* ordering the contract asks the foreign-key graph to derive is not needed by
|
|
83
|
+
* the one consumer there is: the caller empties them with a single
|
|
84
|
+
* `truncate table … cascade`, which is one statement over the whole set and
|
|
85
|
+
* takes no order — PostgreSQL resolves the dependencies itself, which is why the
|
|
86
|
+
* harness's hand-written children-first list has always been a comment rather
|
|
87
|
+
* than a mechanism. A sorted set makes the result a function of the composition
|
|
88
|
+
* and not of the order the contributions were collected in, which is the
|
|
89
|
+
* property R4.2 is actually protecting. The day a consumer empties them one
|
|
90
|
+
* statement at a time, the derivation the contract describes is what it has to
|
|
91
|
+
* take, and this function is where it goes.
|
|
92
|
+
*
|
|
93
|
+
* Two refusals, both about a declaration rather than about a statement: two
|
|
94
|
+
* modules claiming one table, and a name that is not an identifier. The second
|
|
95
|
+
* matters because the collected set is interpolated into SQL by its caller, so a
|
|
96
|
+
* quote or a semicolon has to be refused where it is written down.
|
|
97
|
+
*/
|
|
98
|
+
export function collectVolatileTables(contributions) {
|
|
99
|
+
const claimedBy = new Map();
|
|
100
|
+
for (const contribution of contributions) {
|
|
101
|
+
for (const table of contribution.volatileTables ?? []) {
|
|
102
|
+
if (!VALID_TABLE_NAME.test(table)) {
|
|
103
|
+
throw new UndeclarableVolatileTableError(contribution.moduleId, table);
|
|
104
|
+
}
|
|
105
|
+
const owner = claimedBy.get(table);
|
|
106
|
+
// One module naming its own table twice has made no claim it had not
|
|
107
|
+
// already made; only a *second* module is a conflict.
|
|
108
|
+
if (owner !== undefined && owner !== contribution.moduleId) {
|
|
109
|
+
throw new ConflictingVolatileTableError(table, owner, contribution.moduleId);
|
|
110
|
+
}
|
|
111
|
+
claimedBy.set(table, contribution.moduleId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...claimedBy.keys()].sort();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* What a table name may be: the unquoted PostgreSQL identifier every migration
|
|
118
|
+
* in this repository writes. Anything else is refused rather than quoted around.
|
|
119
|
+
*/
|
|
120
|
+
const VALID_TABLE_NAME = /^[a-z_][a-z0-9_]*$/;
|
|
121
|
+
/** Two modules declaring one table volatile. */
|
|
122
|
+
export class ConflictingVolatileTableError extends Error {
|
|
123
|
+
tableName;
|
|
124
|
+
firstModuleId;
|
|
125
|
+
secondModuleId;
|
|
126
|
+
constructor(tableName, firstModuleId, secondModuleId) {
|
|
127
|
+
super(`the table '${tableName}' is declared volatile by both '${firstModuleId}' and ` +
|
|
128
|
+
`'${secondModuleId}'. A table has one owner (R4.3), and emptying another module's ` +
|
|
129
|
+
`table between tests deletes rows that module's own tests rely on — so only the ` +
|
|
130
|
+
`module that owns it may declare it. A test that needs a neighbour's table emptied ` +
|
|
131
|
+
`does it in its own file, where the coupling is visible.`);
|
|
132
|
+
this.tableName = tableName;
|
|
133
|
+
this.firstModuleId = firstModuleId;
|
|
134
|
+
this.secondModuleId = secondModuleId;
|
|
135
|
+
this.name = 'ConflictingVolatileTableError';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** A declared volatile table whose name is not an identifier. */
|
|
139
|
+
export class UndeclarableVolatileTableError extends Error {
|
|
140
|
+
moduleId;
|
|
141
|
+
tableName;
|
|
142
|
+
constructor(moduleId, tableName) {
|
|
143
|
+
super(`'${moduleId}' declares the volatile table '${tableName}', which is not an unquoted ` +
|
|
144
|
+
`PostgreSQL identifier${tableName.trim() === '' ? ' (it is empty)' : ''}. The collected ` +
|
|
145
|
+
`set is interpolated into a \`truncate table … cascade\` statement, so a name carrying ` +
|
|
146
|
+
`a quote, a semicolon or nothing at all is refused where it is written down rather ` +
|
|
147
|
+
`than where it would run.`);
|
|
148
|
+
this.moduleId = moduleId;
|
|
149
|
+
this.tableName = tableName;
|
|
150
|
+
this.name = 'UndeclarableVolatileTableError';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/support/index.ts"],"names":[],"mappings":"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"}
|