/** * Base Backend Domain Service * * Extends BaseDomainService with common backend patterns: * - Automatic validation → mapper → repository → mapper flow * - Event emission throughout CRUD lifecycle * - Repository-based data access * - Configurable error throwing behavior * - Template methods with lifecycle hooks * * Backend domain services should extend this class to get automatic * CRUD operation handling with standardized validation, mapping, and events. * * Note: This class ONLY handles repository operations. If you need HTTP API * calls, import and use fetchers from `@plyaz/api` directly in your service. * * @example * ```typescript * import { BaseBackendDomainService } from '@plyaz/core/domain/base'; * import type { BaseRepository } from '@plyaz/db'; * * class MyBackendService extends BaseBackendDomainService< * MyBackendServiceConfig, * MyEntity, * MyResponseDTO, * CreateMyDTO, * UpdateMyDTO, * PatchMyDTO, * QueryMyDTO, * DeleteMyOptions, * MyRepository, * MyDatabaseRow, * MyMapper, * MyValidator * > { * protected repository: MyRepository; * protected eventPrefix = 'my-entity'; * * constructor(config: MyBackendServiceConfig) { * super({ * serviceName: 'MyBackendService', * supportedRuntimes: ['backend'], * serviceConfig: config, * mapperClass: MyMapperClass, * validatorClass: MyValidatorClass, * }); * this.repository = MyRepository.create(); * } * } * ``` */ import { BaseDomainService } from './BaseDomainService'; import { type CacheManager } from '@/base/cache'; import type { StreamChannel } from '@plyaz/types/core'; import type { CoreBaseBackendServiceConfig, CoreBaseMapperInstance, CoreBaseValidatorInstance, CoreServiceCreateOptions, CoreBaseServiceConfig, CrudOperationOptions, CoreStorageServiceInstance, CoreNotificationServiceInstance } from '@plyaz/types/core'; import type { UploadParams, UploadResult, GenerateFileParams } from '@plyaz/types/storage'; import type { SendEmailParams, NotificationResult, NotificationResponse } from '@plyaz/types/notifications'; import type { DatabaseResult, PaginatedResult, QueryOptions, OperationConfig, TransactionFn, TransactionOptions } from '@plyaz/types/db'; import type { CoreValidationStartedPayload, CoreValidationSuccessPayload, CoreValidationFailedPayload, CoreSanitizationStartedPayload, CoreSanitizationSuccessPayload, CoreEntityCreatingPayload, CoreEntityCreatedPayload, CoreEntityPatchingPayload, CoreEntityPatchedPayload, CoreEntityDeletingPayload, CoreEntityDeletedPayload, CoreEntityErrorPayload, CoreEntityCompletePayload, CoreOperationRequestPayload, CoreOperationResultPayload, CoreOperationErrorPayload } from '@plyaz/types/core'; export type { CoreBaseBackendServiceConfig } from '@plyaz/types/core'; /** * Union type of all event payloads used in BaseBackendDomainService. * Includes CRUD event payloads and generic storage/notification payloads. */ type BaseBackendEventPayload = CoreValidationStartedPayload | CoreValidationSuccessPayload | CoreValidationFailedPayload | CoreSanitizationStartedPayload | CoreSanitizationSuccessPayload | CoreEntityCreatingPayload | CoreEntityCreatedPayload | CoreEntityPatchingPayload | CoreEntityPatchedPayload | CoreEntityDeletingPayload | CoreEntityDeletedPayload | CoreEntityErrorPayload | CoreEntityCompletePayload | CoreOperationRequestPayload | CoreOperationResultPayload | CoreOperationErrorPayload; /** * Abstract base class for backend domain services with automatic CRUD handling. * * Provides: * - Template methods for create/patch/delete/getById * - Automatic validation → mapper → repository → mapper flow * - Event emission throughout lifecycle * - Repository-based data access only * - Configurable error throwing * - Lifecycle hooks for customization * * @typeParam TConfig - Service configuration type * @typeParam TEntity - Domain entity type * @typeParam TResponseDTO - API response DTO type * @typeParam TCreateDTO - Create DTO type (POST) * @typeParam TUpdateDTO - Update DTO type (PUT) * @typeParam TPatchDTO - Patch DTO type (PATCH) * @typeParam TQueryDTO - Query DTO type for listing/filtering (defaults to Record) * @typeParam TDeleteOptions - Delete options type * @typeParam TRepository - Repository type (must have create/update/delete/findById/findMany methods) * @typeParam TDatabaseRow - Database row type * @typeParam TStoreState - Store state type (serializable) * @typeParam TMapper - Mapper type * @typeParam TValidator - Validator type */ export declare abstract class BaseBackendDomainService = Partial, TQueryDTO extends object = object, TDeleteOptions = { soft: boolean; }, TDatabaseRow extends object = TCreateDTO & { id: string; }, TRepository extends { create(data: TCreateDTO): Promise>; update(id: string, data: TPatchDTO): Promise>; softDelete(id: string): Promise>; delete(id: string): Promise>; findById(id: string): Promise>; findMany(options?: QueryOptions, config?: OperationConfig): Promise>>; /** Get table name for transaction operations */ getTableName(): string; } = never, TStoreState = TEntity, TMapper extends CoreBaseMapperInstance = CoreBaseMapperInstance, TValidator extends CoreBaseValidatorInstance = CoreBaseValidatorInstance> extends BaseDomainService { /** * Repository instance for database operations. * Subclasses must initialize this in constructor. */ protected abstract repository: TRepository; /** * Event prefix for event emission (e.g., 'example', 'user', 'product'). * Used to construct event names like '{prefix}:creating', '{prefix}:created'. */ protected abstract eventPrefix: string; /** * Whether to use cache by default for all CRUD operations. * Set from config.cache.enabled or defaults to true. * Individual operations can override this via options.cache.useCache. * Only applies if cacheManager is available. */ protected useCacheByDefault: boolean; /** * Default TTL for cache entries (in seconds). * Set from config.cache.defaultTtl or defaults to 300 (5 minutes). */ protected defaultCacheTtl: number; /** * Storage service instance for file upload and document generation. * Injected by ServiceRegistry, backend-only. * * Provides: * - uploadFile() - Upload file (with optional template-based generation) * - generateFile() - Generate document from template (returns Buffer) * - downloadFile(), deleteFile(), getSignedUrl() */ protected readonly storageService?: CoreStorageServiceInstance; /** * Notification service instance for email, SMS, push. * Injected by ServiceRegistry, backend-only. * * Provides: * - sendEmail() - Send email using templates * - sendSMS() - Send SMS using templates * - sendPush() - Send push notifications */ protected readonly notificationsService?: CoreNotificationServiceInstance; /** * Check if should throw on validation errors (default: true) */ protected get throwOnValidationError(): boolean; /** * Check if should throw on repository errors (default: true) */ protected get throwOnRepositoryError(): boolean; /** * Check if should emit events (default: true) */ protected get emitEvents(): boolean; constructor(baseConfig: CoreBaseServiceConfig); /** * Create a new entity (POST) * * Flow: * 1. Assert service is ready * 2. Emit validation:started event * 3. Validate data using validator * 4. Emit validation:success event * 5. Emit sanitization:started event (Zod transforms already applied) * 6. Map to create DTO using mapper * 7. Emit creating event * 8. Call repository.create() * 9. Map response to domain entity * 10. Cache the new entity (if enabled) * 11. Emit created event * * @param data - Raw data to validate and create * @param options - Operation options (cache, etc.) * @returns Created entity * @throws ValidationError[] if validation fails (array of errors) * @throws CorePackageError if repository call fails */ create(data: TInput, options?: CrudOperationOptions): Promise; /** * Patch an entity (PATCH - partial update) * * Flow similar to create but for partial updates * * @param id - Entity ID * @param data - Raw data to validate and patch * @param options - Operation options (cache, etc.) * @returns Updated entity * @throws ValidationError[] if validation fails * @throws CorePackageError if repository call fails */ patch(id: string, data: TInput, options?: CrudOperationOptions): Promise; /** * Delete an entity (DELETE) * * @param id - Entity ID * @param deleteOptions - Delete options (e.g., soft delete) * @param crudOptions - CRUD operation options (cache, etc.) * @returns void * @throws ValidationError[] if options validation fails * @throws CorePackageError if repository call fails */ delete(id: string, deleteOptions?: TInput, crudOptions?: CrudOperationOptions): Promise; /** * Get entity by ID (GET) with cache-aside pattern * * Cache-aside flow: * 1. Check cache first (if enabled) * 2. If cache hit, return cached entity * 3. If cache miss, fetch from DB * 4. Cache the result (if enabled) * * @param id - Entity ID * @param options - Operation options (cache, pagination, etc.) * @returns Entity or null if not found * @throws CorePackageError if repository call fails * * @example * ```typescript * // With cache * const entity = await service.getById('123', { * cache: { useCache: true, cacheTtl: 300 } * }); * * // Without cache * const entity = await service.getById('123', { * cache: { useCache: false } * }); * * // With custom cache key (still prefixed with cachePrefix) * const entity = await service.getById('123', { * cache: { customKey: 'user:active:123' } * }); * // Final cache key will be: 'example:user:active:123' * ``` */ getById(id: string, options?: CrudOperationOptions): Promise; /** * Get all entities (list/query) * * Flow: * 1. Check cache (if enabled) * 2. If cache miss, fetch from repository * 3. Map response to domain entities * 4. Cache the result * 5. Return entities * * @param query - Query parameters for filtering/pagination * @param options - Operation options (cache, etc.) * @returns Array of entities * * @example * ```typescript * // Basic list * const users = await service.getAll(); * * // With filters * const users = await service.getAll({ status: 'active', page: 1, limit: 10 }); * * // With cache options * const users = await service.getAll( * { status: 'active' }, * { cache: { useCache: true, cacheTtl: 600 } } * ); * * // With custom cache key * const users = await service.getAll( * { status: 'active' }, * { cache: { customKey: CacheKeyBuilder.query('active', { status: 'active' }) } } * ); * ``` */ getAll(query?: Partial, options?: CrudOperationOptions): Promise; /** * Check if an entity exists by ID * * More efficient than getById when you only need to check existence. * Uses cache if available, falls back to repository.findById. * * @param id - Entity ID to check * @param options - Optional CRUD operation options * @returns Promise resolving to boolean (true if exists, false otherwise) * * @example * ```typescript * const exists = await service.exists('123'); * if (exists) { * // Entity exists * } * ``` */ exists(id: string, options?: CrudOperationOptions): Promise; /** * Create multiple entities at once (bulk operation) * * **Transaction Support (Default: ON)** * - Uses database transaction by default for atomic operations * - All entities created successfully or none (rollback on failure) * - Falls back to sequential creation if transactions not supported * - Disable with `options.transaction.useTransaction: false` * * @param dataArray - Array of data to create entities from * @param options - Optional CRUD operation options (includes transaction config) * @returns Promise resolving to array of created entities * * @example * ```typescript * // Default: Uses transaction (atomic) * const entities = await service.bulkCreate([ * { name: 'Entity 1', amount: 100 }, * { name: 'Entity 2', amount: 200 }, * ]); * * // Explicit: Disable transaction (sequential, partial failures possible) * const entities = await service.bulkCreate(data, { * transaction: { useTransaction: false } * }); * ``` */ bulkCreate(dataArray: TInput[], options?: CrudOperationOptions): Promise; /** * Delete multiple entities at once (bulk operation) * * **Transaction Support (Default: ON)** * - Uses database transaction by default for atomic operations * - All entities deleted successfully or none (rollback on failure) * - Falls back to sequential deletion if transactions not supported * - Disable with `crudOptions.transaction.useTransaction: false` * * @param ids - Array of entity IDs to delete * @param deleteOptions - Optional delete options (soft/hard delete) * @param crudOptions - Optional CRUD operation options (includes transaction config) * @returns Promise resolving when all deletions complete * * @example * ```typescript * // Default: Uses transaction (atomic) * await service.bulkDelete(['id1', 'id2', 'id3'], { soft: true }); * * // Explicit: Disable transaction * await service.bulkDelete(ids, { soft: true }, { * transaction: { useTransaction: false } * }); * ``` */ bulkDelete(ids: string[], deleteOptions?: TInput, crudOptions?: CrudOperationOptions): Promise; /** * Execute operations within a database transaction * * Provides atomic operations with automatic rollback on failure. * Uses the underlying database adapter's transaction support. * * @param fn - Function containing operations to execute within transaction * @param options - Optional transaction configuration * @returns Promise resolving to the transaction result * @throws Error if transaction fails (auto-rollback occurs) * * @example * ```typescript * // Single service transaction * const result = await userService.withTransaction(async (trx) => { * const user = await trx.create('users', userData); * const profile = await trx.create('profiles', { userId: user.id, ...profileData }); * return { user, profile }; * }); * * // Cross-service transaction (using shared db instance) * const db = Core.db.getDatabase(); * const result = await db.transaction(async (trx) => { * await trx.create('users', userData); * await trx.create('audit_logs', { action: 'user_created', ... }); * }); * ``` */ withTransaction(fn: TransactionFn, options?: TransactionOptions): Promise>; /** * Check if transaction support is available * * @returns true if the database adapter supports transactions */ get supportsTransactions(): boolean; /** * Called before create operation * Override to add custom logic before creating */ protected beforeCreate(_data: TCreateDTO): Promise; /** * Called after create operation * Override to add custom logic after creating */ protected afterCreate(_entity: TEntity): Promise; /** * Called before bulk create operation * Override to add custom logic before bulk creating */ protected beforeBulkCreate(_dataArray: TInput[]): Promise; /** * Called after bulk create operation * Override to add custom logic after bulk creating */ protected afterBulkCreate(_entities: TEntity[], _dataArray: TInput[]): Promise; /** * Called before getAll operation * Override to add custom logic before fetching list */ protected beforeGetAll(_query?: Partial): Promise; /** * Called after getAll operation * Override to add custom logic after fetching list (e.g., invalidate related caches) */ protected afterGetAll(_entities: TEntity[], _query?: Partial): Promise; /** * Called before patch operation * Override to add custom logic before patching */ protected beforePatch(_id: string, _data: TPatchDTO): Promise; /** * Called after patch operation * Override to add custom logic after patching */ protected afterPatch(_id: string, _entity: TEntity): Promise; /** * Called before delete operation * Override to add custom logic before deleting */ protected beforeDelete(_id: string, _options: TDeleteOptions): Promise; /** * Called after delete operation * Override to add custom logic after deleting */ protected afterDelete(_id: string, _options: TDeleteOptions): Promise; /** * Called before bulk delete operation * Override to add custom logic before bulk deleting */ protected beforeBulkDelete(_ids: string[], _deleteOptions?: TInput): Promise; /** * Called after bulk delete operation * Override to add custom logic after bulk deleting */ protected afterBulkDelete(_ids: string[], _deleteOptions?: TInput): Promise; /** * Called before exists check * Override to add custom logic before checking existence */ protected beforeExists(_id: string): Promise; /** * Called after exists check * Override to add custom logic after checking existence */ protected afterExists(_id: string, _exists: boolean): Promise; /** * Get default delete options * Override to customize default behavior */ protected getDefaultDeleteOptions(): TDeleteOptions; /** * Emit event with prefix * Only emits if emitEvents is true */ protected emitEvent(event: string, payload: TPayload): void; /** * Build cache key with service prefix. * ALWAYS applies the cachePrefix. Override this method to customize key structure. * * @param key - Cache key (without prefix) * @returns Prefixed cache key (e.g., 'example:entity:123') * * @example * ```typescript * // Default behavior * buildCacheKey('entity:123') // Returns: 'example:entity:123' * * // Override for custom namespacing * protected buildCacheKey(key: string): string { * const baseKey = super.buildCacheKey(key); * return `${baseKey}:tenant:${this.tenantId}`; * } * ``` */ protected buildCacheKey(key: string): string; /** * Get value from cache with automatic prefix. * Prefix is ALWAYS applied via buildCacheKey(). * * @param key - Cache key (will be prefixed automatically) * @returns Cached value or null */ protected cacheGet(key: string): Promise; /** * Set value in cache with automatic prefix. * Prefix is ALWAYS applied via buildCacheKey(). * * @param key - Cache key (will be prefixed automatically) * @param value - Value to cache * @param ttl - TTL in seconds (optional) */ protected cacheSet(key: string, value: T, ttl?: number): Promise; /** * Delete value from cache with automatic prefix. * Prefix is ALWAYS applied via buildCacheKey(). * * @param key - Cache key (will be prefixed automatically) */ protected cacheDelete(key: string): Promise; /** * Clear all cache for this service (by prefix pattern) */ protected cacheClear(): Promise; /** * Check if cache is available */ protected get hasCacheManager(): boolean; /** * Get cache manager instance from create options. * The cache instance is injected by ServiceRegistry, not created here. * * @param options - Service create options from ServiceRegistry * @returns CacheManager instance or undefined if not available */ static getCacheManager(options?: CoreServiceCreateOptions): CacheManager | undefined; /** * Track a CRUD operation with observability (span + metrics). * Wraps an operation with automatic span creation, duration recording, and error tracking. * * @param operation - Operation name (create, patch, delete, getById, getAll) * @param entityId - Optional entity ID for context * @param fn - The operation function to execute * @returns Result of the operation * * @example * ```typescript * const entity = await this.trackOperation('create', undefined, async () => { * return await this.repository.create(data); * }); * ``` */ protected trackOperation(operation: 'create' | 'patch' | 'delete' | 'getById' | 'getAll' | 'bulkCreate' | 'bulkDelete' | 'transaction', entityId: string | undefined, fn: () => Promise): Promise; /** * Record operation metrics (duration and count). * Called automatically by CRUD methods when observability is enabled. */ protected recordOperationMetrics(operation: string, duration: number, success: boolean): Promise; /** * Record cache hit/miss metric. */ protected recordCacheMetric(hit: boolean, operation: string): Promise; /** * Upload a file to storage. * * Uses storageService.uploadFile() which supports: * - Direct file upload (buffer/stream) * - Template-based document generation + upload (when templateId provided) * * When templateId is provided: * 1. Loads template from templates/{locale}/{category}/{templateId}.md * 2. Renders with Handlebars + templateData * 3. Converts to PDF/DOCX/XLSX * 4. Uploads to storage (R2/Supabase) * 5. Returns URL * * NOTE: In production, generated documents should be linked to entities via * a media table with foreign keys (e.g., campaign_id -> media.entity_id). * * @param params - Upload parameters (supports all UploadParams options) * @returns Promise resolving to upload result with URL * @throws CorePackageError if storage service is not available */ uploadFile(params: Partial): Promise; /** * Lifecycle hook: called before file upload. * Override in subclass for custom pre-upload logic. */ protected beforeUploadFile?(params: Partial): Promise; /** * Lifecycle hook: called after file upload. * Override in subclass for custom post-upload logic. */ protected afterUploadFile?(result: UploadResult): Promise; /** * Upload multiple files to storage with concurrency control. * * Uses storageService.uploadMultipleFiles() which: * 1. Processes files in parallel with configurable concurrency * 2. Optionally continues on error (partial success) * 3. Returns results for each file * * @param files - Array of upload parameters for each file * @param options - Bulk upload options (concurrency, continueOnError, useQueue) * @returns Promise resolving to array of upload results * @throws CorePackageError if storage service is not available */ uploadFiles(files: Partial[], options?: { concurrency?: number; continueOnError?: boolean; useQueue?: boolean; }): Promise; /** * Lifecycle hook: called before bulk file upload. * Override in subclass for custom pre-upload logic. */ protected beforeUploadFiles?(files: Partial[], options?: { concurrency?: number; continueOnError?: boolean; useQueue?: boolean; }): Promise; /** * Lifecycle hook: called after bulk file upload. * Override in subclass for custom post-upload logic. */ protected afterUploadFiles?(results: UploadResult[]): Promise; /** * Check if storage service is available. */ protected get hasStorage(): boolean; /** * Generate a document from template (NO upload - returns Buffer). * * Uses storageService.generateFile() which: * 1. Loads template from templates/{locale}/{category}/{templateId}.md * 2. Renders with Handlebars + templateData * 3. Converts to PDF/DOCX/XLSX * 4. Returns Buffer (caller handles the buffer) * * For generation WITH upload, use uploadFile() with templateId instead. * * @param params - Generation parameters (templateId, templateData, outputFormat, etc.) * @returns Promise resolving to generated document Buffer * @throws CorePackageError if storage service is not available */ generateFile(params: Partial): Promise; /** * Lifecycle hook: called before document generation. * Override in subclass for custom pre-generation logic. */ protected beforeGenerateFile?(params: Partial): Promise; /** * Lifecycle hook: called after document generation. * Override in subclass for custom post-generation logic. */ protected afterGenerateFile?(buffer: Buffer): Promise; /** * Send an email using notification templates. * * Uses notificationsService.sendEmail() which: * 1. Loads template from templates/{locale}/email/{templateId}.md * 2. Parses frontmatter (subject, layout, etc.) * 3. Renders Markdown to HTML with templateData * 4. Inlines CSS for email clients * 5. Sends via configured provider (Infobip/SendGrid) * * NOTE: In production, sent emails should be tracked in a notifications * table with foreign keys (e.g., campaign_id -> notifications.entity_id). * * @param params - Email parameters (to, templateId, templateData, etc.) * @returns Promise resolving to send result * @throws CorePackageError if notifications service is not available */ sendEmail(params: Partial): Promise>; /** * Lifecycle hook: called before email sending. * Override in subclass for custom pre-send logic. */ protected beforeSendEmail?(params: Partial): Promise; /** * Lifecycle hook: called after email sending. * Override in subclass for custom post-send logic. */ protected afterSendEmail?(result: NotificationResult): Promise; /** * Check if notifications service is available. */ protected get hasNotifications(): boolean; /** * Check if stream server/broadcaster is available. * The stream server must be initialized via StreamRegistry.initialize(). */ protected get hasStreamServer(): boolean; /** * Emit a custom stream event to connected clients. * * Use this for domain-specific events that aren't covered by the standard * progress methods. The event is broadcast to the specified channel. * * @param channel - Channel to broadcast to (e.g., 'notifications', 'orders:123') * @param event - Event name (e.g., 'order:status_changed') * @param data - Event payload data * * @example * ```typescript * // Emit order status change * this.emitStreamEvent( * `order:${orderId}`, * 'order:status_changed', * { orderId, status: 'shipped', trackingNumber: '123ABC' } * ); * ``` */ protected emitStreamEvent(channel: StreamChannel | string, event: string, data: T): void; /** * Emit an entity-specific stream event. * Convenience method that constructs the channel from entity type and ID. * * @param entityType - Entity type (e.g., 'order', 'invoice', 'user') * @param entityId - Entity ID * @param event - Event name (e.g., 'updated', 'deleted') * @param data - Event payload data * * @example * ```typescript * // Emit when an order is updated * this.emitEntityStreamEvent('order', orderId, 'updated', { status: 'completed' }); * // Broadcasts to channel: 'order:abc123' with event: 'order:updated' * ``` */ protected emitEntityStreamEvent(entityType: string, entityId: string, event: string, data: T): void; } //# sourceMappingURL=BaseBackendDomainService.d.ts.map