@geekmidas/constructs 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/{AWSScheduledFunction-COv8bB1v.d.mts → AWSScheduledFunction-1GYwJS7F.d.cts} +4 -4
  3. package/dist/{AWSScheduledFunction-D9uCAd76.d.cts.map → AWSScheduledFunction-1GYwJS7F.d.cts.map} +1 -1
  4. package/dist/{AWSScheduledFunction-D-55GfwL.mjs → AWSScheduledFunction-CODxw_vc.mjs} +2 -2
  5. package/dist/{AWSScheduledFunction-D-55GfwL.mjs.map → AWSScheduledFunction-CODxw_vc.mjs.map} +1 -1
  6. package/dist/{AWSScheduledFunction-sWwZC9hg.cjs → AWSScheduledFunction-DbkDr8CA.cjs} +3 -3
  7. package/dist/{AWSScheduledFunction-sWwZC9hg.cjs.map → AWSScheduledFunction-DbkDr8CA.cjs.map} +1 -1
  8. package/dist/{AWSScheduledFunction-D9uCAd76.d.cts → AWSScheduledFunction-ysaCsdWx.d.mts} +4 -4
  9. package/dist/{AWSScheduledFunction-COv8bB1v.d.mts.map → AWSScheduledFunction-ysaCsdWx.d.mts.map} +1 -1
  10. package/dist/{Construct-D0n7QTaI.d.mts → Construct-DCMuYOox.d.mts} +3 -1
  11. package/dist/{Construct-D0n7QTaI.d.mts.map → Construct-DCMuYOox.d.mts.map} +1 -1
  12. package/dist/{Construct-CdJY2DJS.mjs → Construct-DD8EaWzh.mjs} +4 -2
  13. package/dist/{Construct-CdJY2DJS.mjs.map → Construct-DD8EaWzh.mjs.map} +1 -1
  14. package/dist/{Construct-cjIs74Gb.d.cts → Construct-_1wX7p_Y.d.cts} +3 -1
  15. package/dist/{Construct-cjIs74Gb.d.cts.map → Construct-_1wX7p_Y.d.cts.map} +1 -1
  16. package/dist/{Construct-B14miJ6P.cjs → Construct-vXMZJgRp.cjs} +3 -1
  17. package/dist/{Construct-B14miJ6P.cjs.map → Construct-vXMZJgRp.cjs.map} +1 -1
  18. package/dist/{Endpoint-D5SChIsU.d.cts → Endpoint-C3-ZyPSL.d.cts} +5 -5
  19. package/dist/Endpoint-C3-ZyPSL.d.cts.map +1 -0
  20. package/dist/{Endpoint-C_qxza6z.d.mts → Endpoint-CLPSIl6S.d.mts} +5 -5
  21. package/dist/Endpoint-CLPSIl6S.d.mts.map +1 -0
  22. package/dist/{HonoEndpointAdaptor-CWYBGVFQ.mjs → HonoEndpointAdaptor-BwfNyFCi.mjs} +2 -2
  23. package/dist/{HonoEndpointAdaptor-CWYBGVFQ.mjs.map → HonoEndpointAdaptor-BwfNyFCi.mjs.map} +1 -1
  24. package/dist/{HonoEndpointAdaptor-C_2jaZ4k.cjs → HonoEndpointAdaptor-CkJENANP.cjs} +4 -4
  25. package/dist/{HonoEndpointAdaptor-C_2jaZ4k.cjs.map → HonoEndpointAdaptor-CkJENANP.cjs.map} +1 -1
  26. package/dist/Queue-DaxjwmOQ.d.cts +66 -0
  27. package/dist/Queue-DaxjwmOQ.d.cts.map +1 -0
  28. package/dist/Queue-HBDf0fqz.d.mts +66 -0
  29. package/dist/Queue-HBDf0fqz.d.mts.map +1 -0
  30. package/dist/{Subscriber-C6YXAmTp.d.cts → Subscriber-B69ngPwl.d.cts} +17 -3
  31. package/dist/Subscriber-B69ngPwl.d.cts.map +1 -0
  32. package/dist/{Subscriber-C0v9iOrI.d.mts → Subscriber-D8Xm-86W.d.mts} +17 -3
  33. package/dist/Subscriber-D8Xm-86W.d.mts.map +1 -0
  34. package/dist/Topic-C5ZMfveW.d.cts +60 -0
  35. package/dist/Topic-C5ZMfveW.d.cts.map +1 -0
  36. package/dist/Topic-DT10AqHk.d.mts +60 -0
  37. package/dist/Topic-DT10AqHk.d.mts.map +1 -0
  38. package/dist/adaptors/aws.cjs +109 -5
  39. package/dist/adaptors/aws.cjs.map +1 -1
  40. package/dist/adaptors/aws.d.cts +39 -7
  41. package/dist/adaptors/aws.d.cts.map +1 -1
  42. package/dist/adaptors/aws.d.mts +39 -7
  43. package/dist/adaptors/aws.d.mts.map +1 -1
  44. package/dist/adaptors/aws.mjs +108 -5
  45. package/dist/adaptors/aws.mjs.map +1 -1
  46. package/dist/adaptors/hono.cjs +5 -5
  47. package/dist/adaptors/hono.d.cts +5 -5
  48. package/dist/adaptors/hono.d.cts.map +1 -1
  49. package/dist/adaptors/hono.d.mts +5 -5
  50. package/dist/adaptors/hono.mjs +4 -4
  51. package/dist/adaptors/testing.cjs +42 -5
  52. package/dist/adaptors/testing.cjs.map +1 -1
  53. package/dist/adaptors/testing.d.cts +29 -5
  54. package/dist/adaptors/testing.d.cts.map +1 -1
  55. package/dist/adaptors/testing.d.mts +29 -5
  56. package/dist/adaptors/testing.d.mts.map +1 -1
  57. package/dist/adaptors/testing.mjs +41 -5
  58. package/dist/adaptors/testing.mjs.map +1 -1
  59. package/dist/crons/index.cjs +3 -3
  60. package/dist/crons/index.d.cts +3 -3
  61. package/dist/crons/index.d.mts +3 -3
  62. package/dist/crons/index.mjs +3 -3
  63. package/dist/endpoints/index.cjs +3 -3
  64. package/dist/endpoints/index.d.cts +5 -5
  65. package/dist/endpoints/index.d.mts +3 -3
  66. package/dist/endpoints/index.mjs +3 -3
  67. package/dist/functions/index.cjs +2 -2
  68. package/dist/functions/index.d.cts +2 -2
  69. package/dist/functions/index.d.mts +2 -2
  70. package/dist/functions/index.mjs +2 -2
  71. package/dist/{functions-C9QhxXoP.mjs → functions-Bx9Ac0ea.mjs} +2 -2
  72. package/dist/{functions-C9QhxXoP.mjs.map → functions-Bx9Ac0ea.mjs.map} +1 -1
  73. package/dist/{functions-cL1XwEHW.cjs → functions-C8i1kCMM.cjs} +2 -2
  74. package/dist/{functions-cL1XwEHW.cjs.map → functions-C8i1kCMM.cjs.map} +1 -1
  75. package/dist/{index-Cg0m0uBN.d.cts → index-D-s9UrHd.d.cts} +6 -6
  76. package/dist/{index-Cg0m0uBN.d.cts.map → index-D-s9UrHd.d.cts.map} +1 -1
  77. package/dist/{index-C0XoazSK.d.mts → index-DP6fqv0I.d.mts} +2 -2
  78. package/dist/{index-C0XoazSK.d.mts.map → index-DP6fqv0I.d.mts.map} +1 -1
  79. package/dist/index.cjs +1 -1
  80. package/dist/index.d.cts +1 -1
  81. package/dist/index.d.mts +1 -1
  82. package/dist/index.mjs +1 -1
  83. package/dist/{lazyAccessors-Bnq67EHR.mjs → lazyAccessors-DhOPJsRT.mjs} +3 -3
  84. package/dist/lazyAccessors-DhOPJsRT.mjs.map +1 -0
  85. package/dist/{lazyAccessors-DGqzwC-j.cjs → lazyAccessors-DmTG8sQH.cjs} +3 -3
  86. package/dist/lazyAccessors-DmTG8sQH.cjs.map +1 -0
  87. package/dist/{processAudits-BmjKsnF7.cjs → processAudits-CKjjivyz.cjs} +2 -2
  88. package/dist/{processAudits-BmjKsnF7.cjs.map → processAudits-CKjjivyz.cjs.map} +1 -1
  89. package/dist/queue/index.cjs +127 -0
  90. package/dist/queue/index.cjs.map +1 -0
  91. package/dist/queue/index.d.cts +45 -0
  92. package/dist/queue/index.d.cts.map +1 -0
  93. package/dist/queue/index.d.mts +44 -0
  94. package/dist/queue/index.d.mts.map +1 -0
  95. package/dist/queue/index.mjs +125 -0
  96. package/dist/queue/index.mjs.map +1 -0
  97. package/dist/subscribers/index.cjs +21 -3
  98. package/dist/subscribers/index.cjs.map +1 -1
  99. package/dist/subscribers/index.d.cts +18 -4
  100. package/dist/subscribers/index.d.cts.map +1 -1
  101. package/dist/subscribers/index.d.mts +16 -2
  102. package/dist/subscribers/index.d.mts.map +1 -1
  103. package/dist/subscribers/index.mjs +21 -3
  104. package/dist/subscribers/index.mjs.map +1 -1
  105. package/dist/topic/index.cjs +94 -0
  106. package/dist/topic/index.cjs.map +1 -0
  107. package/dist/topic/index.d.cts +32 -0
  108. package/dist/topic/index.d.cts.map +1 -0
  109. package/dist/topic/index.d.mts +32 -0
  110. package/dist/topic/index.d.mts.map +1 -0
  111. package/dist/topic/index.mjs +92 -0
  112. package/dist/topic/index.mjs.map +1 -0
  113. package/package.json +33 -13
  114. package/src/Construct.ts +2 -0
  115. package/src/adaptors/aws.ts +1 -0
  116. package/src/adaptors/testing.ts +1 -0
  117. package/src/endpoints/Endpoint.ts +16 -10
  118. package/src/queue/AWSLambdaQueueAdaptor.ts +162 -0
  119. package/src/queue/Queue.ts +128 -0
  120. package/src/queue/QueueBuilder.ts +115 -0
  121. package/src/queue/TestQueueAdaptor.ts +63 -0
  122. package/src/queue/__tests__/QueueBuilder.spec.ts +137 -0
  123. package/src/queue/index.ts +12 -0
  124. package/src/subscribers/Subscriber.ts +7 -0
  125. package/src/subscribers/SubscriberBuilder.ts +36 -0
  126. package/src/topic/Topic.ts +103 -0
  127. package/src/topic/TopicBuilder.ts +51 -0
  128. package/src/topic/__tests__/TopicBuilder.spec.ts +82 -0
  129. package/src/topic/index.ts +11 -0
  130. package/tsdown.config.ts +2 -0
  131. package/dist/Endpoint-C_qxza6z.d.mts.map +0 -1
  132. package/dist/Endpoint-D5SChIsU.d.cts.map +0 -1
  133. package/dist/Subscriber-C0v9iOrI.d.mts.map +0 -1
  134. package/dist/Subscriber-C6YXAmTp.d.cts.map +0 -1
  135. package/dist/lazyAccessors-Bnq67EHR.mjs.map +0 -1
  136. package/dist/lazyAccessors-DGqzwC-j.cjs.map +0 -1
