@skillstew/common 1.0.13 → 1.0.15

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.
@@ -0,0 +1,18 @@
1
+ import { EventName } from "../events/EventMap";
2
+ import { InfrastructureError } from "./AppError";
3
+ export declare class InvalidEventPayloadError extends InfrastructureError {
4
+ constructor(eventName: EventName, errorString: string);
5
+ toJSON(): object;
6
+ }
7
+ export declare class UnknownEventError extends InfrastructureError {
8
+ constructor(eventName: string);
9
+ toJSON(): object;
10
+ }
11
+ export declare class ConsumerUsedBeforeInitializationError extends InfrastructureError {
12
+ constructor();
13
+ toJSON(): object;
14
+ }
15
+ export declare class ProducerUsedBeforeInitializationError extends InfrastructureError {
16
+ constructor();
17
+ toJSON(): object;
18
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProducerUsedBeforeInitializationError = exports.ConsumerUsedBeforeInitializationError = exports.UnknownEventError = exports.InvalidEventPayloadError = void 0;
4
+ const AppError_1 = require("./AppError");
5
+ const MessageQueueErrorCodes_1 = require("./codes/MessageQueueErrorCodes");
6
+ class InvalidEventPayloadError extends AppError_1.InfrastructureError {
7
+ constructor(eventName, errorString) {
8
+ super(MessageQueueErrorCodes_1.MessageQueueErrorCodes.INVALID_EVENT_PAYLOAD + `for ${eventName}`, "INVALID_EVENT_PAYLOAD", { zodErrorString: errorString });
9
+ }
10
+ toJSON() {
11
+ return {
12
+ message: this.message,
13
+ code: this.code,
14
+ error: this.context.zodErrorStrings,
15
+ };
16
+ }
17
+ }
18
+ exports.InvalidEventPayloadError = InvalidEventPayloadError;
19
+ class UnknownEventError extends AppError_1.InfrastructureError {
20
+ constructor(eventName) {
21
+ super(MessageQueueErrorCodes_1.MessageQueueErrorCodes.UNKNOWN_EVENT + eventName, "UNKNOWN_EVENT");
22
+ }
23
+ toJSON() {
24
+ return { message: this.message, code: this.code };
25
+ }
26
+ }
27
+ exports.UnknownEventError = UnknownEventError;
28
+ class ConsumerUsedBeforeInitializationError extends AppError_1.InfrastructureError {
29
+ constructor() {
30
+ super(MessageQueueErrorCodes_1.MessageQueueErrorCodes.CONSUMER_USED_BEFORE_INITIALIZATION, "CONSUMER_USED_BEFORE_INITIALIZATION");
31
+ }
32
+ toJSON() {
33
+ return { message: this.message, code: this.code };
34
+ }
35
+ }
36
+ exports.ConsumerUsedBeforeInitializationError = ConsumerUsedBeforeInitializationError;
37
+ class ProducerUsedBeforeInitializationError extends AppError_1.InfrastructureError {
38
+ constructor() {
39
+ super(MessageQueueErrorCodes_1.MessageQueueErrorCodes.PRODUCER_USED_BEFORE_INITIALIZATION, "PRODUCER_USED_BEFORE_INITIALIZATION");
40
+ }
41
+ toJSON() {
42
+ return { message: this.message, code: this.code };
43
+ }
44
+ }
45
+ exports.ProducerUsedBeforeInitializationError = ProducerUsedBeforeInitializationError;
@@ -0,0 +1,6 @@
1
+ export declare const MessageQueueErrorCodes: {
2
+ readonly PRODUCER_USED_BEFORE_INITIALIZATION: "Producer used before initialization!";
3
+ readonly CONSUMER_USED_BEFORE_INITIALIZATION: "Consumer used before initialization!";
4
+ readonly UNKNOWN_EVENT: "Unknown event type";
5
+ readonly INVALID_EVENT_PAYLOAD: "Invalid payload";
6
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MessageQueueErrorCodes = void 0;
4
+ exports.MessageQueueErrorCodes = {
5
+ PRODUCER_USED_BEFORE_INITIALIZATION: "Producer used before initialization!",
6
+ CONSUMER_USED_BEFORE_INITIALIZATION: "Consumer used before initialization!",
7
+ UNKNOWN_EVENT: "Unknown event type",
8
+ INVALID_EVENT_PAYLOAD: "Invalid payload",
9
+ };
@@ -0,0 +1,18 @@
1
+ import { Channel } from "amqplib";
2
+ import { EventName } from "./EventMap";
3
+ import { AppEvent } from "./AppEvent";
4
+ export declare class Consumer {
5
+ private _channel;
6
+ private _exchange;
7
+ private _initialized;
8
+ private _handlers;
9
+ constructor();
10
+ init: (channel: Channel, exchange: string, serviceName: string, interestedEvents: (string | EventName)[]) => Promise<void>;
11
+ private handleEvent;
12
+ registerHandler: <T extends EventName>(eventName: T, handler: (event: AppEvent<T>) => Promise<HandlerResult>) => void;
13
+ }
14
+ interface HandlerResult {
15
+ success: boolean;
16
+ retryable?: boolean;
17
+ }
18
+ export {};
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.Consumer = void 0;
16
+ const EventMap_1 = require("./EventMap");
17
+ const MessageQueueErrors_1 = require("../errors/MessageQueueErrors");
18
+ const zod_1 = __importDefault(require("zod"));
19
+ class Consumer {
20
+ constructor() {
21
+ this._initialized = false;
22
+ this._handlers = new Map();
23
+ this.init = (channel, exchange, serviceName, interestedEvents) => __awaiter(this, void 0, void 0, function* () {
24
+ this._channel = channel;
25
+ this._exchange = exchange;
26
+ yield this._channel.assertExchange(this._exchange, "topic", {
27
+ durable: true,
28
+ });
29
+ const queue = yield this._channel.assertQueue(`${serviceName}_queue`, {
30
+ durable: true,
31
+ });
32
+ yield Promise.all(interestedEvents.map((eventName) => this._channel.bindQueue(queue.queue, this._exchange, eventName)));
33
+ this._channel.consume(queue.queue, this.handleEvent, { noAck: true });
34
+ this._initialized = true;
35
+ });
36
+ this.handleEvent = (msg) => __awaiter(this, void 0, void 0, function* () {
37
+ if (!msg) {
38
+ return;
39
+ }
40
+ const event = JSON.parse(msg.content.toString());
41
+ const eventName = event.eventName;
42
+ // validate using schema
43
+ const schema = EventMap_1.EventSchemas[eventName];
44
+ if (!schema) {
45
+ throw new MessageQueueErrors_1.UnknownEventError(eventName);
46
+ }
47
+ const parseResult = schema.safeParse(event.data);
48
+ if (!parseResult.success) {
49
+ this._channel.nack(msg);
50
+ const error = zod_1.default.prettifyError(parseResult.error);
51
+ throw new MessageQueueErrors_1.InvalidEventPayloadError(eventName, error);
52
+ }
53
+ // create typed app event
54
+ const appEvent = Object.assign(Object.assign({}, event), { data: parseResult.data });
55
+ try {
56
+ const handler = this._handlers.get(eventName);
57
+ if (!handler) {
58
+ this._channel.ack(msg); // ack if no handler
59
+ return;
60
+ }
61
+ const result = yield handler(appEvent);
62
+ if (result.success) {
63
+ this._channel.ack(msg);
64
+ }
65
+ else {
66
+ const requeue = result.retryable;
67
+ this._channel.nack(msg, false, requeue);
68
+ }
69
+ }
70
+ catch (err) {
71
+ console.log(err);
72
+ this._channel.nack(msg, false, false);
73
+ }
74
+ });
75
+ this.registerHandler = (eventName, handler) => {
76
+ if (!this._initialized) {
77
+ throw new MessageQueueErrors_1.ConsumerUsedBeforeInitializationError();
78
+ }
79
+ this._handlers.set(eventName, handler);
80
+ };
81
+ }
82
+ }
83
+ exports.Consumer = Consumer;
@@ -0,0 +1,3 @@
1
+ import { AppEvent } from "./AppEvent";
2
+ import { EventName, EventPayload } from "./EventMap";
3
+ export declare function CreateEvent<T extends EventName>(eventName: T, data: EventPayload<T>, producer: string, traceId?: string): AppEvent<T>;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateEvent = CreateEvent;
4
+ const crypto_1 = require("crypto");
5
+ function CreateEvent(eventName, data, producer, traceId) {
6
+ return {
7
+ eventName,
8
+ eventId: (0, crypto_1.randomUUID)(),
9
+ data,
10
+ producer,
11
+ timestamp: new Date().toISOString(),
12
+ traceId,
13
+ };
14
+ }
@@ -0,0 +1,10 @@
1
+ import { Channel } from "amqplib";
2
+ import { AnyAppEvent } from "./AppEvent";
3
+ export declare class Producer {
4
+ private _channel;
5
+ private _exchange;
6
+ private _initialized;
7
+ constructor();
8
+ init: (channel: Channel, exchange: string) => Promise<void>;
9
+ publish: (event: AnyAppEvent) => void;
10
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.Producer = void 0;
13
+ const MessageQueueErrors_1 = require("../errors/MessageQueueErrors");
14
+ class Producer {
15
+ constructor() {
16
+ this._initialized = false;
17
+ this.init = (channel, exchange) => __awaiter(this, void 0, void 0, function* () {
18
+ this._channel = channel;
19
+ this._exchange = exchange;
20
+ yield this._channel.assertExchange(this._exchange, "topic", {
21
+ durable: true,
22
+ });
23
+ this._initialized = true;
24
+ });
25
+ this.publish = (event) => {
26
+ if (!this._initialized) {
27
+ throw new MessageQueueErrors_1.ProducerUsedBeforeInitializationError();
28
+ }
29
+ const routingKey = event.eventName;
30
+ const message = Buffer.from(JSON.stringify(event));
31
+ this._channel.publish(this._exchange, routingKey, message, {
32
+ persistent: true,
33
+ });
34
+ };
35
+ }
36
+ }
37
+ exports.Producer = Producer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillstew/common",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "main": "./build/index.js",
5
5
  "types": "./build/index.d.ts",
6
6
  "scripts": {
@@ -15,9 +15,13 @@
15
15
  "dependencies": {
16
16
  "@types/express": "^5.0.3",
17
17
  "@types/jsonwebtoken": "^9.0.10",
18
+ "amqplib": "^0.10.8",
18
19
  "del-cli": "^6.0.0",
19
20
  "express": "^5.1.0",
20
21
  "jsonwebtoken": "^9.0.2",
21
22
  "zod": "^4.0.10"
23
+ },
24
+ "devDependencies": {
25
+ "@types/amqplib": "^0.10.7"
22
26
  }
23
27
  }
package/publish.sh ADDED
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+
3
+ # Exit immediately on error
4
+ set -e
5
+
6
+ # Change to correct dir
7
+ cd "/home/fahi/dev/brototype/skillStew/skill-stew-api/common/"
8
+
9
+ # Step 1: Bump patch version
10
+ npm version patch
11
+
12
+ # Step 2: Build the package
13
+ npm run build
14
+
15
+ # Step 3: Stage all changes
16
+ git add .
17
+
18
+ # Step 4: Prompt for a commit message
19
+ echo "Enter commit message:"
20
+ read COMMIT_MESSAGE
21
+ git commit -m "$COMMIT_MESSAGE"
22
+
23
+ # Step 5: Push to GitHub
24
+ echo "Enter remote branch to push to:"
25
+ read REMOTE_BRANCH
26
+ git push -u origin "$REMOTE_BRANCH"
27
+
28
+ # Step 6: Publish to npm
29
+ npm publish
@@ -0,0 +1,53 @@
1
+ import { EventName } from "../events/EventMap";
2
+ import { InfrastructureError } from "./AppError";
3
+ import { MessageQueueErrorCodes } from "./codes/MessageQueueErrorCodes";
4
+
5
+ export class InvalidEventPayloadError extends InfrastructureError {
6
+ constructor(eventName: EventName, errorString: string) {
7
+ super(
8
+ MessageQueueErrorCodes.INVALID_EVENT_PAYLOAD + `for ${eventName}`,
9
+ "INVALID_EVENT_PAYLOAD",
10
+ { zodErrorString: errorString },
11
+ );
12
+ }
13
+ toJSON(): object {
14
+ return {
15
+ message: this.message,
16
+ code: this.code,
17
+ error: this.context!.zodErrorStrings,
18
+ };
19
+ }
20
+ }
21
+
22
+ export class UnknownEventError extends InfrastructureError {
23
+ constructor(eventName: string) {
24
+ super(MessageQueueErrorCodes.UNKNOWN_EVENT + eventName, "UNKNOWN_EVENT");
25
+ }
26
+ toJSON(): object {
27
+ return { message: this.message, code: this.code };
28
+ }
29
+ }
30
+
31
+ export class ConsumerUsedBeforeInitializationError extends InfrastructureError {
32
+ constructor() {
33
+ super(
34
+ MessageQueueErrorCodes.CONSUMER_USED_BEFORE_INITIALIZATION,
35
+ "CONSUMER_USED_BEFORE_INITIALIZATION",
36
+ );
37
+ }
38
+ toJSON(): object {
39
+ return { message: this.message, code: this.code };
40
+ }
41
+ }
42
+
43
+ export class ProducerUsedBeforeInitializationError extends InfrastructureError {
44
+ constructor() {
45
+ super(
46
+ MessageQueueErrorCodes.PRODUCER_USED_BEFORE_INITIALIZATION,
47
+ "PRODUCER_USED_BEFORE_INITIALIZATION",
48
+ );
49
+ }
50
+ toJSON(): object {
51
+ return { message: this.message, code: this.code };
52
+ }
53
+ }
@@ -0,0 +1,6 @@
1
+ export const MessageQueueErrorCodes = {
2
+ PRODUCER_USED_BEFORE_INITIALIZATION: "Producer used before initialization!",
3
+ CONSUMER_USED_BEFORE_INITIALIZATION: "Consumer used before initialization!",
4
+ UNKNOWN_EVENT: "Unknown event type",
5
+ INVALID_EVENT_PAYLOAD: "Invalid payload",
6
+ } as const;
@@ -0,0 +1,108 @@
1
+ import { Channel, ConsumeMessage } from "amqplib";
2
+ import { EventName, EventSchemas } from "./EventMap";
3
+ import { AnyAppEvent, AppEvent } from "./AppEvent";
4
+ import {
5
+ ConsumerUsedBeforeInitializationError,
6
+ InvalidEventPayloadError,
7
+ UnknownEventError,
8
+ } from "../errors/MessageQueueErrors";
9
+ import z from "zod";
10
+
11
+ export class Consumer {
12
+ private _channel!: Channel;
13
+ private _exchange!: string;
14
+ private _initialized: boolean = false;
15
+ private _handlers = new Map<
16
+ EventName,
17
+ (event: any) => Promise<HandlerResult>
18
+ >();
19
+ constructor() {}
20
+
21
+ init = async (
22
+ channel: Channel,
23
+ exchange: string,
24
+ serviceName: string,
25
+ interestedEvents: (string | EventName)[], // ex: user.#, payments.subscribed
26
+ ) => {
27
+ this._channel = channel;
28
+ this._exchange = exchange;
29
+ await this._channel.assertExchange(this._exchange, "topic", {
30
+ durable: true,
31
+ });
32
+
33
+ const queue = await this._channel.assertQueue(`${serviceName}_queue`, {
34
+ durable: true,
35
+ });
36
+
37
+ await Promise.all(
38
+ interestedEvents.map((eventName) =>
39
+ this._channel.bindQueue(queue.queue, this._exchange, eventName),
40
+ ),
41
+ );
42
+
43
+ this._channel.consume(queue.queue, this.handleEvent, { noAck: true });
44
+
45
+ this._initialized = true;
46
+ };
47
+
48
+ private handleEvent = async (msg: ConsumeMessage | null) => {
49
+ if (!msg) {
50
+ return;
51
+ }
52
+ const event = JSON.parse(msg.content.toString()) as AnyAppEvent;
53
+ const eventName = event.eventName;
54
+
55
+ // validate using schema
56
+ const schema = EventSchemas[eventName];
57
+ if (!schema) {
58
+ throw new UnknownEventError(eventName);
59
+ }
60
+ const parseResult = schema.safeParse(event.data);
61
+ if (!parseResult.success) {
62
+ this._channel.nack(msg);
63
+ const error = z.prettifyError(parseResult.error);
64
+ throw new InvalidEventPayloadError(eventName, error);
65
+ }
66
+
67
+ // create typed app event
68
+ const appEvent: AppEvent<typeof eventName> = {
69
+ ...event,
70
+ data: parseResult.data,
71
+ };
72
+
73
+ try {
74
+ const handler = this._handlers.get(eventName);
75
+ if (!handler) {
76
+ this._channel.ack(msg); // ack if no handler
77
+ return;
78
+ }
79
+
80
+ const result = await handler(appEvent);
81
+
82
+ if (result.success) {
83
+ this._channel.ack(msg);
84
+ } else {
85
+ const requeue = result.retryable;
86
+ this._channel.nack(msg, false, requeue);
87
+ }
88
+ } catch (err) {
89
+ console.log(err);
90
+ this._channel.nack(msg, false, false);
91
+ }
92
+ };
93
+
94
+ registerHandler = <T extends EventName>(
95
+ eventName: T,
96
+ handler: (event: AppEvent<T>) => Promise<HandlerResult>,
97
+ ) => {
98
+ if (!this._initialized) {
99
+ throw new ConsumerUsedBeforeInitializationError();
100
+ }
101
+ this._handlers.set(eventName, handler);
102
+ };
103
+ }
104
+
105
+ interface HandlerResult {
106
+ success: boolean;
107
+ retryable?: boolean;
108
+ }
@@ -0,0 +1,19 @@
1
+ import { AppEvent } from "./AppEvent";
2
+ import { EventName, EventPayload } from "./EventMap";
3
+ import { randomUUID } from "crypto";
4
+
5
+ export function CreateEvent<T extends EventName>(
6
+ eventName: T,
7
+ data: EventPayload<T>,
8
+ producer: string,
9
+ traceId?: string,
10
+ ): AppEvent<T> {
11
+ return {
12
+ eventName,
13
+ eventId: randomUUID(),
14
+ data,
15
+ producer,
16
+ timestamp: new Date().toISOString(),
17
+ traceId,
18
+ };
19
+ }
@@ -0,0 +1,30 @@
1
+ import { Channel } from "amqplib";
2
+ import { AnyAppEvent } from "./AppEvent";
3
+ import { ProducerUsedBeforeInitializationError } from "../errors/MessageQueueErrors";
4
+
5
+ export class Producer {
6
+ private _channel!: Channel;
7
+ private _exchange!: string;
8
+ private _initialized: boolean = false;
9
+ constructor() {}
10
+
11
+ init = async (channel: Channel, exchange: string) => {
12
+ this._channel = channel;
13
+ this._exchange = exchange;
14
+ await this._channel.assertExchange(this._exchange, "topic", {
15
+ durable: true,
16
+ });
17
+ this._initialized = true;
18
+ };
19
+
20
+ publish = (event: AnyAppEvent) => {
21
+ if (!this._initialized) {
22
+ throw new ProducerUsedBeforeInitializationError();
23
+ }
24
+ const routingKey = event.eventName;
25
+ const message = Buffer.from(JSON.stringify(event));
26
+ this._channel.publish(this._exchange, routingKey, message, {
27
+ persistent: true,
28
+ });
29
+ };
30
+ }