@geekmidas/constructs 10.0.0-alpha.20 → 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.
Files changed (68) hide show
  1. package/README.md +48 -0
  2. package/dist/{Endpoint-BxkRWQ8G.d.mts → Endpoint-Bmi2-PBN.d.mts} +2 -2
  3. package/dist/{Endpoint-BxkRWQ8G.d.mts.map → Endpoint-Bmi2-PBN.d.mts.map} +1 -1
  4. package/dist/{Endpoint-DbYQJgb-.d.cts → Endpoint-CjvraSfp.d.cts} +2 -2
  5. package/dist/{Endpoint-DbYQJgb-.d.cts.map → Endpoint-CjvraSfp.d.cts.map} +1 -1
  6. package/dist/{EndpointFactory-T06kTFHg.d.mts → EndpointFactory-BFfvgko6.d.mts} +2 -2
  7. package/dist/{EndpointFactory-DE6nKtFZ.d.cts.map → EndpointFactory-BFfvgko6.d.mts.map} +1 -1
  8. package/dist/{EndpointFactory-DE6nKtFZ.d.cts → EndpointFactory-vfC3NiJi.d.cts} +2 -2
  9. package/dist/{EndpointFactory-T06kTFHg.d.mts.map → EndpointFactory-vfC3NiJi.d.cts.map} +1 -1
  10. package/dist/Queue-BB2-DgqJ.cjs +123 -0
  11. package/dist/Queue-BB2-DgqJ.cjs.map +1 -0
  12. package/dist/Queue-Dfumm1WR.mjs +118 -0
  13. package/dist/Queue-Dfumm1WR.mjs.map +1 -0
  14. package/dist/Topic-BaNCgP61.cjs +107 -0
  15. package/dist/Topic-BaNCgP61.cjs.map +1 -0
  16. package/dist/Topic-QqnO_6Dl.mjs +102 -0
  17. package/dist/Topic-QqnO_6Dl.mjs.map +1 -0
  18. package/dist/adaptors/aws.d.cts +1 -1
  19. package/dist/adaptors/aws.d.mts +1 -1
  20. package/dist/adaptors/hono.d.cts +1 -1
  21. package/dist/adaptors/hono.d.mts +1 -1
  22. package/dist/adaptors/testing.cjs +315 -9
  23. package/dist/adaptors/testing.cjs.map +1 -1
  24. package/dist/adaptors/testing.d.cts +129 -8
  25. package/dist/adaptors/testing.d.cts.map +1 -1
  26. package/dist/adaptors/testing.d.mts +129 -8
  27. package/dist/adaptors/testing.d.mts.map +1 -1
  28. package/dist/adaptors/testing.mjs +307 -8
  29. package/dist/adaptors/testing.mjs.map +1 -1
  30. package/dist/{auth-BlO8MUkR.d.cts → auth-C_fHD1We.d.cts} +9 -1
  31. package/dist/{auth-BlO8MUkR.d.cts.map → auth-C_fHD1We.d.cts.map} +1 -1
  32. package/dist/{auth-CHWIdcQO.d.mts → auth-D_Ntp1Nj.d.mts} +9 -1
  33. package/dist/{auth-CHWIdcQO.d.mts.map → auth-D_Ntp1Nj.d.mts.map} +1 -1
  34. package/dist/auth.cjs +15 -2
  35. package/dist/auth.cjs.map +1 -1
  36. package/dist/auth.d.cts +1 -1
  37. package/dist/auth.d.mts +1 -1
  38. package/dist/auth.mjs +15 -2
  39. package/dist/auth.mjs.map +1 -1
  40. package/dist/cache-AgQUMma6.d.mts +44 -0
  41. package/dist/cache-AgQUMma6.d.mts.map +1 -0
  42. package/dist/cache-obq0fOIM.d.cts +44 -0
  43. package/dist/cache-obq0fOIM.d.cts.map +1 -0
  44. package/dist/cache.d.cts +2 -43
  45. package/dist/cache.d.mts +2 -43
  46. package/dist/database/kysely.d.cts +2 -149
  47. package/dist/database/kysely.d.mts +2 -149
  48. package/dist/endpoints/index.d.cts +2 -2
  49. package/dist/endpoints/index.d.mts +2 -2
  50. package/dist/kysely-CXi49zc2.d.cts +150 -0
  51. package/dist/kysely-CXi49zc2.d.cts.map +1 -0
  52. package/dist/kysely-D3-G5b0j.d.mts +150 -0
  53. package/dist/kysely-D3-G5b0j.d.mts.map +1 -0
  54. package/dist/queue/index.cjs +3 -116
  55. package/dist/queue/index.cjs.map +1 -1
  56. package/dist/queue/index.mjs +1 -114
  57. package/dist/queue/index.mjs.map +1 -1
  58. package/dist/rest-api.d.cts +2 -2
  59. package/dist/rest-api.d.mts +2 -2
  60. package/dist/topic/index.cjs +3 -100
  61. package/dist/topic/index.cjs.map +1 -1
  62. package/dist/topic/index.mjs +1 -98
  63. package/dist/topic/index.mjs.map +1 -1
  64. package/package.json +44 -40
  65. package/dist/cache.d.cts.map +0 -1
  66. package/dist/cache.d.mts.map +0 -1
  67. package/dist/database/kysely.d.cts.map +0 -1
  68. package/dist/database/kysely.d.mts.map +0 -1
