@geekmidas/constructs 10.0.0-alpha.21 → 10.0.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -23
- package/dist/{Endpoint-BxkRWQ8G.d.mts → Endpoint-Bmi2-PBN.d.mts} +2 -2
- package/dist/{Endpoint-BxkRWQ8G.d.mts.map → Endpoint-Bmi2-PBN.d.mts.map} +1 -1
- package/dist/{Endpoint-DbYQJgb-.d.cts → Endpoint-CjvraSfp.d.cts} +2 -2
- package/dist/{Endpoint-DbYQJgb-.d.cts.map → Endpoint-CjvraSfp.d.cts.map} +1 -1
- package/dist/{EndpointFactory-T06kTFHg.d.mts → EndpointFactory-BFfvgko6.d.mts} +2 -2
- package/dist/{EndpointFactory-DE6nKtFZ.d.cts.map → EndpointFactory-BFfvgko6.d.mts.map} +1 -1
- package/dist/{EndpointFactory-DE6nKtFZ.d.cts → EndpointFactory-vfC3NiJi.d.cts} +2 -2
- package/dist/{EndpointFactory-T06kTFHg.d.mts.map → EndpointFactory-vfC3NiJi.d.cts.map} +1 -1
- package/dist/Queue-BB2-DgqJ.cjs +123 -0
- package/dist/Queue-BB2-DgqJ.cjs.map +1 -0
- package/dist/Queue-Dfumm1WR.mjs +118 -0
- package/dist/Queue-Dfumm1WR.mjs.map +1 -0
- package/dist/Topic-BaNCgP61.cjs +107 -0
- package/dist/Topic-BaNCgP61.cjs.map +1 -0
- package/dist/Topic-QqnO_6Dl.mjs +102 -0
- package/dist/Topic-QqnO_6Dl.mjs.map +1 -0
- package/dist/adaptors/aws.d.cts +1 -1
- package/dist/adaptors/aws.d.mts +1 -1
- package/dist/adaptors/hono.d.cts +1 -1
- package/dist/adaptors/hono.d.mts +1 -1
- package/dist/adaptors/testing.cjs +136 -32
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts +94 -13
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +94 -13
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +133 -33
- package/dist/adaptors/testing.mjs.map +1 -1
- package/dist/{auth-BlO8MUkR.d.cts → auth-C_fHD1We.d.cts} +9 -1
- package/dist/{auth-BlO8MUkR.d.cts.map → auth-C_fHD1We.d.cts.map} +1 -1
- package/dist/{auth-CHWIdcQO.d.mts → auth-D_Ntp1Nj.d.mts} +9 -1
- package/dist/{auth-CHWIdcQO.d.mts.map → auth-D_Ntp1Nj.d.mts.map} +1 -1
- package/dist/auth.cjs +15 -2
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.mts +1 -1
- package/dist/auth.mjs +15 -2
- package/dist/auth.mjs.map +1 -1
- package/dist/endpoints/index.d.cts +2 -2
- package/dist/endpoints/index.d.mts +2 -2
- package/dist/queue/index.cjs +3 -116
- package/dist/queue/index.cjs.map +1 -1
- package/dist/queue/index.mjs +1 -114
- package/dist/queue/index.mjs.map +1 -1
- package/dist/rest-api.d.cts +2 -2
- package/dist/rest-api.d.mts +2 -2
- package/dist/topic/index.cjs +3 -100
- package/dist/topic/index.cjs.map +1 -1
- package/dist/topic/index.mjs +1 -98
- package/dist/topic/index.mjs.map +1 -1
- package/package.json +31 -31
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const require_Construct = require("./Construct-yG9Sw-5p.cjs");
|
|
2
|
+
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
3
|
+
let _geekmidas_logger_console = require("@geekmidas/logger/console");
|
|
4
|
+
let _geekmidas_events = require("@geekmidas/events");
|
|
5
|
+
//#region src/topic/Topic.ts
|
|
6
|
+
/**
|
|
7
|
+
* A topic — pub/sub fan-out. Unlike a `Queue` (point-to-point, one consumer), a
|
|
8
|
+
* topic is a *resource* owned by no single handler: it declares the event
|
|
9
|
+
* contract, derives a typed producer (`topic.publisher`), and any number of
|
|
10
|
+
* subscribers (`s.topic(topic)`) bind to it. `gkm build` discovers it into the
|
|
11
|
+
* manifest's `topics` field; infra provisions an SNS topic.
|
|
12
|
+
*
|
|
13
|
+
* This replaces hand-writing a publisher `Service`: the publisher is *derived*
|
|
14
|
+
* from the declared event contract, the same way `Queue` derives its publisher.
|
|
15
|
+
*/
|
|
16
|
+
var Topic = class extends require_Construct.Construct {
|
|
17
|
+
name;
|
|
18
|
+
eventSchemas;
|
|
19
|
+
__IS_TOPIC__ = true;
|
|
20
|
+
static isTopic(obj) {
|
|
21
|
+
return Boolean(obj && obj.__IS_TOPIC__ === true && obj.type === "dev.geekmidas.resource.topic");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The canonical id this topic is declared under.
|
|
25
|
+
*
|
|
26
|
+
* Derived from the name, so `users` and `Users` are one topic. The name is
|
|
27
|
+
* left alone: subscribers bind to it and it is what the broker routes on.
|
|
28
|
+
*/
|
|
29
|
+
id;
|
|
30
|
+
/**
|
|
31
|
+
* The producer's env key, read by both {@link declare} and
|
|
32
|
+
* {@link publisher} so the two cannot drift.
|
|
33
|
+
*/
|
|
34
|
+
connectionKey;
|
|
35
|
+
constructor(name, eventSchemas, logger = _geekmidas_logger_console.DEFAULT_LOGGER) {
|
|
36
|
+
super("dev.geekmidas.resource.topic", logger, [], []);
|
|
37
|
+
this.name = name;
|
|
38
|
+
this.eventSchemas = eventSchemas;
|
|
39
|
+
this.id = (0, _geekmidas_manifest.canonicalId)(name);
|
|
40
|
+
this.connectionKey = (0, _geekmidas_manifest.provideKey)(this.id, "publisherConnectionString");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What this topic is in the manifest.
|
|
44
|
+
*
|
|
45
|
+
* Only the producer's key: a subscriber is *bound* to a topic rather than
|
|
46
|
+
* depending on it, so the binding is an edge the deploy target reads and not
|
|
47
|
+
* an env key anyone can hold. Locally both sides meet on the same broker.
|
|
48
|
+
*/
|
|
49
|
+
declare() {
|
|
50
|
+
return [{
|
|
51
|
+
kind: "topic",
|
|
52
|
+
id: this.id,
|
|
53
|
+
provides: [this.connectionKey],
|
|
54
|
+
events: this.eventTypes,
|
|
55
|
+
subscribers: []
|
|
56
|
+
}];
|
|
57
|
+
}
|
|
58
|
+
/** The event type names this topic carries. */
|
|
59
|
+
get eventTypes() {
|
|
60
|
+
return Object.keys(this.eventSchemas);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The producer side — a `Service` exposing an `EventPublisher` typed to the
|
|
64
|
+
* union of this topic's events. Inject via `.publisher(topic.publisher)` (for
|
|
65
|
+
* declarative `.event(...)`) or `.services([topic.publisher])` (to publish
|
|
66
|
+
* imperatively). Reads `<NAME>_PUBLISHER_CONNECTION_STRING` and selects the
|
|
67
|
+
* transport from the URL protocol — `pgboss://` locally, `sns://` deployed.
|
|
68
|
+
*
|
|
69
|
+
* Because it's a `Service`, the connection-string requirement is sniffed into
|
|
70
|
+
* the manifest of whatever construct injects it (least-privilege linking).
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* The topic as a dependency — what `.dependsOn([users])` dissolves into,
|
|
74
|
+
* reachable as `services.users`.
|
|
75
|
+
*
|
|
76
|
+
* It is the producer, because publishing is the only thing depending on a
|
|
77
|
+
* topic can mean: a subscriber *binds* with `s.topic(…)` instead, and is
|
|
78
|
+
* deliberately never handed this. {@link publisher} is the same service
|
|
79
|
+
* under its older `<name>Publisher` key.
|
|
80
|
+
*/
|
|
81
|
+
get service() {
|
|
82
|
+
const { register } = this.publisher;
|
|
83
|
+
return {
|
|
84
|
+
serviceName: (0, _geekmidas_manifest.serviceKey)(this.id),
|
|
85
|
+
register
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
get publisher() {
|
|
89
|
+
const envVar = this.connectionKey;
|
|
90
|
+
return {
|
|
91
|
+
serviceName: `${this.name}Publisher`,
|
|
92
|
+
async register({ envParser }) {
|
|
93
|
+
const { connectionString } = envParser.create((get) => ({ connectionString: get(envVar).string() })).parse();
|
|
94
|
+
return _geekmidas_events.Publisher.fromConnectionString(connectionString);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
Object.defineProperty(exports, "Topic", {
|
|
101
|
+
enumerable: true,
|
|
102
|
+
get: function() {
|
|
103
|
+
return Topic;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=Topic-BaNCgP61.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Topic-BaNCgP61.cjs","names":["Construct","DEFAULT_LOGGER","canonicalId","provideKey","serviceKey","Publisher"],"sources":["../src/topic/Topic.ts"],"sourcesContent":["import {\n\ttype EventPublisher,\n\ttype EventPublisherConnectionString,\n\ttype PublishableMessage,\n\tPublisher,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport { DEFAULT_LOGGER } from '@geekmidas/logger/console';\nimport {\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { Construct, ConstructType } from '../Construct';\n\n/** A topic's event contract — a map of event type → payload schema. */\nexport type TopicEvents = Record<string, StandardSchemaV1>;\n\n/**\n * The union of wire messages a topic carries, derived from its event map:\n * `{ type: 'user.created'; payload: … } | { type: 'user.updated'; payload: … }`.\n */\nexport type TopicMessage<TEvents extends TopicEvents> = {\n\t[K in keyof TEvents & string]: PublishableMessage<\n\t\tK,\n\t\tInferStandardSchema<TEvents[K]>\n\t>;\n}[keyof TEvents & string];\n\n/**\n * A topic — pub/sub fan-out. Unlike a `Queue` (point-to-point, one consumer), a\n * topic is a *resource* owned by no single handler: it declares the event\n * contract, derives a typed producer (`topic.publisher`), and any number of\n * subscribers (`s.topic(topic)`) bind to it. `gkm build` discovers it into the\n * manifest's `topics` field; infra provisions an SNS topic.\n *\n * This replaces hand-writing a publisher `Service`: the publisher is *derived*\n * from the declared event contract, the same way `Queue` derives its publisher.\n */\nexport class Topic<\n\tTName extends string = string,\n\tTEvents extends TopicEvents = TopicEvents,\n> extends Construct {\n\t__IS_TOPIC__ = true;\n\n\tstatic isTopic(obj: unknown): obj is Topic<string, TopicEvents> {\n\t\treturn Boolean(\n\t\t\tobj &&\n\t\t\t\t(obj as { __IS_TOPIC__?: boolean }).__IS_TOPIC__ === true &&\n\t\t\t\t(obj as Construct).type === ConstructType.Topic,\n\t\t);\n\t}\n\n\t/**\n\t * The canonical id this topic is declared under.\n\t *\n\t * Derived from the name, so `users` and `Users` are one topic. The name is\n\t * left alone: subscribers bind to it and it is what the broker routes on.\n\t */\n\treadonly id: string;\n\n\t/**\n\t * The producer's env key, read by both {@link declare} and\n\t * {@link publisher} so the two cannot drift.\n\t */\n\tprivate readonly connectionKey: string;\n\n\tconstructor(\n\t\tpublic readonly name: TName,\n\t\t/**\n\t\t * The event contract — a map of event type → payload schema. Named\n\t\t * `eventSchemas` (not `events`) to avoid clashing with `Construct.events`,\n\t\t * which is the array of `MappedEvent`s a construct *publishes*.\n\t\t */\n\t\tpublic readonly eventSchemas: TEvents,\n\t\tlogger: Logger = DEFAULT_LOGGER,\n\t) {\n\t\tsuper(ConstructType.Topic, logger, [], []);\n\n\t\tthis.id = canonicalId(name);\n\t\tthis.connectionKey = provideKey(this.id, 'publisherConnectionString');\n\t}\n\n\t/**\n\t * What this topic is in the manifest.\n\t *\n\t * Only the producer's key: a subscriber is *bound* to a topic rather than\n\t * depending on it, so the binding is an edge the deploy target reads and not\n\t * an env key anyone can hold. Locally both sides meet on the same broker.\n\t */\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'topic',\n\t\t\t\tid: this.id,\n\t\t\t\tprovides: [this.connectionKey],\n\t\t\t\t// The contract this topic carries. Structural: which events exist\n\t\t\t\t// is a fact about the application, and a subscriber binding to one\n\t\t\t\t// that does not is a mistake worth catching at build.\n\t\t\t\tevents: this.eventTypes,\n\t\t\t\t// Bound rather than declared here — a subscriber names the topic,\n\t\t\t\t// not the other way round — so the build fills these from what it\n\t\t\t\t// found. Nested when it does, because position is the trigger.\n\t\t\t\tsubscribers: [],\n\t\t\t},\n\t\t];\n\t}\n\n\t/** The event type names this topic carries. */\n\tget eventTypes(): (keyof TEvents & string)[] {\n\t\treturn Object.keys(this.eventSchemas) as (keyof TEvents & string)[];\n\t}\n\n\t/**\n\t * The producer side — a `Service` exposing an `EventPublisher` typed to the\n\t * union of this topic's events. Inject via `.publisher(topic.publisher)` (for\n\t * declarative `.event(...)`) or `.services([topic.publisher])` (to publish\n\t * imperatively). Reads `<NAME>_PUBLISHER_CONNECTION_STRING` and selects the\n\t * transport from the URL protocol — `pgboss://` locally, `sns://` deployed.\n\t *\n\t * Because it's a `Service`, the connection-string requirement is sniffed into\n\t * the manifest of whatever construct injects it (least-privilege linking).\n\t */\n\t/**\n\t * The topic as a dependency — what `.dependsOn([users])` dissolves into,\n\t * reachable as `services.users`.\n\t *\n\t * It is the producer, because publishing is the only thing depending on a\n\t * topic can mean: a subscriber *binds* with `s.topic(…)` instead, and is\n\t * deliberately never handed this. {@link publisher} is the same service\n\t * under its older `<name>Publisher` key.\n\t */\n\tget service(): Service<\n\t\tUncapitalize<TName>,\n\t\tEventPublisher<TopicMessage<TEvents>>\n\t> {\n\t\tconst { register } = this.publisher;\n\n\t\treturn {\n\t\t\tserviceName: serviceKey(this.id) as Uncapitalize<TName>,\n\t\t\tregister,\n\t\t};\n\t}\n\n\tget publisher(): Service<\n\t\t`${TName}Publisher`,\n\t\tEventPublisher<TopicMessage<TEvents>>\n\t> {\n\t\tconst envVar = this.connectionKey;\n\t\treturn {\n\t\t\tserviceName: `${this.name}Publisher`,\n\t\t\tasync register({ envParser }) {\n\t\t\t\tconst { connectionString } = envParser\n\t\t\t\t\t.create((get) => ({\n\t\t\t\t\t\tconnectionString: get(envVar).string(),\n\t\t\t\t\t}))\n\t\t\t\t\t.parse();\n\n\t\t\t\treturn Publisher.fromConnectionString<TopicMessage<TEvents>>(\n\t\t\t\t\tconnectionString as EventPublisherConnectionString,\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;AA2CA,IAAa,QAAb,cAGUA,kBAAAA,UAAU;CA0BF;CAMA;CA/BjB,eAAe;CAEf,OAAO,QAAQ,KAAiD;EAC/D,OAAO,QACN,OACE,IAAmC,iBAAiB,QACpD,IAAkB,SAAA,8BACrB;CACD;;;;;;;CAQA;;;;;CAMA;CAEA,YACC,MAMA,cACA,SAAiBC,0BAAAA,gBAChB;EACD,MAAA,gCAA2B,QAAQ,CAAC,GAAG,CAAC,CAAC;EATzB,KAAA,OAAA;EAMA,KAAA,eAAA;EAKhB,KAAK,MAAA,GAAKC,oBAAAA,YAAAA,CAAY,IAAI;EAC1B,KAAK,iBAAA,GAAgBC,oBAAAA,WAAAA,CAAW,KAAK,IAAI,2BAA2B;CACrE;;;;;;;;CASA,UAAyB;EACxB,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GACT,UAAU,CAAC,KAAK,aAAa;GAI7B,QAAQ,KAAK;GAIb,aAAa,CAAC;EACf,CACD;CACD;;CAGA,IAAI,aAAyC;EAC5C,OAAO,OAAO,KAAK,KAAK,YAAY;CACrC;;;;;;;;;;;;;;;;;;;;CAqBA,IAAI,UAGF;EACD,MAAM,EAAE,aAAa,KAAK;EAE1B,OAAO;GACN,cAAA,GAAaC,oBAAAA,WAAAA,CAAW,KAAK,EAAE;GAC/B;EACD;CACD;CAEA,IAAI,YAGF;EACD,MAAM,SAAS,KAAK;EACpB,OAAO;GACN,aAAa,GAAG,KAAK,KAAK;GAC1B,MAAM,SAAS,EAAE,aAAa;IAC7B,MAAM,EAAE,qBAAqB,UAC3B,QAAQ,SAAS,EACjB,kBAAkB,IAAI,MAAM,CAAC,CAAC,OAAO,EACtC,EAAE,CAAC,CACF,MAAM;IAER,OAAOC,kBAAAA,UAAU,qBAChB,gBACD;GACD;EACD;CACD;AACD"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { t as Construct } from "./Construct-Detz65RA.mjs";
|
|
2
|
+
import { canonicalId, provideKey, serviceKey } from "@geekmidas/manifest";
|
|
3
|
+
import { DEFAULT_LOGGER } from "@geekmidas/logger/console";
|
|
4
|
+
import { Publisher } from "@geekmidas/events";
|
|
5
|
+
//#region src/topic/Topic.ts
|
|
6
|
+
/**
|
|
7
|
+
* A topic — pub/sub fan-out. Unlike a `Queue` (point-to-point, one consumer), a
|
|
8
|
+
* topic is a *resource* owned by no single handler: it declares the event
|
|
9
|
+
* contract, derives a typed producer (`topic.publisher`), and any number of
|
|
10
|
+
* subscribers (`s.topic(topic)`) bind to it. `gkm build` discovers it into the
|
|
11
|
+
* manifest's `topics` field; infra provisions an SNS topic.
|
|
12
|
+
*
|
|
13
|
+
* This replaces hand-writing a publisher `Service`: the publisher is *derived*
|
|
14
|
+
* from the declared event contract, the same way `Queue` derives its publisher.
|
|
15
|
+
*/
|
|
16
|
+
var Topic = class extends Construct {
|
|
17
|
+
name;
|
|
18
|
+
eventSchemas;
|
|
19
|
+
__IS_TOPIC__ = true;
|
|
20
|
+
static isTopic(obj) {
|
|
21
|
+
return Boolean(obj && obj.__IS_TOPIC__ === true && obj.type === "dev.geekmidas.resource.topic");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The canonical id this topic is declared under.
|
|
25
|
+
*
|
|
26
|
+
* Derived from the name, so `users` and `Users` are one topic. The name is
|
|
27
|
+
* left alone: subscribers bind to it and it is what the broker routes on.
|
|
28
|
+
*/
|
|
29
|
+
id;
|
|
30
|
+
/**
|
|
31
|
+
* The producer's env key, read by both {@link declare} and
|
|
32
|
+
* {@link publisher} so the two cannot drift.
|
|
33
|
+
*/
|
|
34
|
+
connectionKey;
|
|
35
|
+
constructor(name, eventSchemas, logger = DEFAULT_LOGGER) {
|
|
36
|
+
super("dev.geekmidas.resource.topic", logger, [], []);
|
|
37
|
+
this.name = name;
|
|
38
|
+
this.eventSchemas = eventSchemas;
|
|
39
|
+
this.id = canonicalId(name);
|
|
40
|
+
this.connectionKey = provideKey(this.id, "publisherConnectionString");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What this topic is in the manifest.
|
|
44
|
+
*
|
|
45
|
+
* Only the producer's key: a subscriber is *bound* to a topic rather than
|
|
46
|
+
* depending on it, so the binding is an edge the deploy target reads and not
|
|
47
|
+
* an env key anyone can hold. Locally both sides meet on the same broker.
|
|
48
|
+
*/
|
|
49
|
+
declare() {
|
|
50
|
+
return [{
|
|
51
|
+
kind: "topic",
|
|
52
|
+
id: this.id,
|
|
53
|
+
provides: [this.connectionKey],
|
|
54
|
+
events: this.eventTypes,
|
|
55
|
+
subscribers: []
|
|
56
|
+
}];
|
|
57
|
+
}
|
|
58
|
+
/** The event type names this topic carries. */
|
|
59
|
+
get eventTypes() {
|
|
60
|
+
return Object.keys(this.eventSchemas);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The producer side — a `Service` exposing an `EventPublisher` typed to the
|
|
64
|
+
* union of this topic's events. Inject via `.publisher(topic.publisher)` (for
|
|
65
|
+
* declarative `.event(...)`) or `.services([topic.publisher])` (to publish
|
|
66
|
+
* imperatively). Reads `<NAME>_PUBLISHER_CONNECTION_STRING` and selects the
|
|
67
|
+
* transport from the URL protocol — `pgboss://` locally, `sns://` deployed.
|
|
68
|
+
*
|
|
69
|
+
* Because it's a `Service`, the connection-string requirement is sniffed into
|
|
70
|
+
* the manifest of whatever construct injects it (least-privilege linking).
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* The topic as a dependency — what `.dependsOn([users])` dissolves into,
|
|
74
|
+
* reachable as `services.users`.
|
|
75
|
+
*
|
|
76
|
+
* It is the producer, because publishing is the only thing depending on a
|
|
77
|
+
* topic can mean: a subscriber *binds* with `s.topic(…)` instead, and is
|
|
78
|
+
* deliberately never handed this. {@link publisher} is the same service
|
|
79
|
+
* under its older `<name>Publisher` key.
|
|
80
|
+
*/
|
|
81
|
+
get service() {
|
|
82
|
+
const { register } = this.publisher;
|
|
83
|
+
return {
|
|
84
|
+
serviceName: serviceKey(this.id),
|
|
85
|
+
register
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
get publisher() {
|
|
89
|
+
const envVar = this.connectionKey;
|
|
90
|
+
return {
|
|
91
|
+
serviceName: `${this.name}Publisher`,
|
|
92
|
+
async register({ envParser }) {
|
|
93
|
+
const { connectionString } = envParser.create((get) => ({ connectionString: get(envVar).string() })).parse();
|
|
94
|
+
return Publisher.fromConnectionString(connectionString);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
export { Topic as t };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=Topic-QqnO_6Dl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Topic-QqnO_6Dl.mjs","names":[],"sources":["../src/topic/Topic.ts"],"sourcesContent":["import {\n\ttype EventPublisher,\n\ttype EventPublisherConnectionString,\n\ttype PublishableMessage,\n\tPublisher,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport { DEFAULT_LOGGER } from '@geekmidas/logger/console';\nimport {\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { Construct, ConstructType } from '../Construct';\n\n/** A topic's event contract — a map of event type → payload schema. */\nexport type TopicEvents = Record<string, StandardSchemaV1>;\n\n/**\n * The union of wire messages a topic carries, derived from its event map:\n * `{ type: 'user.created'; payload: … } | { type: 'user.updated'; payload: … }`.\n */\nexport type TopicMessage<TEvents extends TopicEvents> = {\n\t[K in keyof TEvents & string]: PublishableMessage<\n\t\tK,\n\t\tInferStandardSchema<TEvents[K]>\n\t>;\n}[keyof TEvents & string];\n\n/**\n * A topic — pub/sub fan-out. Unlike a `Queue` (point-to-point, one consumer), a\n * topic is a *resource* owned by no single handler: it declares the event\n * contract, derives a typed producer (`topic.publisher`), and any number of\n * subscribers (`s.topic(topic)`) bind to it. `gkm build` discovers it into the\n * manifest's `topics` field; infra provisions an SNS topic.\n *\n * This replaces hand-writing a publisher `Service`: the publisher is *derived*\n * from the declared event contract, the same way `Queue` derives its publisher.\n */\nexport class Topic<\n\tTName extends string = string,\n\tTEvents extends TopicEvents = TopicEvents,\n> extends Construct {\n\t__IS_TOPIC__ = true;\n\n\tstatic isTopic(obj: unknown): obj is Topic<string, TopicEvents> {\n\t\treturn Boolean(\n\t\t\tobj &&\n\t\t\t\t(obj as { __IS_TOPIC__?: boolean }).__IS_TOPIC__ === true &&\n\t\t\t\t(obj as Construct).type === ConstructType.Topic,\n\t\t);\n\t}\n\n\t/**\n\t * The canonical id this topic is declared under.\n\t *\n\t * Derived from the name, so `users` and `Users` are one topic. The name is\n\t * left alone: subscribers bind to it and it is what the broker routes on.\n\t */\n\treadonly id: string;\n\n\t/**\n\t * The producer's env key, read by both {@link declare} and\n\t * {@link publisher} so the two cannot drift.\n\t */\n\tprivate readonly connectionKey: string;\n\n\tconstructor(\n\t\tpublic readonly name: TName,\n\t\t/**\n\t\t * The event contract — a map of event type → payload schema. Named\n\t\t * `eventSchemas` (not `events`) to avoid clashing with `Construct.events`,\n\t\t * which is the array of `MappedEvent`s a construct *publishes*.\n\t\t */\n\t\tpublic readonly eventSchemas: TEvents,\n\t\tlogger: Logger = DEFAULT_LOGGER,\n\t) {\n\t\tsuper(ConstructType.Topic, logger, [], []);\n\n\t\tthis.id = canonicalId(name);\n\t\tthis.connectionKey = provideKey(this.id, 'publisherConnectionString');\n\t}\n\n\t/**\n\t * What this topic is in the manifest.\n\t *\n\t * Only the producer's key: a subscriber is *bound* to a topic rather than\n\t * depending on it, so the binding is an edge the deploy target reads and not\n\t * an env key anyone can hold. Locally both sides meet on the same broker.\n\t */\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'topic',\n\t\t\t\tid: this.id,\n\t\t\t\tprovides: [this.connectionKey],\n\t\t\t\t// The contract this topic carries. Structural: which events exist\n\t\t\t\t// is a fact about the application, and a subscriber binding to one\n\t\t\t\t// that does not is a mistake worth catching at build.\n\t\t\t\tevents: this.eventTypes,\n\t\t\t\t// Bound rather than declared here — a subscriber names the topic,\n\t\t\t\t// not the other way round — so the build fills these from what it\n\t\t\t\t// found. Nested when it does, because position is the trigger.\n\t\t\t\tsubscribers: [],\n\t\t\t},\n\t\t];\n\t}\n\n\t/** The event type names this topic carries. */\n\tget eventTypes(): (keyof TEvents & string)[] {\n\t\treturn Object.keys(this.eventSchemas) as (keyof TEvents & string)[];\n\t}\n\n\t/**\n\t * The producer side — a `Service` exposing an `EventPublisher` typed to the\n\t * union of this topic's events. Inject via `.publisher(topic.publisher)` (for\n\t * declarative `.event(...)`) or `.services([topic.publisher])` (to publish\n\t * imperatively). Reads `<NAME>_PUBLISHER_CONNECTION_STRING` and selects the\n\t * transport from the URL protocol — `pgboss://` locally, `sns://` deployed.\n\t *\n\t * Because it's a `Service`, the connection-string requirement is sniffed into\n\t * the manifest of whatever construct injects it (least-privilege linking).\n\t */\n\t/**\n\t * The topic as a dependency — what `.dependsOn([users])` dissolves into,\n\t * reachable as `services.users`.\n\t *\n\t * It is the producer, because publishing is the only thing depending on a\n\t * topic can mean: a subscriber *binds* with `s.topic(…)` instead, and is\n\t * deliberately never handed this. {@link publisher} is the same service\n\t * under its older `<name>Publisher` key.\n\t */\n\tget service(): Service<\n\t\tUncapitalize<TName>,\n\t\tEventPublisher<TopicMessage<TEvents>>\n\t> {\n\t\tconst { register } = this.publisher;\n\n\t\treturn {\n\t\t\tserviceName: serviceKey(this.id) as Uncapitalize<TName>,\n\t\t\tregister,\n\t\t};\n\t}\n\n\tget publisher(): Service<\n\t\t`${TName}Publisher`,\n\t\tEventPublisher<TopicMessage<TEvents>>\n\t> {\n\t\tconst envVar = this.connectionKey;\n\t\treturn {\n\t\t\tserviceName: `${this.name}Publisher`,\n\t\t\tasync register({ envParser }) {\n\t\t\t\tconst { connectionString } = envParser\n\t\t\t\t\t.create((get) => ({\n\t\t\t\t\t\tconnectionString: get(envVar).string(),\n\t\t\t\t\t}))\n\t\t\t\t\t.parse();\n\n\t\t\t\treturn Publisher.fromConnectionString<TopicMessage<TEvents>>(\n\t\t\t\t\tconnectionString as EventPublisherConnectionString,\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;AA2CA,IAAa,QAAb,cAGU,UAAU;CA0BF;CAMA;CA/BjB,eAAe;CAEf,OAAO,QAAQ,KAAiD;EAC/D,OAAO,QACN,OACE,IAAmC,iBAAiB,QACpD,IAAkB,SAAA,8BACrB;CACD;;;;;;;CAQA;;;;;CAMA;CAEA,YACC,MAMA,cACA,SAAiB,gBAChB;EACD,MAAA,gCAA2B,QAAQ,CAAC,GAAG,CAAC,CAAC;EATzB,KAAA,OAAA;EAMA,KAAA,eAAA;EAKhB,KAAK,KAAK,YAAY,IAAI;EAC1B,KAAK,gBAAgB,WAAW,KAAK,IAAI,2BAA2B;CACrE;;;;;;;;CASA,UAAyB;EACxB,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GACT,UAAU,CAAC,KAAK,aAAa;GAI7B,QAAQ,KAAK;GAIb,aAAa,CAAC;EACf,CACD;CACD;;CAGA,IAAI,aAAyC;EAC5C,OAAO,OAAO,KAAK,KAAK,YAAY;CACrC;;;;;;;;;;;;;;;;;;;;CAqBA,IAAI,UAGF;EACD,MAAM,EAAE,aAAa,KAAK;EAE1B,OAAO;GACN,aAAa,WAAW,KAAK,EAAE;GAC/B;EACD;CACD;CAEA,IAAI,YAGF;EACD,MAAM,SAAS,KAAK;EACpB,OAAO;GACN,aAAa,GAAG,KAAK,KAAK;GAC1B,MAAM,SAAS,EAAE,aAAa;IAC7B,MAAM,EAAE,qBAAqB,UAC3B,QAAQ,SAAS,EACjB,kBAAkB,IAAI,MAAM,CAAC,CAAC,OAAO,EACtC,EAAE,CAAC,CACF,MAAM;IAER,OAAO,UAAU,qBAChB,gBACD;GACD;EACD;CACD;AACD"}
|
package/dist/adaptors/aws.d.cts
CHANGED
|
@@ -3,7 +3,7 @@ import { a as FunctionHandler, r as Function$1 } from "../index-nBQT1tAh.cjs";
|
|
|
3
3
|
import { t as Cron } from "../Cron-Bdcg9OAL.cjs";
|
|
4
4
|
import { t as HttpMethod } from "../types-DMuSQeLL.cjs";
|
|
5
5
|
import { t as Telemetry } from "../telemetry-DGcFlVpB.cjs";
|
|
6
|
-
import { c as EndpointSchemas, i as Endpoint, r as CookieFn } from "../Endpoint-
|
|
6
|
+
import { c as EndpointSchemas, i as Endpoint, r as CookieFn } from "../Endpoint-CjvraSfp.cjs";
|
|
7
7
|
import { t as Queue } from "../Queue-Bhzi2WQC.cjs";
|
|
8
8
|
import { t as Subscriber } from "../Subscriber-DQCaiOXF.cjs";
|
|
9
9
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
package/dist/adaptors/aws.d.mts
CHANGED
|
@@ -3,7 +3,7 @@ import { a as FunctionHandler, r as Function$1 } from "../index-DU3Twmwh.mjs";
|
|
|
3
3
|
import { t as Cron } from "../Cron-AYpQq725.mjs";
|
|
4
4
|
import { t as HttpMethod } from "../types-DMuSQeLL.mjs";
|
|
5
5
|
import { t as Telemetry } from "../telemetry-DGcFlVpB.mjs";
|
|
6
|
-
import { c as EndpointSchemas, i as Endpoint, r as CookieFn } from "../Endpoint-
|
|
6
|
+
import { c as EndpointSchemas, i as Endpoint, r as CookieFn } from "../Endpoint-Bmi2-PBN.mjs";
|
|
7
7
|
import { t as Queue } from "../Queue-BoUA-e83.mjs";
|
|
8
8
|
import { t as Subscriber } from "../Subscriber-CIO3pwji.mjs";
|
|
9
9
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
package/dist/adaptors/hono.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as StandardSchemaV1 } from "../index-CeEtxDmK.cjs";
|
|
2
2
|
import { t as HttpMethod } from "../types-DMuSQeLL.cjs";
|
|
3
|
-
import { c as EndpointSchemas, i as Endpoint } from "../Endpoint-
|
|
3
|
+
import { c as EndpointSchemas, i as Endpoint } from "../Endpoint-CjvraSfp.cjs";
|
|
4
4
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
5
5
|
import { EventPublisher } from "@geekmidas/events";
|
|
6
6
|
import { Logger } from "@geekmidas/logger";
|
package/dist/adaptors/hono.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as StandardSchemaV1 } from "../index-CeEtxDmK.mjs";
|
|
2
2
|
import { t as HttpMethod } from "../types-DMuSQeLL.mjs";
|
|
3
|
-
import { c as EndpointSchemas, i as Endpoint } from "../Endpoint-
|
|
3
|
+
import { c as EndpointSchemas, i as Endpoint } from "../Endpoint-Bmi2-PBN.mjs";
|
|
4
4
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
5
5
|
import { EventPublisher } from "@geekmidas/events";
|
|
6
6
|
import { Service, ServiceDiscovery, ServiceRecord } from "@geekmidas/services";
|
|
@@ -5,6 +5,8 @@ const require_Endpoint = require("../Endpoint-CtiPUpRX.cjs");
|
|
|
5
5
|
const require_email = require("../email.cjs");
|
|
6
6
|
const require_database_kysely = require("../database/kysely.cjs");
|
|
7
7
|
const require_lazyAccessors = require("../lazyAccessors-Dx9NLwJZ.cjs");
|
|
8
|
+
const require_Queue = require("../Queue-BB2-DgqJ.cjs");
|
|
9
|
+
const require_Topic = require("../Topic-BaNCgP61.cjs");
|
|
8
10
|
const require_HonoEndpointAdaptor = require("../HonoEndpointAdaptor-C9aivwKl.cjs");
|
|
9
11
|
const require_processAudits = require("../processAudits-D_2W5x8k.cjs");
|
|
10
12
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
@@ -21,6 +23,8 @@ let _geekmidas_testkit_mailbox = require("@geekmidas/testkit/mailbox");
|
|
|
21
23
|
let _geekmidas_testkit_transactions = require("@geekmidas/testkit/transactions");
|
|
22
24
|
let msw_node = require("msw/node");
|
|
23
25
|
let vitest = require("vitest");
|
|
26
|
+
let node_fs = require("node:fs");
|
|
27
|
+
let node_url = require("node:url");
|
|
24
28
|
//#region src/endpoints/MswEndpointAdaptor.ts
|
|
25
29
|
const MSW_HTTP_METHODS = [
|
|
26
30
|
"get",
|
|
@@ -416,6 +420,38 @@ var TestSubscriberAdaptor = class TestSubscriberAdaptor {
|
|
|
416
420
|
}
|
|
417
421
|
};
|
|
418
422
|
//#endregion
|
|
423
|
+
//#region src/testing/manifest.ts
|
|
424
|
+
/**
|
|
425
|
+
* The test manifest: what `gkm test` knows about an app, written down for the
|
|
426
|
+
* suite it starts.
|
|
427
|
+
*
|
|
428
|
+
* `gkm test` discovers the constructs, reconciles the test stage and resolves
|
|
429
|
+
* its environment — then, until this existed, threw all of it away and left a
|
|
430
|
+
* test to declare it again by hand. The manifest keeps it: which construct and
|
|
431
|
+
* endpoint is exported where, and the environment the test stage resolved,
|
|
432
|
+
* under the keys the constructs themselves derive. A feature test is built from
|
|
433
|
+
* it, so nothing a test says can drift from what the app declares.
|
|
434
|
+
*/
|
|
435
|
+
/** The environment variable `gkm test` names the manifest in. */
|
|
436
|
+
const TEST_MANIFEST_ENV = "GKM_TEST_MANIFEST";
|
|
437
|
+
/**
|
|
438
|
+
* Read a test manifest: from a path, a `file:` URL, or — given nothing — the
|
|
439
|
+
* one `gkm test` named in `GKM_TEST_MANIFEST`.
|
|
440
|
+
*/
|
|
441
|
+
function loadTestManifest(from) {
|
|
442
|
+
const location = from ?? process.env["GKM_TEST_MANIFEST"];
|
|
443
|
+
if (!location) throw new NoTestManifest();
|
|
444
|
+
const path = location instanceof URL || location.startsWith("file:") ? (0, node_url.fileURLToPath)(location) : location;
|
|
445
|
+
return JSON.parse((0, node_fs.readFileSync)(path, "utf-8"));
|
|
446
|
+
}
|
|
447
|
+
/** A feature test ran without a manifest to build itself from. */
|
|
448
|
+
var NoTestManifest = class extends Error {
|
|
449
|
+
constructor() {
|
|
450
|
+
super(`No test manifest: run the suite with \`gkm test\`, which writes one and names it in ${TEST_MANIFEST_ENV}.`);
|
|
451
|
+
this.name = "NoTestManifest";
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
//#endregion
|
|
419
455
|
//#region src/testing/featureTest.ts
|
|
420
456
|
/**
|
|
421
457
|
* `featureTest`: an app, driven the way it runs deployed.
|
|
@@ -426,15 +462,14 @@ var TestSubscriberAdaptor = class TestSubscriberAdaptor {
|
|
|
426
462
|
* from the real handler, for the test that made the request. Everything else
|
|
427
463
|
* with a real local equivalent — Postgres, Mailpit, MinIO — is used directly.
|
|
428
464
|
*
|
|
465
|
+
* It is built from the test manifest `gkm test` writes — the constructs and
|
|
466
|
+
* endpoints the app exports, and the environment the test stage resolved — so
|
|
467
|
+
* a test declares none of it. `gkm test` also generates the configured `it`
|
|
468
|
+
* and a `Browser` with a typed client per surface:
|
|
469
|
+
*
|
|
429
470
|
* ```ts
|
|
430
|
-
*
|
|
431
|
-
* export const it = featureTest({
|
|
432
|
-
* modules: import.meta.glob(['../../constructs/*.ts', '../src/endpoints/**\/*.ts'], { eager: true }),
|
|
433
|
-
* database,
|
|
434
|
-
* browser: Browser,
|
|
435
|
-
* });
|
|
471
|
+
* import { it } from '#test';
|
|
436
472
|
*
|
|
437
|
-
* // a test
|
|
438
473
|
* it('shows the signed-in user their profile', async ({ browser, mailbox }) => {
|
|
439
474
|
* await browser.auth.signIn.magicLink({ email: 'ada@shop.test' });
|
|
440
475
|
* await browser.visit((await mailbox('ada@shop.test').last()).link!);
|
|
@@ -446,17 +481,16 @@ var TestSubscriberAdaptor = class TestSubscriberAdaptor {
|
|
|
446
481
|
const contexts = /* @__PURE__ */ new Map();
|
|
447
482
|
/** Databases already resolving to test transactions — patched once each. */
|
|
448
483
|
const bound = /* @__PURE__ */ new WeakSet();
|
|
449
|
-
function featureTest(options) {
|
|
450
|
-
const
|
|
451
|
-
const envParser = new _geekmidas_envkit.EnvironmentParser({ ...env });
|
|
452
|
-
const found = discover(options.modules);
|
|
484
|
+
function featureTest(options = {}) {
|
|
485
|
+
const manifest = options.manifest ?? loadTestManifest();
|
|
453
486
|
const BrowserClass = options.browser ?? _geekmidas_testkit_browser.Browser;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const inbox = found.emails.map((email) => env[(0, _geekmidas_manifest.provideKey)(email.id, "inboxUrl")]).find(Boolean);
|
|
457
|
-
const readMail = inbox ? (0, _geekmidas_testkit_mailbox.createMailbox)({ inbox }) : void 0;
|
|
487
|
+
let app;
|
|
488
|
+
let network;
|
|
458
489
|
let restoreFetch = () => {};
|
|
459
|
-
(0, vitest.beforeAll)(() => {
|
|
490
|
+
(0, vitest.beforeAll)(async () => {
|
|
491
|
+
app = await load(manifest, options.database);
|
|
492
|
+
for (const database of app.databases) bindToTests(database);
|
|
493
|
+
network = (0, msw_node.setupServer)(...surfaceHandlers(app.endpoints, manifest.env), ...app.auths.map((auth) => authHandler(auth, manifest.env, app.envParser)));
|
|
460
494
|
network.listen({ onUnhandledRequest(request, print) {
|
|
461
495
|
if (isLocal(new URL(request.url))) return;
|
|
462
496
|
print.error();
|
|
@@ -465,18 +499,20 @@ function featureTest(options) {
|
|
|
465
499
|
});
|
|
466
500
|
(0, vitest.afterAll)(() => {
|
|
467
501
|
restoreFetch();
|
|
468
|
-
network
|
|
502
|
+
network?.close();
|
|
469
503
|
});
|
|
470
504
|
const run = (fn) => async () => {
|
|
471
505
|
const id = (0, node_crypto.randomUUID)();
|
|
472
506
|
const state = {
|
|
473
507
|
transactions: new _geekmidas_testkit_transactions.TransactionRegistry(),
|
|
474
|
-
discovery: new _geekmidas_services.ServiceDiscovery(envParser),
|
|
508
|
+
discovery: new _geekmidas_services.ServiceDiscovery(app.envParser),
|
|
475
509
|
surfaces: /* @__PURE__ */ new Map(),
|
|
476
510
|
auth: /* @__PURE__ */ new Map(),
|
|
477
|
-
addresses: /* @__PURE__ */ new Set()
|
|
511
|
+
addresses: /* @__PURE__ */ new Set(),
|
|
512
|
+
published: /* @__PURE__ */ new Map()
|
|
478
513
|
};
|
|
479
514
|
contexts.set(id, state);
|
|
515
|
+
await state.discovery.register(recorders(app.channels, state));
|
|
480
516
|
try {
|
|
481
517
|
await (0, _geekmidas_testkit_context.runInTestContext)(id, async () => {
|
|
482
518
|
const browser = new BrowserClass();
|
|
@@ -484,12 +520,15 @@ function featureTest(options) {
|
|
|
484
520
|
try {
|
|
485
521
|
await fn({
|
|
486
522
|
browser,
|
|
487
|
-
db:
|
|
523
|
+
db: app.database ? await state.transactions.get(app.database.id, urlOf(app.database, app.envParser)) : void 0,
|
|
488
524
|
mailbox: (address) => {
|
|
489
|
-
if (!readMail) throw new NoInbox();
|
|
525
|
+
if (!app.readMail) throw new NoInbox();
|
|
490
526
|
state.addresses.add(address);
|
|
491
|
-
return readMail(address);
|
|
492
|
-
}
|
|
527
|
+
return app.readMail(address);
|
|
528
|
+
},
|
|
529
|
+
published: (channel) => [...state.published.get(channel.id) ?? []],
|
|
530
|
+
subscriber: (subscriber) => new TestSubscriberAdaptor(subscriber, state.discovery),
|
|
531
|
+
queue: (queue) => new TestQueueAdaptor(queue, state.discovery)
|
|
493
532
|
});
|
|
494
533
|
} finally {
|
|
495
534
|
restore();
|
|
@@ -497,7 +536,7 @@ function featureTest(options) {
|
|
|
497
536
|
});
|
|
498
537
|
} finally {
|
|
499
538
|
contexts.delete(id);
|
|
500
|
-
await Promise.all([state.transactions.rollbackAll(), ...[...state.addresses].map((address) => readMail?.(address).clear())]);
|
|
539
|
+
await Promise.all([state.transactions.rollbackAll(), ...[...state.addresses].map((address) => app.readMail?.(address).clear())]);
|
|
501
540
|
}
|
|
502
541
|
};
|
|
503
542
|
const it = ((name, fn, timeout) => (0, vitest.test)(name, run(fn), timeout));
|
|
@@ -505,17 +544,67 @@ function featureTest(options) {
|
|
|
505
544
|
it.skip = (name, fn, timeout) => vitest.test.skip(name, run(fn), timeout);
|
|
506
545
|
return it;
|
|
507
546
|
}
|
|
508
|
-
/**
|
|
509
|
-
|
|
510
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Import what the manifest points at.
|
|
549
|
+
*
|
|
550
|
+
* The same modules the app runs, so a construct here *is* the app's construct —
|
|
551
|
+
* the one its endpoints were built from — never a copy declared again.
|
|
552
|
+
*/
|
|
553
|
+
async function load(manifest, database) {
|
|
554
|
+
const envParser = new _geekmidas_envkit.EnvironmentParser({ ...manifest.env });
|
|
555
|
+
const imported = async ({ file, export: name }) => (await import(file))[name];
|
|
556
|
+
const constructs = await Promise.all(Object.values(manifest.constructs).map(({ source }) => imported(source)));
|
|
557
|
+
const endpoints = (await Promise.all(manifest.endpoints.map(({ source }) => imported(source)))).filter((value) => require_Endpoint.Endpoint.isEndpoint(value));
|
|
558
|
+
const unique = (values) => [...new Set(values)];
|
|
559
|
+
const databases = unique(constructs.filter((value) => value instanceof require_database_kysely.KyselyDatabase));
|
|
560
|
+
const auths = unique(constructs.filter((value) => value instanceof require_auth.BetterAuth));
|
|
561
|
+
const emails = unique(constructs.filter((value) => value instanceof require_email.Email));
|
|
562
|
+
const channels = unique(constructs.filter((value) => value instanceof require_Topic.Topic || value instanceof require_Queue.Queue));
|
|
563
|
+
const inbox = emails.map((email) => manifest.env[(0, _geekmidas_manifest.provideKey)(email.id, "inboxUrl")]).find(Boolean);
|
|
511
564
|
return {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
565
|
+
envParser,
|
|
566
|
+
endpoints,
|
|
567
|
+
databases,
|
|
568
|
+
auths,
|
|
569
|
+
emails,
|
|
570
|
+
channels,
|
|
571
|
+
database: databaseOf(databases, endpoints, database),
|
|
572
|
+
...inbox ? { readMail: (0, _geekmidas_testkit_mailbox.createMailbox)({ inbox }) } : {}
|
|
516
573
|
};
|
|
517
574
|
}
|
|
518
575
|
/**
|
|
576
|
+
* A publisher that records rather than sends, under every name a topic or
|
|
577
|
+
* queue is injected by — `users`, and the older `usersPublisher`.
|
|
578
|
+
*/
|
|
579
|
+
function recorders(channels, state) {
|
|
580
|
+
return channels.flatMap((channel) => {
|
|
581
|
+
const publisher = { async publish(messages) {
|
|
582
|
+
state.published.set(channel.id, [...state.published.get(channel.id) ?? [], ...messages.map(({ type, payload }) => ({
|
|
583
|
+
type,
|
|
584
|
+
payload
|
|
585
|
+
}))]);
|
|
586
|
+
} };
|
|
587
|
+
return [channel.service.serviceName, channel.publisher.serviceName].map((serviceName) => ({
|
|
588
|
+
serviceName,
|
|
589
|
+
register: () => publisher
|
|
590
|
+
}));
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* The database a test sees as `db`: the one named, or else the one the
|
|
595
|
+
* endpoints were given with `.database()` — found by the service they hold,
|
|
596
|
+
* which is the construct's own.
|
|
597
|
+
*/
|
|
598
|
+
function databaseOf(databases, endpoints, named) {
|
|
599
|
+
if (named) {
|
|
600
|
+
const found = databases.find(({ id }) => id === named);
|
|
601
|
+
if (!found) throw new UnknownDatabase(named, databases.map(({ id }) => id));
|
|
602
|
+
return found;
|
|
603
|
+
}
|
|
604
|
+
const services = new Set(endpoints.map((e) => e.databaseService).filter(Boolean));
|
|
605
|
+
return databases.find((database) => services.has(database.service));
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
519
608
|
* Make a database resolve, inside a test, to that test's transaction.
|
|
520
609
|
*
|
|
521
610
|
* Everything reaches a database through its service — an endpoint through
|
|
@@ -566,7 +655,7 @@ function surfaceHandlers(endpoints, env) {
|
|
|
566
655
|
/** An auth server, served at its URL, on the transaction of the test that asked. */
|
|
567
656
|
function authHandler(auth, env, envParser) {
|
|
568
657
|
const base = trim(env[(0, _geekmidas_manifest.provideKey)(auth.id, "url")] ?? "");
|
|
569
|
-
return msw.http.all(`${base}
|
|
658
|
+
return msw.http.all(`${base}/*`, ({ request }) => serve(request, async (state) => {
|
|
570
659
|
let app = state.auth.get(auth);
|
|
571
660
|
if (!app) {
|
|
572
661
|
app = auth.server({
|
|
@@ -609,15 +698,30 @@ var NoInbox = class extends Error {
|
|
|
609
698
|
this.name = "NoInbox";
|
|
610
699
|
}
|
|
611
700
|
};
|
|
701
|
+
/** `featureTest({ database })` named a database the app does not declare. */
|
|
702
|
+
var UnknownDatabase = class extends Error {
|
|
703
|
+
id;
|
|
704
|
+
declared;
|
|
705
|
+
constructor(id, declared) {
|
|
706
|
+
super(`featureTest was asked for the database '${id}', and the app declares ${declared.length ? declared.map((d) => `'${d}'`).join(", ") : "none"}.`);
|
|
707
|
+
this.id = id;
|
|
708
|
+
this.declared = declared;
|
|
709
|
+
this.name = "UnknownDatabase";
|
|
710
|
+
}
|
|
711
|
+
};
|
|
612
712
|
//#endregion
|
|
613
713
|
exports.NoInbox = NoInbox;
|
|
714
|
+
exports.NoTestManifest = NoTestManifest;
|
|
614
715
|
exports.TEST_CONTEXT_HEADER = _geekmidas_testkit_context.TEST_CONTEXT_HEADER;
|
|
716
|
+
exports.TEST_MANIFEST_ENV = TEST_MANIFEST_ENV;
|
|
615
717
|
exports.TestEndpointAdaptor = TestEndpointAdaptor;
|
|
616
718
|
exports.TestFunctionAdaptor = TestFunctionAdaptor;
|
|
617
719
|
exports.TestQueueAdaptor = TestQueueAdaptor;
|
|
618
720
|
exports.TestSubscriberAdaptor = TestSubscriberAdaptor;
|
|
721
|
+
exports.UnknownDatabase = UnknownDatabase;
|
|
619
722
|
exports.UnknownTestContext = UnknownTestContext;
|
|
620
723
|
exports.createMswHandlers = createMswHandlers;
|
|
621
724
|
exports.featureTest = featureTest;
|
|
725
|
+
exports.loadTestManifest = loadTestManifest;
|
|
622
726
|
|
|
623
727
|
//# sourceMappingURL=testing.cjs.map
|