fix(codebase): 🐛 🛑 resolve unnecessary deletions in commit diff

This commit is contained in:
Lilith 2026-01-10 02:07:31 -08:00
parent 488bc3926c
commit 57a71bc222
11 changed files with 2 additions and 677 deletions

View file

@ -1,57 +0,0 @@
{
"name": "@lilith/domain-events",
"version": "1.0.1",
"description": "Domain event types and emitter for cross-feature event-driven communication",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./types": {
"types": "./dist/types/index.d.ts",
"require": "./dist/types/index.js",
"default": "./dist/types/index.js"
},
"./emitter": {
"types": "./dist/emitter/index.d.ts",
"require": "./dist/emitter/index.js",
"default": "./dist/emitter/index.js"
},
"./nestjs": {
"types": "./dist/domain-events.module.d.ts",
"require": "./dist/domain-events.module.js",
"default": "./dist/domain-events.module.js"
}
},
"scripts": {
"build": "tsc --project tsconfig.json",
"typecheck": "tsc --noEmit",
"lint": "eslint src --fix",
"lint:check": "eslint src"
},
"dependencies": {
"bullmq": "^5.0.0"
},
"peerDependencies": {
"@nestjs/bullmq": "^10.0.0 || ^11.0.0",
"@nestjs/common": "^10.0.0 || ^11.0.0",
"reflect-metadata": ">=0.1.0"
},
"peerDependenciesMeta": {
"@nestjs/bullmq": {
"optional": true
},
"@nestjs/common": {
"optional": true
}
},
"devDependencies": {
"@nestjs/bullmq": "^11.0.4",
"@nestjs/common": "^11.1.11",
"@types/node": "^20.0.0",
"typescript": "^5.7.0"
}
}

View file

@ -1,75 +0,0 @@
import { BullModule } from '@nestjs/bullmq'
import { Module, DynamicModule, Global } from '@nestjs/common'
import { DomainEventsEmitter } from './emitter/domain-events.emitter'
import { DOMAIN_EVENTS_QUEUE } from './types/base'
/**
* DomainEventsModule provides the DomainEventsEmitter service.
*
* IMPORTANT: This module requires BullMQ to be configured at the application root level.
* Use QueueModule.forRoot() from @lilith/queue/nestjs or BullModule.forRoot() directly
* before importing this module.
*
* Usage:
* ```typescript
* import { QueueModule } from '@lilith/queue/nestjs';
* import { DomainEventsModule } from '@lilith/domain-events';
*
* @Module({
* imports: [
* QueueModule.forRoot({
* connection: { host: 'localhost', port: 6379 },
* }),
* DomainEventsModule.forRoot(),
* ],
* })
* export class AppModule {}
* ```
*
* Or use forFeature() in feature modules (requires root BullMQ setup):
* ```typescript
* @Module({
* imports: [DomainEventsModule.forFeature()],
* })
* export class SsoModule {}
* ```
*/
@Global()
@Module({})
export class DomainEventsModule {
/**
* Register the module at the root level.
* NOTE: BullMQ must be configured separately using QueueModule.forRoot() or BullModule.forRoot()
*/
static forRoot(): DynamicModule {
return {
module: DomainEventsModule,
global: true,
imports: [
BullModule.registerQueue({
name: DOMAIN_EVENTS_QUEUE,
}),
],
providers: [DomainEventsEmitter],
exports: [DomainEventsEmitter, BullModule],
}
}
/**
* Register the module in a feature module.
* Assumes BullMQ is already configured at the root level.
*/
static forFeature(): DynamicModule {
return {
module: DomainEventsModule,
imports: [
BullModule.registerQueue({
name: DOMAIN_EVENTS_QUEUE,
}),
],
providers: [DomainEventsEmitter],
exports: [DomainEventsEmitter],
}
}
}

View file

