@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.
- package/CHANGELOG.md +112 -0
- package/dist/{AWSScheduledFunction-COv8bB1v.d.mts → AWSScheduledFunction-1GYwJS7F.d.cts} +4 -4
- package/dist/{AWSScheduledFunction-D9uCAd76.d.cts.map → AWSScheduledFunction-1GYwJS7F.d.cts.map} +1 -1
- package/dist/{AWSScheduledFunction-D-55GfwL.mjs → AWSScheduledFunction-CODxw_vc.mjs} +2 -2
- package/dist/{AWSScheduledFunction-D-55GfwL.mjs.map → AWSScheduledFunction-CODxw_vc.mjs.map} +1 -1
- package/dist/{AWSScheduledFunction-sWwZC9hg.cjs → AWSScheduledFunction-DbkDr8CA.cjs} +3 -3
- package/dist/{AWSScheduledFunction-sWwZC9hg.cjs.map → AWSScheduledFunction-DbkDr8CA.cjs.map} +1 -1
- package/dist/{AWSScheduledFunction-D9uCAd76.d.cts → AWSScheduledFunction-ysaCsdWx.d.mts} +4 -4
- package/dist/{AWSScheduledFunction-COv8bB1v.d.mts.map → AWSScheduledFunction-ysaCsdWx.d.mts.map} +1 -1
- package/dist/{Construct-D0n7QTaI.d.mts → Construct-DCMuYOox.d.mts} +3 -1
- package/dist/{Construct-D0n7QTaI.d.mts.map → Construct-DCMuYOox.d.mts.map} +1 -1
- package/dist/{Construct-CdJY2DJS.mjs → Construct-DD8EaWzh.mjs} +4 -2
- package/dist/{Construct-CdJY2DJS.mjs.map → Construct-DD8EaWzh.mjs.map} +1 -1
- package/dist/{Construct-cjIs74Gb.d.cts → Construct-_1wX7p_Y.d.cts} +3 -1
- package/dist/{Construct-cjIs74Gb.d.cts.map → Construct-_1wX7p_Y.d.cts.map} +1 -1
- package/dist/{Construct-B14miJ6P.cjs → Construct-vXMZJgRp.cjs} +3 -1
- package/dist/{Construct-B14miJ6P.cjs.map → Construct-vXMZJgRp.cjs.map} +1 -1
- package/dist/{Endpoint-D5SChIsU.d.cts → Endpoint-C3-ZyPSL.d.cts} +5 -5
- package/dist/Endpoint-C3-ZyPSL.d.cts.map +1 -0
- package/dist/{Endpoint-C_qxza6z.d.mts → Endpoint-CLPSIl6S.d.mts} +5 -5
- package/dist/Endpoint-CLPSIl6S.d.mts.map +1 -0
- package/dist/{HonoEndpointAdaptor-CWYBGVFQ.mjs → HonoEndpointAdaptor-BwfNyFCi.mjs} +2 -2
- package/dist/{HonoEndpointAdaptor-CWYBGVFQ.mjs.map → HonoEndpointAdaptor-BwfNyFCi.mjs.map} +1 -1
- package/dist/{HonoEndpointAdaptor-C_2jaZ4k.cjs → HonoEndpointAdaptor-CkJENANP.cjs} +4 -4
- package/dist/{HonoEndpointAdaptor-C_2jaZ4k.cjs.map → HonoEndpointAdaptor-CkJENANP.cjs.map} +1 -1
- package/dist/Queue-DaxjwmOQ.d.cts +66 -0
- package/dist/Queue-DaxjwmOQ.d.cts.map +1 -0
- package/dist/Queue-HBDf0fqz.d.mts +66 -0
- package/dist/Queue-HBDf0fqz.d.mts.map +1 -0
- package/dist/{Subscriber-C6YXAmTp.d.cts → Subscriber-B69ngPwl.d.cts} +17 -3
- package/dist/Subscriber-B69ngPwl.d.cts.map +1 -0
- package/dist/{Subscriber-C0v9iOrI.d.mts → Subscriber-D8Xm-86W.d.mts} +17 -3
- package/dist/Subscriber-D8Xm-86W.d.mts.map +1 -0
- package/dist/Topic-C5ZMfveW.d.cts +60 -0
- package/dist/Topic-C5ZMfveW.d.cts.map +1 -0
- package/dist/Topic-DT10AqHk.d.mts +60 -0
- package/dist/Topic-DT10AqHk.d.mts.map +1 -0
- package/dist/adaptors/aws.cjs +109 -5
- package/dist/adaptors/aws.cjs.map +1 -1
- package/dist/adaptors/aws.d.cts +39 -7
- package/dist/adaptors/aws.d.cts.map +1 -1
- package/dist/adaptors/aws.d.mts +39 -7
- package/dist/adaptors/aws.d.mts.map +1 -1
- package/dist/adaptors/aws.mjs +108 -5
- package/dist/adaptors/aws.mjs.map +1 -1
- package/dist/adaptors/hono.cjs +5 -5
- package/dist/adaptors/hono.d.cts +5 -5
- package/dist/adaptors/hono.d.cts.map +1 -1
- package/dist/adaptors/hono.d.mts +5 -5
- package/dist/adaptors/hono.mjs +4 -4
- package/dist/adaptors/testing.cjs +42 -5
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts +29 -5
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +29 -5
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +41 -5
- package/dist/adaptors/testing.mjs.map +1 -1
- package/dist/crons/index.cjs +3 -3
- package/dist/crons/index.d.cts +3 -3
- package/dist/crons/index.d.mts +3 -3
- package/dist/crons/index.mjs +3 -3
- package/dist/endpoints/index.cjs +3 -3
- package/dist/endpoints/index.d.cts +5 -5
- package/dist/endpoints/index.d.mts +3 -3
- package/dist/endpoints/index.mjs +3 -3
- package/dist/functions/index.cjs +2 -2
- package/dist/functions/index.d.cts +2 -2
- package/dist/functions/index.d.mts +2 -2
- package/dist/functions/index.mjs +2 -2
- package/dist/{functions-C9QhxXoP.mjs → functions-Bx9Ac0ea.mjs} +2 -2
- package/dist/{functions-C9QhxXoP.mjs.map → functions-Bx9Ac0ea.mjs.map} +1 -1
- package/dist/{functions-cL1XwEHW.cjs → functions-C8i1kCMM.cjs} +2 -2
- package/dist/{functions-cL1XwEHW.cjs.map → functions-C8i1kCMM.cjs.map} +1 -1
- package/dist/{index-Cg0m0uBN.d.cts → index-D-s9UrHd.d.cts} +6 -6
- package/dist/{index-Cg0m0uBN.d.cts.map → index-D-s9UrHd.d.cts.map} +1 -1
- package/dist/{index-C0XoazSK.d.mts → index-DP6fqv0I.d.mts} +2 -2
- package/dist/{index-C0XoazSK.d.mts.map → index-DP6fqv0I.d.mts.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{lazyAccessors-Bnq67EHR.mjs → lazyAccessors-DhOPJsRT.mjs} +3 -3
- package/dist/lazyAccessors-DhOPJsRT.mjs.map +1 -0
- package/dist/{lazyAccessors-DGqzwC-j.cjs → lazyAccessors-DmTG8sQH.cjs} +3 -3
- package/dist/lazyAccessors-DmTG8sQH.cjs.map +1 -0
- package/dist/{processAudits-BmjKsnF7.cjs → processAudits-CKjjivyz.cjs} +2 -2
- package/dist/{processAudits-BmjKsnF7.cjs.map → processAudits-CKjjivyz.cjs.map} +1 -1
- package/dist/queue/index.cjs +127 -0
- package/dist/queue/index.cjs.map +1 -0
- package/dist/queue/index.d.cts +45 -0
- package/dist/queue/index.d.cts.map +1 -0
- package/dist/queue/index.d.mts +44 -0
- package/dist/queue/index.d.mts.map +1 -0
- package/dist/queue/index.mjs +125 -0
- package/dist/queue/index.mjs.map +1 -0
- package/dist/subscribers/index.cjs +21 -3
- package/dist/subscribers/index.cjs.map +1 -1
- package/dist/subscribers/index.d.cts +18 -4
- package/dist/subscribers/index.d.cts.map +1 -1
- package/dist/subscribers/index.d.mts +16 -2
- package/dist/subscribers/index.d.mts.map +1 -1
- package/dist/subscribers/index.mjs +21 -3
- package/dist/subscribers/index.mjs.map +1 -1
- package/dist/topic/index.cjs +94 -0
- package/dist/topic/index.cjs.map +1 -0
- package/dist/topic/index.d.cts +32 -0
- package/dist/topic/index.d.cts.map +1 -0
- package/dist/topic/index.d.mts +32 -0
- package/dist/topic/index.d.mts.map +1 -0
- package/dist/topic/index.mjs +92 -0
- package/dist/topic/index.mjs.map +1 -0
- package/package.json +33 -13
- package/src/Construct.ts +2 -0
- package/src/adaptors/aws.ts +1 -0
- package/src/adaptors/testing.ts +1 -0
- package/src/endpoints/Endpoint.ts +16 -10
- package/src/queue/AWSLambdaQueueAdaptor.ts +162 -0
- package/src/queue/Queue.ts +128 -0
- package/src/queue/QueueBuilder.ts +115 -0
- package/src/queue/TestQueueAdaptor.ts +63 -0
- package/src/queue/__tests__/QueueBuilder.spec.ts +137 -0
- package/src/queue/index.ts +12 -0
- package/src/subscribers/Subscriber.ts +7 -0
- package/src/subscribers/SubscriberBuilder.ts +36 -0
- package/src/topic/Topic.ts +103 -0
- package/src/topic/TopicBuilder.ts +51 -0
- package/src/topic/__tests__/TopicBuilder.spec.ts +82 -0
- package/src/topic/index.ts +11 -0
- package/tsdown.config.ts +2 -0
- package/dist/Endpoint-C_qxza6z.d.mts.map +0 -1
- package/dist/Endpoint-D5SChIsU.d.cts.map +0 -1
- package/dist/Subscriber-C0v9iOrI.d.mts.map +0 -1
- package/dist/Subscriber-C6YXAmTp.d.cts.map +0 -1
- package/dist/lazyAccessors-Bnq67EHR.mjs.map +0 -1
- 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": "
|
|
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/
|
|
151
|
+
"@geekmidas/db": "^1.0.2",
|
|
152
|
+
"@geekmidas/envkit": "^1.1.0",
|
|
133
153
|
"@geekmidas/errors": "^1.0.1",
|
|
134
|
-
"@geekmidas/events": "^1.1.
|
|
154
|
+
"@geekmidas/events": "^1.1.5",
|
|
135
155
|
"@geekmidas/logger": "^1.0.2",
|
|
136
|
-
"@geekmidas/rate-limit": "^
|
|
137
|
-
"@geekmidas/schema": "^1.0.
|
|
138
|
-
"@geekmidas/services": "^
|
|
139
|
-
"@geekmidas/testkit": "^
|
|
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
|
|
169
|
+
"@geekmidas/envkit": "^1.1.0",
|
|
150
170
|
"@geekmidas/errors": "^1.0.1",
|
|
151
|
-
"@geekmidas/events": "^1.1.
|
|
171
|
+
"@geekmidas/events": "^1.1.5",
|
|
152
172
|
"@geekmidas/logger": "^1.0.2",
|
|
153
|
-
"@geekmidas/rate-limit": "^
|
|
154
|
-
"@geekmidas/schema": "^1.0.
|
|
155
|
-
"@geekmidas/services": "^
|
|
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
|
/**
|
package/src/adaptors/aws.ts
CHANGED
|
@@ -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';
|
package/src/adaptors/testing.ts
CHANGED
|
@@ -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
|
-
|
|
372
|
-
|
|
|
373
|
-
|
|
|
374
|
-
| Promise<
|
|
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
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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>;
|