@@ -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"}
@@ -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-DbYQJgb-.cjs";
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";
@@ -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-BxkRWQ8G.mjs";
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";
@@ -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-DbYQJgb-.cjs";
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";
@@ -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-BxkRWQ8G.mjs";
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";
@@ -1,22 +1,31 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_auth = require("../auth.cjs");
2
3
  const require_functions = require("../functions-VnOTEp74.cjs");
3
4
  const require_Endpoint = require("../Endpoint-CtiPUpRX.cjs");
5
+ const require_email = require("../email.cjs");
6
+ const require_database_kysely = require("../database/kysely.cjs");
4
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");
5
10
  const require_HonoEndpointAdaptor = require("../HonoEndpointAdaptor-C9aivwKl.cjs");
6
11
  const require_processAudits = require("../processAudits-D_2W5x8k.cjs");
12
+ let _geekmidas_manifest = require("@geekmidas/manifest");
7
13
  let _geekmidas_errors = require("@geekmidas/errors");
8
14
  let _geekmidas_envkit = require("@geekmidas/envkit");
9
15
  let _geekmidas_services = require("@geekmidas/services");
10
16
  let hono = require("hono");
11
17
  let _geekmidas_audit = require("@geekmidas/audit");
18
+ let _geekmidas_testkit_context = require("@geekmidas/testkit/context");
12
19
  let msw = require("msw");
20
+ let node_crypto = require("node:crypto");
21
+ let _geekmidas_testkit_browser = require("@geekmidas/testkit/browser");
22
+ let _geekmidas_testkit_mailbox = require("@geekmidas/testkit/mailbox");
23
+ let _geekmidas_testkit_transactions = require("@geekmidas/testkit/transactions");
24
+ let msw_node = require("msw/node");
25
+ let vitest = require("vitest");
26
+ let node_fs = require("node:fs");
27
+ let node_url = require("node:url");
13
28
  //#region src/endpoints/MswEndpointAdaptor.ts