@@ -0,0 +1,32 @@
1
+ import "../Construct-DCMuYOox.mjs";
2
+ import { Topic, TopicEvents, TopicMessage } from "../Topic-DT10AqHk.mjs";
3
+ import { Logger } from "@geekmidas/logger";
4
+
5
+ //#region src/topic/TopicBuilder.d.ts
6
+
7
+ /**
8
+ * Builds a {@link Topic} — `t.topic('users').events({ 'user.created': schema, … })`.
9
+ * The event map is the topic's contract: it types the derived `topic.publisher`
10
+ * and the subscribers that bind via `s.topic(topic)`.
11
+ */
12
+ declare class TopicBuilder<TName extends string = string, TEvents extends TopicEvents = TopicEvents> {
13
+ private _name?;
14
+ private _logger;
15
+ /** The topic name — drives the infra topic and its `<NAME>_*` env vars. */
16
+ topic<T extends string>(name: T): TopicBuilder<T, TEvents>;
17
+ logger(logger: Logger): this;
18
+ /**
19
+ * The event contract — a map of event type → payload schema. Building the
20
+ * topic requires this; it's what types the publisher and subscribers.
21
+ */
22
+ events<T extends TopicEvents>(events: T): Topic<TName, T>;
23
+ }
24
+ //# sourceMappingURL=TopicBuilder.d.ts.map
25
+ //#endregion
26
+ //#region src/topic/index.d.ts
27
+ declare const t: TopicBuilder<string, TopicEvents>;
28
+ //# sourceMappingURL=index.d.ts.map
29
+
30
+ //#endregion
31
+ export { Topic, TopicBuilder, TopicEvents, TopicMessage, t };
32
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/topic/TopicBuilder.ts","../../src/topic/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AASA;;AAEiB,cAFJ,YAEI,CAAA,cAAA,MAAA,GAAA,MAAA,EAAA,gBAAA,WAAA,GAAc,WAAd,CAAA,CAAA;EAAW,QAAG,KAAA;EAAW,QAMX,OAAA;EAAC;EAAiB,KAAE,CAAA,UAAA,MAAA,CAAA,CAAA,IAAA,EAApB,CAAoB,CAAA,EAAhB,YAAgB,CAAH,CAAG,EAAA,OAAA,CAAA;EAAO,MAAvB,CAAA,MAAA,EAKnB,MALmB,CAAA,EAAA,IAAA;EAAY;;;;EAcO,MAAE,CAAA,UAAtC,WAAsC,CAAA,CAAA,MAAA,EAAjB,CAAiB,CAAA,EAAb,KAAa,CAAP,KAAO,EAAA,CAAA,CAAA;;AAAR;;;cCrBnC,GAAC,qBAAqB,WAAA"}
@@ -0,0 +1,92 @@
1
+ import { Construct, ConstructType } from "../Construct-DD8EaWzh.mjs";
2
+ import { ConsoleLogger, DEFAULT_LOGGER } from "@geekmidas/logger/console";
3
+ import { environmentCase } from "@geekmidas/envkit";
4
+ import { Publisher } from "@geekmidas/events";
5
+
6
+ //#region src/topic/Topic.ts
7
+ const DEFAULT_LOGGER$1 = new ConsoleLogger();
8
+ /**
9
+ * A topic — pub/sub fan-out. Unlike a `Queue` (point-to-point, one consumer), a
10
+ * topic is a *resource* owned by no single handler: it declares the event
11
+ * contract, derives a typed producer (`topic.publisher`), and any number of
12
+ * subscribers (`s.topic(topic)`) bind to it. `gkm build` discovers it into the
13
+ * manifest's `topics` field; infra provisions an SNS topic.
14
+ *
15
+ * This replaces hand-writing a publisher `Service`: the publisher is *derived*
16
+ * from the declared event contract, the same way `Queue` derives its publisher.
17
+ */
18
+ var Topic = class extends Construct {
19
+ __IS_TOPIC__ = true;
20
+ static isTopic(obj) {
21
+ return Boolean(obj && obj.__IS_TOPIC__ === true && obj.type === ConstructType.Topic);
22
+ }
23
+ constructor(name, eventSchemas, logger = DEFAULT_LOGGER$1) {
24
+ super(ConstructType.Topic, logger, [], []);
25
+ this.name = name;
26
+ this.eventSchemas = eventSchemas;
27
+ }
28
+ /** The event type names this topic carries. */
29
+ get eventTypes() {
30
+ return Object.keys(this.eventSchemas);
31
+ }
32
+ /**
33
+ * The producer side — a `Service` exposing an `EventPublisher` typed to the
34
+ * union of this topic's events. Inject via `.publisher(topic.publisher)` (for
35
+ * declarative `.event(...)`) or `.services([topic.publisher])` (to publish
36
+ * imperatively). Reads `<NAME>_PUBLISHER_CONNECTION_STRING` and selects the
37
+ * transport from the URL protocol — `pgboss://` locally, `sns://` deployed.
38
+ *
39
+ * Because it's a `Service`, the connection-string requirement is sniffed into
40
+ * the manifest of whatever construct injects it (least-privilege linking).
41
+ */
42
+ get publisher() {
43
+ const envVar = `${environmentCase(this.name)}_PUBLISHER_CONNECTION_STRING`;
44
+ return {
45
+ serviceName: `${this.name}Publisher`,
46
+ async register({ envParser }) {
47
+ const { connectionString } = envParser.create((get) => ({ connectionString: get(envVar).string() })).parse();
48
+ return Publisher.fromConnectionString(connectionString);
49
+ }
50
+ };
51
+ }
52
+ };
53
+
54
+ //#endregion
55
+ //#region src/topic/TopicBuilder.ts
56
+ /**
57
+ * Builds a {@link Topic} — `t.topic('users').events({ 'user.created': schema, … })`.
58
+ * The event map is the topic's contract: it types the derived `topic.publisher`
59
+ * and the subscribers that bind via `s.topic(topic)`.
60
+ */
61
+ var TopicBuilder = class {
62
+ _name;
63
+ _logger = DEFAULT_LOGGER;
64
+ /** The topic name — drives the infra topic and its `<NAME>_*` env vars. */
65
+ topic(name) {
66
+ this._name = name;
67
+ return this;
68
+ }
69
+ logger(logger) {
70
+ this._logger = logger;
71
+ return this;
72
+ }
73
+ /**
74
+ * The event contract — a map of event type → payload schema. Building the
75
+ * topic requires this; it's what types the publisher and subscribers.
76
+ */
77
+ events(events) {
78
+ if (!this._name) throw new Error("Topic requires a name — call .topic(name) before .events().");
79
+ const topic = new Topic(this._name, events, this._logger);
80
+ this._name = void 0;
81
+ this._logger = DEFAULT_LOGGER;
82
+ return topic;
83
+ }
84
+ };
85
+
86
+ //#endregion
87
+ //#region src/topic/index.ts
88
+ const t = new TopicBuilder();
89
+
90
+ //#endregion
91
+ export { Topic, TopicBuilder, t };
92
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["DEFAULT_LOGGER","obj: unknown","name: TName","eventSchemas: TEvents","logger: Logger","name: T","logger: Logger","events: T"],"sources":["../../src/topic/Topic.ts","../../src/topic/TopicBuilder.ts","../../src/topic/index.ts"],"sourcesContent":["import { environmentCase } from '@geekmidas/envkit';\nimport {\n\ttype EventPublisher,\n\ttype EventPublisherConnectionString,\n\ttype PublishableMessage,\n\tPublisher,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport { ConsoleLogger } from '@geekmidas/logger/console';\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\nconst DEFAULT_LOGGER = new ConsoleLogger() as unknown as Logger;\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\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\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\tget publisher(): Service<\n\t\t`${TName}Publisher`,\n\t\tEventPublisher<TopicMessage<TEvents>>\n\t> {\n\t\tconst envVar = `${environmentCase(this.name)}_PUBLISHER_CONNECTION_STRING`;\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","import type { Logger } from '@geekmidas/logger';\nimport { DEFAULT_LOGGER } from '@geekmidas/logger/console';\nimport { Topic, type TopicEvents } from './Topic';\n\n/**\n * Builds a {@link Topic} — `t.topic('users').events({ 'user.created': schema, … })`.\n * The event map is the topic's contract: it types the derived `topic.publisher`\n * and the subscribers that bind via `s.topic(topic)`.\n */\nexport class TopicBuilder<\n\tTName extends string = string,\n\tTEvents extends TopicEvents = TopicEvents,\n> {\n\tprivate _name?: string;\n\tprivate _logger: Logger = DEFAULT_LOGGER;\n\n\t/** The topic name — drives the infra topic and its `<NAME>_*` env vars. */\n\ttopic<T extends string>(name: T): TopicBuilder<T, TEvents> {\n\t\tthis._name = name;\n\t\treturn this as unknown as TopicBuilder<T, TEvents>;\n\t}\n\n\tlogger(logger: Logger): this {\n\t\tthis._logger = logger;\n\t\treturn this;\n\t}\n\n\t/**\n\t * The event contract — a map of event type → payload schema. Building the\n\t * topic requires this; it's what types the publisher and subscribers.\n\t */\n\tevents<T extends TopicEvents>(events: T): Topic<TName, T> {\n\t\tif (!this._name) {\n\t\t\tthrow new Error(\n\t\t\t\t'Topic requires a name — call .topic(name) before .events().',\n\t\t\t);\n\t\t}\n\n\t\tconst topic = new Topic<TName, T>(\n\t\t\tthis._name as TName,\n\t\t\tevents,\n\t\t\tthis._logger,\n\t\t);\n\n\t\t// Reset builder state to prevent pollution across reuse.\n\t\tthis._name = undefined;\n\t\tthis._logger = DEFAULT_LOGGER;\n\n\t\treturn topic;\n\t}\n}\n","export {\n\tTopic,\n\ttype TopicEvents,\n\ttype TopicMessage,\n} from './Topic';\n\nimport { TopicBuilder } from './TopicBuilder';\n\nexport { TopicBuilder } from './TopicBuilder';\n\nexport const t = new TopicBuilder();\n"],"mappings":";;;;;;AAcA,MAAMA,mBAAiB,IAAI;;;;;;;;;;;AA0B3B,IAAa,QAAb,cAGU,UAAU;CACnB,eAAe;CAEf,OAAO,QAAQC,KAAiD;AAC/D,SAAO,QACN,OACE,IAAmC,iBAAiB,QACpD,IAAkB,SAAS,cAAc,MAC3C;CACD;CAED,YACiBC,MAMAC,cAChBC,SAAiBJ,kBAChB;AACD,QAAM,cAAc,OAAO,QAAQ,CAAE,GAAE,CAAE,EAAC;EAT1B;EAMA;CAIhB;;CAGD,IAAI,aAAyC;AAC5C,SAAO,OAAO,KAAK,KAAK,aAAa;CACrC;;;;;;;;;;;CAYD,IAAI,YAGF;EACD,MAAM,UAAU,EAAE,gBAAgB,KAAK,KAAK,CAAC;AAC7C,SAAO;GACN,cAAc,EAAE,KAAK,KAAK;GAC1B,MAAM,SAAS,EAAE,WAAW,EAAE;IAC7B,MAAM,EAAE,kBAAkB,GAAG,UAC3B,OAAO,CAAC,SAAS,EACjB,kBAAkB,IAAI,OAAO,CAAC,QAAQ,CACtC,GAAE,CACF,OAAO;AAET,WAAO,UAAU,qBAChB,iBACA;GACD;EACD;CACD;AACD;;;;;;;;;AC7FD,IAAa,eAAb,MAGE;CACD,AAAQ;CACR,AAAQ,UAAkB;;CAG1B,MAAwBK,MAAmC;AAC1D,OAAK,QAAQ;AACb,SAAO;CACP;CAED,OAAOC,QAAsB;AAC5B,OAAK,UAAU;AACf,SAAO;CACP;;;;;CAMD,OAA8BC,QAA4B;AACzD,OAAK,KAAK,MACT,OAAM,IAAI,MACT;EAIF,MAAM,QAAQ,IAAI,MACjB,KAAK,OACL,QACA,KAAK;AAIN,OAAK;AACL,OAAK,UAAU;AAEf,SAAO;CACP;AACD;;;;ACxCD,MAAa,IAAI,IAAI"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/constructs",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,6 +34,26 @@
34
34
  "default": "./dist/functions/index.cjs"