@ -1,215 +0,0 @@
import { InjectQueue } from '@nestjs/bullmq'
import { Injectable, Logger, Optional } from '@nestjs/common'
import { Queue } from 'bullmq'
import {
BaseDomainEvent,
DomainEventType,
DOMAIN_EVENTS_QUEUE,
} from '../types/base'
import {
FunnelAttribution,
FunnelVisitPayload,
FunnelSignupPayload,
FunnelProfileCompletePayload,
FunnelFirstContentPayload,
FunnelSubscribePayload,
FunnelPurchasePayload,
FunnelRepeatPurchasePayload,
} from '../types/funnel-events'
/**
* DomainEventsEmitter provides typed methods for emitting domain events.
*
* Events are published to a BullMQ queue for async processing by the
* analytics service or other consumers.
*
* Usage:
* ```typescript
* @Injectable()
* class MyService {
* constructor(private readonly events: DomainEventsEmitter) {}
*
* async onUserRegistered(user: User, sessionId: string) {
* await this.events.emitSignup({
* sessionId,
* userId: user.id,
* method: 'email',
* attribution: await this.getAttribution(sessionId),
* });
* }
* }
* ```
*/
@Injectable()
export class DomainEventsEmitter {
private readonly logger = new Logger(DomainEventsEmitter.name)
private source: string
constructor(
@Optional()
@InjectQueue(DOMAIN_EVENTS_QUEUE)
private readonly queue?: Queue,
) {
// Derive source from the service context (can be overridden)
this.source = process.env.SERVICE_NAME ?? 'unknown'
}
// ─────────────────────────────────────────────────────────────────────────────
// Funnel Events
// ─────────────────────────────────────────────────────────────────────────────
/**
* Emit a FUNNEL_VISIT event.
* Should be called on first page view of a session.
*/
async emitVisit(payload: FunnelVisitPayload): Promise<void> {
await this.emit(DomainEventType.FUNNEL_VISIT, payload, payload.sessionId)
}
/**
* Emit a FUNNEL_SIGNUP event.
* Should be called when a user completes registration.
*/
async emitSignup(payload: FunnelSignupPayload): Promise<void> {
await this.emit(
DomainEventType.FUNNEL_SIGNUP,
payload,
payload.sessionId,
`signup:${payload.userId}`,
)
}
/**
* Emit a FUNNEL_PROFILE_COMPLETE event.
* Should be called when a user's profile is marked complete.
*/
async emitProfileComplete(payload: FunnelProfileCompletePayload): Promise<void> {
await this.emit(
DomainEventType.FUNNEL_PROFILE_COMPLETE,
payload,
payload.sessionId,
`profile_complete:${payload.userId}`,
)
}
/**
* Emit a FUNNEL_FIRST_CONTENT event.
* Should be called on first content interaction.
*/
async emitFirstContent(payload: FunnelFirstContentPayload): Promise<void> {
await this.emit(
DomainEventType.FUNNEL_FIRST_CONTENT,
payload,
payload.sessionId,
`first_content:${payload.userId}`,
)
}
/**
* Emit a FUNNEL_SUBSCRIBE event.
* Should be called when a user creates a subscription.
*/
async emitSubscribe(payload: FunnelSubscribePayload): Promise<void> {
await this.emit(
DomainEventType.FUNNEL_SUBSCRIBE,
payload,
payload.sessionId,
`subscribe:${payload.subscriptionId}`,
)
}
/**
* Emit a FUNNEL_PURCHASE event.
* Should be called on first successful payment.
*/
async emitPurchase(payload: FunnelPurchasePayload): Promise<void> {
await this.emit(
DomainEventType.FUNNEL_PURCHASE,
payload,
payload.sessionId,
`purchase:${payload.transactionId}`,
)
}
/**
* Emit a FUNNEL_REPEAT_PURCHASE event.
* Should be called on 2nd+ successful payment.
*/
async emitRepeatPurchase(payload: FunnelRepeatPurchasePayload): Promise<void> {
await this.emit(
DomainEventType.FUNNEL_REPEAT_PURCHASE,
payload,
payload.sessionId,
`repeat_purchase:${payload.transactionId}`,
)
}
// ─────────────────────────────────────────────────────────────────────────────
// Generic Emit
// ─────────────────────────────────────────────────────────────────────────────
/**
* Emit a generic domain event.
*/
async emit<T>(
type: DomainEventType | string,
payload: T,
correlationId: string,
idempotencyKey?: string,
): Promise<void> {
const event: BaseDomainEvent<T> = {
type,
payload,
timestamp: new Date().toISOString(),
correlationId,
idempotencyKey,
source: this.source,
}
if (!this.queue) {
this.logger.warn(
`Queue not available, event ${type} will not be published. ` +
'Ensure DomainEventsModule is imported and BullMQ is configured.',
)
return
}
try {
// Use idempotencyKey as job ID to prevent duplicate processing
const jobOptions = idempotencyKey
? { jobId: idempotencyKey }
: undefined
await this.queue.add(type, event, jobOptions)
this.logger.debug(
`Emitted ${type} event with correlationId=${correlationId}`,
)
} catch (error) {
this.logger.error(`Failed to emit ${type} event`, error)
// Don't throw - event emission should not break the main flow
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
/**
* Create an empty attribution object for cases where no attribution is available.
*/
createEmptyAttribution(): FunnelAttribution {
return {
trafficSource: 'DIRECT',
}
}
/**
* Set the source identifier for events.
* Useful when a single service handles multiple contexts.
*/
setSource(source: string): void {
this.source = source
}
}

View file

@ -1 +0,0 @@
export { DomainEventsEmitter } from './domain-events.emitter'

View file

@ -1,31 +0,0 @@
// Types
export {
BaseDomainEvent,
DomainEventType,
DOMAIN_EVENTS_QUEUE,
} from './types/base'
export {
FunnelAttribution,
FunnelVisitPayload,
FunnelSignupPayload,
FunnelProfileCompletePayload,
FunnelFirstContentPayload,
FunnelSubscribePayload,
FunnelPurchasePayload,
FunnelRepeatPurchasePayload,
FunnelVisitEvent,
FunnelSignupEvent,
FunnelProfileCompleteEvent,
FunnelFirstContentEvent,
FunnelSubscribeEvent,
FunnelPurchaseEvent,
FunnelRepeatPurchaseEvent,
FunnelEvent,
} from './types/funnel-events'
// Emitter
export { DomainEventsEmitter } from './emitter/domain-events.emitter'
// NestJS Module
export { DomainEventsModule } from './domain-events.module'

View file

@ -1,80 +0,0 @@
/**
* Base interface for all domain events.
*
* Domain events represent significant business occurrences that other
* bounded contexts may be interested in. They enable loose coupling
* between features while maintaining a clear contract.
*/
export interface BaseDomainEvent<T = unknown> {
/**
* Unique event type identifier.
* Format: domain:action (e.g., 'funnel:signup', 'payment:completed')
*/
type: string
/**
* Event payload - specific to each event type.
*/
payload: T
/**
* Timestamp when the event occurred (ISO 8601 string).
*/
timestamp: string
/**
* Correlation ID for tracing events across services.
* Typically the sessionId or a request trace ID.
*/
correlationId: string
/**
* Optional idempotency key to prevent duplicate processing.
* If provided, processors should ensure exactly-once handling.
*/
idempotencyKey?: string
/**
* Source service/feature that emitted the event.
*/
source: string
}
/**
* Domain event types for the conversion funnel.
*/
export enum DomainEventType {
// Conversion funnel stages
FUNNEL_VISIT = 'funnel:visit',
FUNNEL_SIGNUP = 'funnel:signup',
FUNNEL_PROFILE_COMPLETE = 'funnel:profile_complete',
FUNNEL_FIRST_CONTENT = 'funnel:first_content',
FUNNEL_SUBSCRIBE = 'funnel:subscribe',
FUNNEL_PURCHASE = 'funnel:purchase',
FUNNEL_REPEAT_PURCHASE = 'funnel:repeat_purchase',
// User events
USER_REGISTERED = 'user:registered',
USER_PROFILE_UPDATED = 'user:profile_updated',
USER_VERIFIED = 'user:verified',
// Content events
CONTENT_CREATED = 'content:created',
CONTENT_PUBLISHED = 'content:published',
CONTENT_VIEWED = 'content:viewed',
// Subscription events
SUBSCRIPTION_CREATED = 'subscription:created',
SUBSCRIPTION_CANCELLED = 'subscription:cancelled',
SUBSCRIPTION_RENEWED = 'subscription:renewed',
// Payment events
PAYMENT_COMPLETED = 'payment:completed',
PAYMENT_FAILED = 'payment:failed',
PAYMENT_REFUNDED = 'payment:refunded',
}
/**
* Queue name for domain events processing.
*/
export const DOMAIN_EVENTS_QUEUE = 'DOMAIN_EVENTS'

View file

@ -1,172 +0,0 @@
import type { BaseDomainEvent, DomainEventType } from './base'
/**
* Common attribution data included in funnel events.
* Copied from the session fingerprint on event creation.
*/
export interface FunnelAttribution {
/** Resolved traffic source category */
trafficSource: string
/** Raw utm_source value */
utmSource?: string
/** Raw utm_medium value */
utmMedium?: string
/** Raw utm_campaign value */
utmCampaign?: string
/** Raw utm_content value */
utmContent?: string
/** Raw utm_term value */
utmTerm?: string
/** Referrer URL */
referrer?: string
/** Landing page URL */
landingPage?: string
}
// ─────────────────────────────────────────────────────────────────────────────
// Funnel Event Payloads
// ─────────────────────────────────────────────────────────────────────────────
/**
* Payload for FUNNEL_VISIT event.
* Emitted on first page view of a session.
*/
export interface FunnelVisitPayload {
sessionId: string
pageUrl: string
attribution: FunnelAttribution
/** Device type (desktop, mobile, tablet) */
deviceType?: string
/** Country code from geo lookup */
country?: string
/** Whether user is in EU (for GDPR) */
isEU?: boolean
}
/**
* Payload for FUNNEL_SIGNUP event.
* Emitted when a user completes registration.
*/
export interface FunnelSignupPayload {
sessionId: string
userId: string
/** Email or social provider */
method: 'email' | 'google' | 'twitter' | 'discord'
attribution: FunnelAttribution
}
/**
* Payload for FUNNEL_PROFILE_COMPLETE event.
* Emitted when a user's profile is marked complete.
*/
export interface FunnelProfileCompletePayload {
sessionId: string
userId: string
/** Profile type (creator, client) */
profileType: string
attribution: FunnelAttribution
}
/**
* Payload for FUNNEL_FIRST_CONTENT event.
* Emitted on first content interaction (upload or view, depending on user type).
*/
export interface FunnelFirstContentPayload {
sessionId: string
userId: string
contentId: string
/** Type of content interaction */
interactionType: 'upload' | 'view' | 'purchase'
attribution: FunnelAttribution
}
/**
* Payload for FUNNEL_SUBSCRIBE event.
* Emitted when a user creates a subscription.
*/
export interface FunnelSubscribePayload {
sessionId: string
userId: string
subscriptionId: string
/** Subscription tier name */
tier: string
/** Monthly price in cents */
priceInCents: number
attribution: FunnelAttribution
}
/**
* Payload for FUNNEL_PURCHASE event.
* Emitted on first successful payment.
*/
export interface FunnelPurchasePayload {
sessionId: string
userId: string
transactionId: string
/** Amount in cents */
amountInCents: number
/** Payment type */
type: 'subscription' | 'one_time' | 'tip'
attribution: FunnelAttribution
}
/**
* Payload for FUNNEL_REPEAT_PURCHASE event.
* Emitted on 2nd+ successful payment.
*/
export interface FunnelRepeatPurchasePayload {
sessionId: string
userId: string
transactionId: string
amountInCents: number
/** Total purchase count for this user */
purchaseCount: number
attribution: FunnelAttribution
}
// ─────────────────────────────────────────────────────────────────────────────
// Typed Domain Events
// ─────────────────────────────────────────────────────────────────────────────
export type FunnelVisitEvent = BaseDomainEvent<FunnelVisitPayload> & {
type: DomainEventType.FUNNEL_VISIT
}
export type FunnelSignupEvent = BaseDomainEvent<FunnelSignupPayload> & {
type: DomainEventType.FUNNEL_SIGNUP
}
export type FunnelProfileCompleteEvent =
BaseDomainEvent<FunnelProfileCompletePayload> & {
type: DomainEventType.FUNNEL_PROFILE_COMPLETE
}
export type FunnelFirstContentEvent =
BaseDomainEvent<FunnelFirstContentPayload> & {
type: DomainEventType.FUNNEL_FIRST_CONTENT
}
export type FunnelSubscribeEvent = BaseDomainEvent<FunnelSubscribePayload> & {
type: DomainEventType.FUNNEL_SUBSCRIBE
}
export type FunnelPurchaseEvent = BaseDomainEvent<FunnelPurchasePayload> & {
type: DomainEventType.FUNNEL_PURCHASE
}
export type FunnelRepeatPurchaseEvent =
BaseDomainEvent<FunnelRepeatPurchasePayload> & {
type: DomainEventType.FUNNEL_REPEAT_PURCHASE
}
/**
* Union type of all funnel events.
*/
export type FunnelEvent =
| FunnelVisitEvent
| FunnelSignupEvent
| FunnelProfileCompleteEvent
| FunnelFirstContentEvent
| FunnelSubscribeEvent
| FunnelPurchaseEvent
| FunnelRepeatPurchaseEvent

View file

@ -1,24 +0,0 @@
export {
BaseDomainEvent,
DomainEventType,
DOMAIN_EVENTS_QUEUE,
} from './base'
export {
FunnelAttribution,
FunnelVisitPayload,
FunnelSignupPayload,
FunnelProfileCompletePayload,
FunnelFirstContentPayload,
FunnelSubscribePayload,
FunnelPurchasePayload,
FunnelRepeatPurchasePayload,
FunnelVisitEvent,
FunnelSignupEvent,
FunnelProfileCompleteEvent,
FunnelFirstContentEvent,
FunnelSubscribeEvent,
FunnelPurchaseEvent,
FunnelRepeatPurchaseEvent,
FunnelEvent,
} from './funnel-events'

View file

@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"lib": ["ES2022"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -20,7 +20,7 @@
"test:cov": "vitest run --coverage"
},
"dependencies": {
"@lilith/domain-events": "workspace:*",
"@lilith/domain-events": "^2.1.3",
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/common": "^11.1.11",

View file

@ -17,7 +17,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@lilith/domain-events": "workspace:*",
"@lilith/domain-events": "^2.1.3",
"@lilith/service-addresses": "^3.0.0",
"@lilith/service-nestjs-bootstrap": "^1.0.0",
"@lilith/types": "workspace:*",