14
- /**
15
- * Header used to identify which test context a request belongs to.
16
- * Each concurrent test registers its own context (services, db transaction, etc.)
17
- * and the MSW handler looks it up by this header value.
18
- */
19
- const TEST_CONTEXT_HEADER = "x-test-context-id";
20
29
  const MSW_HTTP_METHODS = [
21
30
  "get",
22
31
  "post",
@@ -84,11 +93,11 @@ function createMswHandlers(endpoints, options) {
84
93
  const mswUrl = `${baseURL}${endpoint.route}`;
85
94
  const mswMethod = msw.http[method];
86
95
  handlers.push(mswMethod(mswUrl, async ({ request }) => {
87
- const contextId = request.headers.get(TEST_CONTEXT_HEADER);
96
+ const contextId = request.headers.get(_geekmidas_testkit_context.TEST_CONTEXT_HEADER);
88
97
  const ctx = contextId ? contexts.get(contextId) : void 0;
89
98
  if (!ctx) return new Response(JSON.stringify({
90
99
  error: "Missing or unknown test context ID",
91
- hint: `Set the '${TEST_CONTEXT_HEADER}' header to a registered context ID`
100
+ hint: `Set the '${_geekmidas_testkit_context.TEST_CONTEXT_HEADER}' header to a registered context ID`
92
101
  }), {
93
102
  status: 500,
94
103
  headers: { "content-type": "application/json" }
@@ -411,11 +420,308 @@ var TestSubscriberAdaptor = class TestSubscriberAdaptor {
411
420
  }
412
421
  };
413
422
  //#endregion
414
- exports.TEST_CONTEXT_HEADER = TEST_CONTEXT_HEADER;
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
455
+ //#region src/testing/featureTest.ts
456
+ /**
457
+ * `featureTest`: an app, driven the way it runs deployed.
458
+ *
459
+ * A browser signs in and calls the API; the API calls the auth server; each
460
+ * over its URL, each database in its own transaction. Nothing runs as a server:
461
+ * every URL an app's constructs answer on is served in-process through MSW,
462
+ * from the real handler, for the test that made the request. Everything else
463
+ * with a real local equivalent — Postgres, Mailpit, MinIO — is used directly.
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
+ *
470
+ * ```ts
471
+ * import { it } from '#test';
472
+ *
473
+ * it('shows the signed-in user their profile', async ({ browser, mailbox }) => {
474
+ * await browser.auth.signIn.magicLink({ email: 'ada@shop.test' });
475
+ * await browser.visit((await mailbox('ada@shop.test').last()).link!);
476
+ * expect(await browser.api.get('/profile')).toMatchObject({ email: 'ada@shop.test' });
477
+ * });
478
+ * ```
479
+ */
480
+ /** Every test in flight in this process, by id. */
481
+ const contexts = /* @__PURE__ */ new Map();
482
+ /** Databases already resolving to test transactions — patched once each. */
483
+ const bound = /* @__PURE__ */ new WeakSet();
484
+ function featureTest(options = {}) {
485
+ const manifest = options.manifest ?? loadTestManifest();
486
+ const BrowserClass = options.browser ?? _geekmidas_testkit_browser.Browser;
487
+ let app;
488
+ let network;
489
+ let restoreFetch = () => {};
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)));
494
+ network.listen({ onUnhandledRequest(request, print) {
495
+ if (isLocal(new URL(request.url))) return;
496
+ print.error();
497
+ } });
498
+ restoreFetch = (0, _geekmidas_testkit_context.installContextFetch)();
499
+ });
500
+ (0, vitest.afterAll)(() => {
501
+ restoreFetch();
502
+ network?.close();
503
+ });
504
+ const run = (fn) => async () => {
505
+ const id = (0, node_crypto.randomUUID)();
506
+ const state = {
507
+ transactions: new _geekmidas_testkit_transactions.TransactionRegistry(),
508
+ discovery: new _geekmidas_services.ServiceDiscovery(app.envParser),
509
+ surfaces: /* @__PURE__ */ new Map(),
510
+ auth: /* @__PURE__ */ new Map(),
511
+ addresses: /* @__PURE__ */ new Set(),
512
+ published: /* @__PURE__ */ new Map()
513
+ };
514
+ contexts.set(id, state);
515
+ await state.discovery.register(recorders(app.channels, state));
516
+ try {
517
+ await (0, _geekmidas_testkit_context.runInTestContext)(id, async () => {
518
+ const browser = new BrowserClass();
519
+ const restore = browser.install();
520
+ try {
521
+ await fn({
522
+ browser,
523
+ db: app.database ? await state.transactions.get(app.database.id, urlOf(app.database, app.envParser)) : void 0,
524
+ mailbox: (address) => {
525
+ if (!app.readMail) throw new NoInbox();
526
+ state.addresses.add(address);
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)
532
+ });
533
+ } finally {
534
+ restore();
535
+ }
536
+ });
537
+ } finally {
538
+ contexts.delete(id);
539
+ await Promise.all([state.transactions.rollbackAll(), ...[...state.addresses].map((address) => app.readMail?.(address).clear())]);
540
+ }
541
+ };
542
+ const it = ((name, fn, timeout) => (0, vitest.test)(name, run(fn), timeout));
543
+ it.only = (name, fn, timeout) => vitest.test.only(name, run(fn), timeout);
544
+ it.skip = (name, fn, timeout) => vitest.test.skip(name, run(fn), timeout);
545
+ return it;
546
+ }
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);
564
+ return {
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 }) } : {}
573
+ };
574
+ }
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
+ /**
608
+ * Make a database resolve, inside a test, to that test's transaction.
609
+ *
610
+ * Everything reaches a database through its service — an endpoint through
611
+ * service discovery, the auth server through its tenant directly — so this is
612
+ * the one place a test's connection can be handed over. Outside a test it is
613
+ * the database it always was.
614
+ */
615
+ function bindToTests(database) {
616
+ if (bound.has(database)) return;
617
+ bound.add(database);
618
+ const service = database.service;
619
+ const register = service.register;
620
+ service.register = (options) => {
621
+ const context = (0, _geekmidas_testkit_context.currentTestContext)();
622
+ const state = context && contexts.get(context.id);
623
+ if (!state) return register(options);
624
+ return state.transactions.get(database.id, urlOf(database, options.envParser));
625
+ };
626
+ }
627
+ function urlOf(database, envParser) {
628
+ return envParser.create((get) => ({ url: get((0, _geekmidas_manifest.provideKey)(database.id, "url")).string() })).parse().url;
629
+ }
630
+ /** Each surface's endpoints, served at its URL for the test that asked. */
631
+ function surfaceHandlers(endpoints, env) {
632
+ const bySurface = /* @__PURE__ */ new Map();
633
+ for (const endpoint of endpoints) {
634
+ const surface = endpoint.surface?.id;
635
+ if (!surface) continue;
636
+ bySurface.set(surface, [...bySurface.get(surface) ?? [], endpoint]);
637
+ }
638
+ return [...bySurface].flatMap(([surface, served]) => {
639
+ const base = env[(0, _geekmidas_manifest.provideKey)(surface, "url")];
640
+ if (!base) return [];
641
+ return msw.http.all(`${trim(base)}/*`, ({ request }) => serve(request, async (state) => {
642
+ let app = state.surfaces.get(surface);
643
+ if (!app) {
644
+ app = Promise.resolve().then(() => {
645
+ const hono$1 = new hono.Hono();
646
+ require_HonoEndpointAdaptor.HonoEndpoint.addRoutes(served, state.discovery, hono$1);
647
+ return hono$1;
648
+ });
649
+ state.surfaces.set(surface, app);
650
+ }
651
+ return (await app).fetch(request);
652
+ }));
653
+ });
654
+ }
655
+ /** An auth server, served at its URL, on the transaction of the test that asked. */
656
+ function authHandler(auth, env, envParser) {
657
+ const base = trim(env[(0, _geekmidas_manifest.provideKey)(auth.id, "url")] ?? "");
658
+ return msw.http.all(`${base}/*`, ({ request }) => serve(request, async (state) => {
659
+ let app = state.auth.get(auth);
660
+ if (!app) {
661
+ app = auth.server({
662
+ envParser,
663
+ context: _geekmidas_services.serviceContext
664
+ }).then(({ app }) => app);
665
+ state.auth.set(auth, app);
666
+ }
667
+ return (await app).fetch(request);
668
+ }));
669
+ }
670
+ /** Handle a request as the server side of the test it was made for. */
671
+ async function serve(request, handle) {
672
+ const id = (0, _geekmidas_testkit_context.testContextOf)(request);
673
+ const state = id ? contexts.get(id) : void 0;
674
+ if (!id || !state) throw new UnknownTestContext(request.url, id);
675
+ return (0, _geekmidas_testkit_context.runAsServer)(id, () => handle(state));
676
+ }
677
+ function trim(url) {
678
+ return url.replace(/\/$/, "");
679
+ }
680
+ function isLocal(url) {
681
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname.endsWith(".localhost");
682
+ }
683
+ /** A request reached an in-process server without a test to belong to. */
684
+ var UnknownTestContext = class extends Error {
685
+ url;
686
+ id;
687
+ constructor(url, id) {
688
+ super(id ? `${url} was requested for test ${id}, which is not running. A request that outlives its test — an un-awaited call — lands here.` : `${url} was requested outside any test. Make the request inside a featureTest, so it reaches that test's transactions.`);
689
+ this.url = url;
690
+ this.id = id;
691
+ this.name = "UnknownTestContext";
692
+ }
693
+ };
694
+ /** `mailbox` was used in an app that declares no Email construct. */
695
+ var NoInbox = class extends Error {
696
+ constructor() {
697
+ super("mailbox() reads the inbox of the app's Email construct, and none was found in featureTest's modules — or `gkm test` did not inject its <ID>_INBOX_URL.");
698
+ this.name = "NoInbox";
699
+ }
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
+ };
712
+ //#endregion
713
+ exports.NoInbox = NoInbox;
714
+ exports.NoTestManifest = NoTestManifest;
715
+ exports.TEST_CONTEXT_HEADER = _geekmidas_testkit_context.TEST_CONTEXT_HEADER;
716
+ exports.TEST_MANIFEST_ENV = TEST_MANIFEST_ENV;
415
717
  exports.TestEndpointAdaptor = TestEndpointAdaptor;
416
718
  exports.TestFunctionAdaptor = TestFunctionAdaptor;
417
719
  exports.TestQueueAdaptor = TestQueueAdaptor;
418
720
  exports.TestSubscriberAdaptor = TestSubscriberAdaptor;
721
+ exports.UnknownDatabase = UnknownDatabase;
722
+ exports.UnknownTestContext = UnknownTestContext;
419
723
  exports.createMswHandlers = createMswHandlers;
724
+ exports.featureTest = featureTest;
725
+ exports.loadTestManifest = loadTestManifest;
420
726
 
421
727
  //# sourceMappingURL=testing.cjs.map