35
35
  }
36
36
  },
37
+ "./queue": {
38
+ "import": {
39
+ "types": "./dist/queue/index.d.mts",
40
+ "default": "./dist/queue/index.mjs"
41
+ },
42
+ "require": {
43
+ "types": "./dist/queue/index.d.cts",
44
+ "default": "./dist/queue/index.cjs"
45
+ }
46
+ },
47
+ "./topic": {
48
+ "import": {
49
+ "types": "./dist/topic/index.d.mts",
50
+ "default": "./dist/topic/index.mjs"
51
+ },
52
+ "require": {
53
+ "types": "./dist/topic/index.d.cts",
54
+ "default": "./dist/topic/index.cjs"
55
+ }
56
+ },
37
57
  "./crons": {
38
58
  "import": {
39
59
  "types": "./dist/crons/index.d.mts",
@@ -127,16 +147,16 @@
127
147
  "msw": "~2.10.3",
128
148
  "zod": "~4.1.13",
129
149
  "@geekmidas/audit": "^2.0.1",
130
- "@geekmidas/db": "^1.0.2",
131
150
  "@geekmidas/cache": "^1.1.1",
132
- "@geekmidas/envkit": "^1.0.7",
151
+ "@geekmidas/db": "^1.0.2",
152
+ "@geekmidas/envkit": "^1.1.0",
133
153
  "@geekmidas/errors": "^1.0.1",
134
- "@geekmidas/events": "^1.1.4",
154
+ "@geekmidas/events": "^1.1.5",
135
155
  "@geekmidas/logger": "^1.0.2",
136
- "@geekmidas/rate-limit": "^3.0.0",
137
- "@geekmidas/schema": "^1.0.2",
138
- "@geekmidas/services": "^1.1.0",
139
- "@geekmidas/testkit": "^2.0.0"
156
+ "@geekmidas/rate-limit": "^4.0.0",
157
+ "@geekmidas/schema": "^1.0.3",
158
+ "@geekmidas/services": "^2.0.0",
159
+ "@geekmidas/testkit": "^3.0.0"
140
160
  },
141
161
  "peerDependencies": {
142
162
  "@middy/core": ">=6.3.1",
@@ -146,13 +166,13 @@
146
166
  "@geekmidas/audit": "^2.0.1",
147
167
  "@geekmidas/cache": "^1.1.1",
148
168
  "@geekmidas/db": "^1.0.2",
149
- "@geekmidas/envkit": "^1.0.7",
169
+ "@geekmidas/envkit": "^1.1.0",
150
170
  "@geekmidas/errors": "^1.0.1",
151
- "@geekmidas/events": "^1.1.4",
171
+ "@geekmidas/events": "^1.1.5",
152
172
  "@geekmidas/logger": "^1.0.2",
153
- "@geekmidas/rate-limit": "^3.0.0",
154
- "@geekmidas/schema": "^1.0.2",
155
- "@geekmidas/services": "^1.1.0"
173
+ "@geekmidas/rate-limit": "^4.0.0",
174
+ "@geekmidas/schema": "^1.0.3",
175
+ "@geekmidas/services": "^2.0.0"
156
176
  },
157
177
  "peerDependenciesMeta": {
158
178
  "@geekmidas/audit": {
package/src/Construct.ts CHANGED
@@ -171,6 +171,8 @@ export enum ConstructType {
171
171
  Endpoint = 'dev.geekmidas.function.endpoint',
172
172
  Function = 'dev.geekmidas.function.function',
173
173
  Subscriber = 'dev.geekmidas.function.subscriber',
174
+ Queue = 'dev.geekmidas.function.queue',
175
+ Topic = 'dev.geekmidas.resource.topic',
174
176
  }
175
177
 
176
178
  /**
@@ -2,4 +2,5 @@ export { AWSScheduledFunction } from '../crons/AWSScheduledFunction';
2
2
  export { AmazonApiGatewayV1Endpoint } from '../endpoints/AmazonApiGatewayV1EndpointAdaptor';
3
3
  export { AmazonApiGatewayV2Endpoint } from '../endpoints/AmazonApiGatewayV2EndpointAdaptor';
4
4
  export { AWSLambdaFunction } from '../functions/AWSLambdaFunction';
5
+ export { AWSLambdaQueue } from '../queue/AWSLambdaQueueAdaptor';
5
6
  export { AWSLambdaSubscriber } from '../subscribers/AWSLambdaSubscriberAdaptor';
@@ -7,4 +7,5 @@ export {
7
7
  } from '../endpoints/MswEndpointAdaptor';
8
8
  export { TestEndpointAdaptor } from '../endpoints/TestEndpointAdaptor';
9
9
  export { TestFunctionAdaptor } from '../functions/TestFunctionAdaptor';
10
+ export { TestQueueAdaptor } from '../queue/TestQueueAdaptor';
10
11
  export { TestSubscriberAdaptor } from '../subscribers/TestSubscriberAdaptor';
@@ -9,6 +9,7 @@ import type { RateLimitConfig } from '@geekmidas/rate-limit';
9
9
  import type {
10
10
  InferComposableStandardSchema,
11
11
  InferStandardSchema,
12
+ InferStandardSchemaInput,
12
13
  } from '@geekmidas/schema';
13
14
  import {
14
15
  convertSchemaWithComponents,
@@ -367,11 +368,12 @@ export class Endpoint<
367
368
  >,
368
369
  response: ResponseBuilder,
369
370
  ): OutSchema extends StandardSchemaV1
370
- ?
371
- | InferStandardSchema<OutSchema>
372
- | ResponseWithMetadata<InferStandardSchema<OutSchema>>
373
- | Promise<InferStandardSchema<OutSchema>>
374
- | Promise<ResponseWithMetadata<InferStandardSchema<OutSchema>>>
371
+ ? // Handler produces the output schema's INPUT type (the schema
372
+ // may coerce it on the way out — see EndpointHandler).
373
+ | InferStandardSchemaInput<OutSchema>
374
+ | ResponseWithMetadata<InferStandardSchemaInput<OutSchema>>
375
+ | Promise<InferStandardSchemaInput<OutSchema>>
376
+ | Promise<ResponseWithMetadata<InferStandardSchemaInput<OutSchema>>>
375
377
  :
376
378
  | any
377
379
  | ResponseWithMetadata<any>
@@ -1241,11 +1243,15 @@ export type EndpointHandler<
1241
1243
  >,
1242
1244
  response: ResponseBuilder,
1243
1245
  ) => OutSchema extends StandardSchemaV1
1244
- ?
1245
- | InferStandardSchema<OutSchema>
1246
- | ResponseWithMetadata<InferStandardSchema<OutSchema>>
1247
- | Promise<InferStandardSchema<OutSchema>>
1248
- | Promise<ResponseWithMetadata<InferStandardSchema<OutSchema>>>
1246
+ ? // The handler must produce the schema's INPUT type — the output
1247
+ // schema is allowed to coerce it (e.g. a `Date` → ISO `string`),
1248
+ // so the handler may return the looser input while consumers
1249
+ // (EndpointOutput / the generated client) still see the parsed
1250
+ // output type.
1251
+ | InferStandardSchemaInput<OutSchema>
1252
+ | ResponseWithMetadata<InferStandardSchemaInput<OutSchema>>
1253
+ | Promise<InferStandardSchemaInput<OutSchema>>
1254
+ | Promise<ResponseWithMetadata<InferStandardSchemaInput<OutSchema>>>
1249
1255
  :
1250
1256
  | unknown
1251
1257
  | ResponseWithMetadata<unknown>
@@ -0,0 +1,162 @@
1
+ import type { EnvironmentParser } from '@geekmidas/envkit';
2
+ import { wrapError } from '@geekmidas/errors';
3
+ import type { Logger } from '@geekmidas/logger';
4
+ import type { InferStandardSchema } from '@geekmidas/schema';
5
+ import type { Service, ServiceRecord } from '@geekmidas/services';
6
+ import { runWithRequestContext, ServiceDiscovery } from '@geekmidas/services';
7
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
8
+ import type {
9
+ Context,
10
+ Handler,
11
+ SQSBatchResponse,
12
+ SQSEvent,
13
+ SQSRecord,
14
+ } from 'aws-lambda';
15
+ import type { Queue } from './Queue';
16
+
17
+ export type AWSLambdaHandler<TEvent = any, TResult = any> = Handler<
18
+ TEvent,
19
+ TResult
20
+ >;
21
+
22
+ /**
23
+ * Runs a {@link Queue} worker as an AWS Lambda backed by an SQS event-source
24
+ * mapping. Unlike a subscriber, a queue drains *every* record (no type filter):
25
+ * each record body (`{ type, payload }`, as written by the SQS publisher) is
26
+ * unwrapped to its `payload`, validated against the queue's `messageSchema`, and
27
+ * the whole batch is handed to the handler as `messages`.
28
+ *
29
+ * Uses SQS partial-batch responses: a record that fails to parse, validate, or
30
+ * (when the handler throws) process is returned in `batchItemFailures` so SQS
31
+ * retries only those records.
32
+ */
33
+ export class AWSLambdaQueue<
34
+ TName extends string = string,
35
+ TMessage extends StandardSchemaV1 = StandardSchemaV1,
36
+ TServices extends Service[] = [],
37
+ TLogger extends Logger = Logger,
38
+ > {
39
+ private _services?: ServiceRecord<TServices>;
40
+
41
+ constructor(
42
+ private readonly envParser: EnvironmentParser<{}>,
43
+ readonly queue: Queue<TName, TMessage, TServices, TLogger>,
44
+ ) {}
45
+
46
+ get logger(): TLogger {
47
+ return this.queue.logger;
48
+ }
49
+
50
+ private async getServices(): Promise<ServiceRecord<TServices>> {
51
+ if (this._services) {
52
+ return this._services;
53
+ }
54
+
55
+ const serviceDiscovery = ServiceDiscovery.getInstance(this.envParser);
56
+
57
+ this._services =
58
+ this.queue.services.length > 0
59
+ ? ((await serviceDiscovery.register(
60
+ this.queue.services,
61
+ )) as ServiceRecord<TServices>)
62
+ : ({} as ServiceRecord<TServices>);
63
+
64
+ return this._services;
65
+ }
66
+
67
+ private safeJsonParse(value: string): unknown {
68
+ try {
69
+ return JSON.parse(value);
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Unwrap the queue payload from an SQS record. The SQS publisher writes
77
+ * `{ type, payload }`; a payload-only body is also accepted as-is.
78
+ */
79
+ private extractPayload(record: SQSRecord): unknown {
80
+ const body = this.safeJsonParse(record.body);
81
+ if (body && typeof body === 'object' && 'payload' in body) {
82
+ return (body as { payload: unknown }).payload;
83
+ }
84
+ return body ?? record.body;
85
+ }
86
+
87
+ private async _handler(
88
+ event: SQSEvent,
89
+ context: Context,
90
+ ): Promise<SQSBatchResponse> {
91
+ const logger = this.queue.logger.child({
92
+ queue: {
93
+ name: this.queue.name,
94
+ fn: context.functionName,
95
+ },
96
+ req: { id: context.awsRequestId },
97
+ }) as TLogger;
98
+
99
+ const services = await this.getServices();
100
+ const schema = this.queue.messageSchema;
101
+
102
+ const batchItemFailures: { itemIdentifier: string }[] = [];
103
+ const messages: InferStandardSchema<TMessage>[] = [];
104
+
105
+ for (const record of event.Records) {
106
+ try {
107
+ const payload = this.extractPayload(record);
108
+ const validation = await schema['~standard'].validate(payload);
109
+ if (validation.issues) {
110
+ logger.error(
111
+ { issues: validation.issues, messageId: record.messageId },
112
+ 'Queue message failed validation',
113
+ );
114
+ batchItemFailures.push({ itemIdentifier: record.messageId });
115
+ continue;
116
+ }
117
+ messages.push(validation.value as InferStandardSchema<TMessage>);
118
+ } catch (error) {
119
+ logger.error(
120
+ { error, messageId: record.messageId },
121
+ 'Failed to parse SQS record',
122
+ );
123
+ batchItemFailures.push({ itemIdentifier: record.messageId });
124
+ }
125
+ }
126
+
127
+ if (messages.length === 0) {
128
+ return { batchItemFailures };
129
+ }
130
+
131
+ try {
132
+ await this.queue.handler({ messages, services, logger });
133
+ } catch (error) {
134
+ logger.error(wrapError(error), 'Queue handler failed; retrying batch');
135
+ // Handler processes the batch atomically — fail the whole batch so SQS
136
+ // retries every (valid) record.
137
+ for (const record of event.Records) {
138
+ if (
139
+ !batchItemFailures.some((f) => f.itemIdentifier === record.messageId)
140
+ ) {
141
+ batchItemFailures.push({ itemIdentifier: record.messageId });
142
+ }
143
+ }
144
+ }
145
+
146
+ return { batchItemFailures };
147
+ }
148
+
149
+ get handler(): AWSLambdaHandler<SQSEvent, SQSBatchResponse> {
150
+ const handler = this._handler.bind(this);
151
+
152
+ return (async (event: SQSEvent, context: Context) => {
153
+ const startTime = Date.now();
154
+ const requestId = context.awsRequestId;
155
+ const logger = this.queue.logger.child({ requestId }) as TLogger;
156
+
157
+ return runWithRequestContext({ logger, requestId, startTime }, () =>
158
+ handler(event, context),
159
+ );
160
+ }) as unknown as AWSLambdaHandler<SQSEvent, SQSBatchResponse>;
161
+ }
162
+ }
@@ -0,0 +1,128 @@
1
+ import { environmentCase } from '@geekmidas/envkit';
2
+ import {
3
+ type EventPublisher,
4
+ type EventPublisherConnectionString,
5
+ type PublishableMessage,
6
+ Publisher,
7
+ } from '@geekmidas/events';
8
+ import type { Logger } from '@geekmidas/logger';
9
+ import { ConsoleLogger } from '@geekmidas/logger/console';
10
+ import type { InferStandardSchema } from '@geekmidas/schema';
11
+ import type { Service, ServiceRecord } from '@geekmidas/services';
12
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
13
+ import { Construct, ConstructType } from '../Construct';
14
+
15
+ const DEFAULT_LOGGER = new ConsoleLogger() as unknown as Logger;
16
+
17
+ /**
18
+ * The wire message a queue carries: `{ type: <queue name>, payload: <message> }`.
19
+ * The producer publishes this shape and the worker receives the `payload`s as
20
+ * its `messages`. Typing `type` to the queue name keeps the publisher fully
21
+ * typed and lets pg-boss (local) / SQS (deployed) route by name.
22
+ */
23
+ export type QueueMessage<
24
+ TName extends string,
25
+ TMessage extends StandardSchemaV1,
26
+ > = PublishableMessage<TName, InferStandardSchema<TMessage>>;
27
+
28
+ /**
29
+ * A queue worker — a point-to-point SQS-style queue and its single consumer.
30
+ * Unlike a `Subscriber` (topic fan-out, filtered by `subscribedEvents`), a queue
31
+ * drains every message of its one `message` type. Build one with `q` (see
32
+ * `QueueBuilder`); `gkm build` discovers it into the manifest's `queues` field.
33
+ *
34
+ * The producer side is {@link Queue.publisher} — a ready-to-inject `Service`
35
+ * that any endpoint/function drops into `.services([...])` to send messages.
36
+ */
37
+ export class Queue<
38
+ TName extends string = string,
39
+ TMessage extends StandardSchemaV1 = StandardSchemaV1,
40
+ TServices extends Service[] = [],
41
+ TLogger extends Logger = Logger,
42
+ > extends Construct<TLogger, string, undefined, undefined, TServices> {
43
+ __IS_QUEUE__ = true;
44
+
45
+ static isQueue(
46
+ obj: unknown,
47
+ ): obj is Queue<string, StandardSchemaV1, Service[], Logger> {
48
+ return Boolean(
49
+ obj &&
50
+ (obj as { __IS_QUEUE__?: boolean }).__IS_QUEUE__ === true &&
51
+ (obj as Construct).type === ConstructType.Queue,
52
+ );
53
+ }
54
+
55
+ constructor(
56
+ public readonly name: TName,
57
+ public readonly handler: QueueHandler<TMessage, TServices, TLogger>,
58
+ public readonly messageSchema: TMessage,
59
+ public override readonly timeout: number = 30000,
60
+ public override readonly services: TServices = [] as unknown as TServices,
61
+ public override readonly logger: TLogger = DEFAULT_LOGGER as TLogger,
62
+ /** SQS event-source batch size (deployed). */
63
+ public readonly batchSize?: number,
64
+ /** Whether the queue is FIFO (deployed). */
65
+ public readonly fifo?: boolean,
66
+ ) {
67
+ super(
68
+ ConstructType.Queue,
69
+ logger,
70
+ services,
71
+ [],
72
+ undefined,
73
+ undefined,
74
+ timeout,
75
+ );
76
+ }
77
+
78
+ /**
79
+ * The producer side — a `Service` exposing an `EventPublisher` typed to this
80
+ * queue's message. Inject it via `.services([queue.publisher])`; the handler
81
+ * then calls `services.<name>Publisher.publish([{ type, payload }])`.
82
+ *
83
+ * It reads `<NAME>_PUBLISHER_CONNECTION_STRING` and builds the transport from
84
+ * the URL protocol — `pgboss://` locally, `sqs://` deployed — so the same
85
+ * code publishes to Postgres in dev and SQS in prod. Because it's a `Service`,
86
+ * `Construct.getEnvironment()` sniffs it, so the env requirement flows into
87
+ * the manifest and infra links exactly this queue with least privilege.
88
+ */
89
+ get publisher(): Service<
90
+ `${TName}Publisher`,
91
+ EventPublisher<QueueMessage<TName, TMessage>>
92
+ > {
93
+ const envVar = `${environmentCase(this.name)}_PUBLISHER_CONNECTION_STRING`;
94
+ return {
95
+ serviceName: `${this.name}Publisher`,
96
+ async register({ envParser }) {
97
+ const { connectionString } = envParser
98
+ .create((get) => ({
99
+ connectionString: get(envVar).string(),
100
+ }))
101
+ .parse();
102
+
103
+ return Publisher.fromConnectionString<QueueMessage<TName, TMessage>>(
104
+ connectionString as EventPublisherConnectionString,
105
+ );
106
+ },
107
+ };
108
+ }
109
+ }
110
+
111
+ /** The context a queue handler receives — a batch of typed messages. */
112
+ export type QueueContext<
113
+ TMessage extends StandardSchemaV1,
114
+ TServices extends Service[] = [],
115
+ TLogger extends Logger = Logger,
116
+ > = {
117
+ messages: InferStandardSchema<TMessage>[];
118
+ services: ServiceRecord<TServices>;
119
+ logger: TLogger;
120
+ };
121
+
122
+ export type QueueHandler<
123
+ TMessage extends StandardSchemaV1,
124
+ TServices extends Service[] = [],
125
+ TLogger extends Logger = Logger,
126
+ > = (
127
+ ctx: QueueContext<TMessage, TServices, TLogger>,
128
+ ) => unknown | Promise<unknown>;