diff --git a/PACKAGE_GENERATOR.md b/PACKAGE_GENERATOR.md deleted file mode 100644 index 63b17b0..0000000 --- a/PACKAGE_GENERATOR.md +++ /dev/null @@ -1,173 +0,0 @@ -# Package Generator Guide - -Quick and easy way to scaffold new packages in the Rineex monorepo following the -established structure and configuration standards. - -## Usage - -### Option 1: Using npm script (Recommended) - -```bash -pnpm g:pkg [description] -``` - -### Option 2: Using Node.js directly - -```bash -node scripts/generate-package.mjs [description] -``` - -### Option 3: Using shell script - -```bash -./scripts/generate-package.sh [description] -``` - -## Examples - -### Create a basic package - -```bash -pnpm g:pkg validation -``` - -### Create a package with description - -```bash -pnpm g:pkg validation "Input validation utilities" -``` - -### Create a more complex package - -```bash -pnpm g:pkg cache-manager "Redis and in-memory cache management" -``` - -## What Gets Generated - -The generator creates a fully configured package with: - -- **package.json** - Complete configuration with: - - Proper npm scripts (build, test, lint, check-types) - - ESM/CJS dual exports - - TypeScript definitions - - Workspace dependencies - - Publishable metadata - -- **TypeScript Configuration** - - tsconfig.json - - tsconfig.build.json (for builds) - - Path aliases (@/\*) - -- **Build Configuration** - - tsup.config.ts (for bundling) - - vitest.config.ts (for testing) - - eslint.config.mjs (for linting) - -- **Initial Files** - - src/index.ts (empty export) - - README.md (with installation and development instructions) - -## Package Naming Rules - -Package names must: - -- Use lowercase letters, numbers, and hyphens -- Start and end with a letter or number -- Be unique in the packages directory - -Examples: ✅ `auth-core`, `cache-manager`, `validation` ❌ `Auth-Core`, -`cache_manager` - -## Next Steps After Generation - -1. **Install dependencies** - - ```bash - pnpm install - ``` - -2. **Develop your package** - - Add code to `packages//src/` - - Export types/functions from `src/index.ts` - - Create test files with `.spec.ts` suffix - -3. **Common development commands** - - ```bash - # For a specific package - pnpm -F @rineex/ build - pnpm -F @rineex/ test - pnpm -F @rineex/ lint - pnpm -F @rineex/ check-types - - # Or from the package directory - cd packages/ - pnpm build - ``` - -4. **Publishing** - - Add a changeset: `pnpm changeset` - - Version and publish: `pnpm changeset:publish` - -## Configuration Details - -All generated packages use: - -- **TypeScript**: v5.9.3 -- **Build Tool**: tsup (for ESM/CJS bundling) -- **Test Runner**: vitest -- **Linter**: ESLint with @rineex/eslint-config -- **Node.js**: ES6 target, CommonJS module format -- **License**: Apache-2.0 - -## Package Structure - -``` -packages// -├── src/ -│ └── index.ts -├── package.json -├── tsconfig.json -├── tsconfig.build.json -├── tsup.config.ts -├── vitest.config.ts -├── eslint.config.mjs -├── README.md -└── dist/ (generated after build) -``` - -## Tips - -- Use descriptive package names that indicate their purpose -- Keep packages focused on a single responsibility -- Reference similar packages (like `ddd` or `auth-core`) as examples -- Test your package locally before publishing - -## Troubleshooting - -**Package already exists** - -``` -Error: Package directory already exists -``` - -Choose a different package name or remove the existing directory first. - -**Invalid package name** - -``` -Error: Invalid package name. Use lowercase letters, numbers, and hyphens only. -``` - -Ensure your package name follows the naming rules above. - -**Build fails after generation** - -```bash -# Run pnpm install in the workspace root -cd /path/to/core -pnpm install -``` - -Then try building again: `pnpm -F @rineex/ build` diff --git a/README.md b/README.md index 3edc495..0244b9a 100644 --- a/README.md +++ b/README.md @@ -1,528 +1,51 @@ # Rineex Core -> A monorepo containing essential, production-grade packages for building -> scalable and maintainable TypeScript/Node.js applications. +Rineex Core is a pnpm workspace of framework-agnostic domain primitives and +reusable NestJS integrations. Published packages are built as CommonJS, ESM, and +TypeScript declarations from their `src/index.ts` entry points. -> ⚠️ **Note**: This package is currently under heavy development. APIs may -> change without notice. - -[![monorepo](https://img.shields.io/badge/monorepo-turbo-blue.svg)](https://turbo.build/) -[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/) - -## Overview - -Rineex Core is a collection of carefully crafted, battle-tested packages -designed following Domain-Driven Design (DDD) principles and industry best -practices. Each package is independently versioned and published to npm, -enabling teams to adopt only what they need. - -## Packages - -### [`@rineex/ddd`](./packages/ddd) – Domain-Driven Design Framework - -[![npm version](https://img.shields.io/npm/v/@rineex/ddd)](https://www.npmjs.com/package/@rineex/ddd) -[![npm downloads](https://img.shields.io/npm/dm/@rineex/ddd)](https://www.npmjs.com/package/@rineex/ddd) - -A comprehensive, production-grade TypeScript library providing battle-tested -abstractions for implementing Domain-Driven Design patterns. Build maintainable, -scalable applications with type-safe entities, aggregates, value objects, and -domain events. - -#### Quick Summary - -- **Value Objects**: Immutable objects distinguished by value, not identity -- **Entities**: Objects with unique identity and lifecycle -- **Aggregate Roots**: Entry points to aggregates with invariant enforcement and - event support -- **Domain Events**: Immutable records of significant domain occurrences -- **Application Services**: Orchestrators for use cases and commands -- **Zero Dependencies**: Lightweight with only peer dependencies - -#### Key Features - -✨ **Type-Safe Abstractions** - Fully typed base classes for all DDD building -blocks -🔒 **Immutability by Default** - Value objects and entities are frozen to -prevent mutations -📡 **Domain Events Support** - First-class support for event sourcing and -event-driven architectures -✅ **Built-in Validation** - Enforce domain rules at aggregate boundaries -⚡ **Zero Dependencies** - Minimal bundle footprint for maximum flexibility -🏢 **Production Ready** - Used in high-performance systems at scale - -#### Quick Start - -```bash -npm install @rineex/ddd -``` - -```typescript -import { - AggregateRoot, - ValueObject, - AggregateId, - DomainEvent, -} from '@rineex/ddd'; - -// Define a Value Object -class Email extends ValueObject { - public static create(value: string) { - return new Email(value); - } - protected validate(props: string): void { - if (!props.includes('@')) throw new Error('Invalid email'); - } -} - -// Create an Aggregate Root -interface UserProps { - email: Email; - isActive: boolean; -} - -class User extends AggregateRoot { - get email(): Email { - return this.props.email; - } - protected validate(): void { - if (!this.email) throw new Error('Email is required'); - } -} - -// Use it -const user = new User({ - id: AggregateId.create(), - createdAt: new Date(), - props: { email: Email.create('user@example.com'), isActive: true }, -}); -``` - -#### Comprehensive Documentation - -For in-depth guidance, examples, and best practices, see the -[**@rineex/ddd Documentation**](./packages/ddd/README.md): - -- 📚 [Core Concepts Guide](./packages/ddd/README.md#core-concepts) - Value - Objects, Entities, Aggregates, Events -- 🔧 [Complete API Reference](./packages/ddd/README.md#api-reference) - All - classes, interfaces, and methods -- 💡 [Examples](./packages/ddd/README.md#examples) - Order aggregate and - integration patterns -- ✅ [Best Practices](./packages/ddd/README.md#best-practices) - DDD patterns - and principles -- 🚨 [Error Handling](./packages/ddd/README.md#error-handling) - Registry-backed - domain errors and Result -- 🤝 [Contributing Guide](./packages/ddd/README.md#contributing) - Development - setup and guidelines - -#### Included Value Objects - -- `AggregateId` - Unique identifier for aggregates -- `Email` - Email address validation -- `IPAddress` - IPv4/IPv6 validation - ---- - -### [`@rineex/ioredis`](./packages/ioredis) – Redis Adapter for NestJS - -[![npm version](https://img.shields.io/npm/v/@rineex/ioredis)](https://www.npmjs.com/package/@rineex/ioredis) - -NestJS integration for [`ioredis`](https://www.npmjs.com/package/ioredis), -providing Redis client management with dependency injection, lifecycle -management, and health check integration. - -#### Key Features - -- **Multiple Connections** - Support for single instance or cluster - configurations with named connections -- **Lifecycle Management** - Automatic connection health checks on bootstrap and - graceful shutdown -- **Health Check Integration** - Built-in Terminus health indicator for - monitoring -- **Dependency Injection** - Seamless NestJS DI integration with - `@InjectRedis()` decorator - -#### Quick Start - -```bash -npm install @rineex/ioredis -``` - -```typescript -import { Module, Injectable } from '@nestjs/common'; -import { InjectRedis, RedisModule } from '@rineex/ioredis'; -import type Redis from 'ioredis'; - -@Module({ - imports: [ - RedisModule.register({ - type: 'single', - url: process.env.REDIS_URL ?? 'redis://localhost:6379', - }), - ], -}) -export class AppModule {} - -@Injectable() -export class CacheService { - constructor(@InjectRedis() private readonly redis: Redis) {} - - async setValue(key: string, value: string) { - await this.redis.set(key, value); - } -} -``` - -For complete documentation, see the -[@rineex/ioredis README](./packages/ioredis/README.md). - ---- - -### [`@rineex/pg-slonik`](./packages/pg-slonik) – PostgreSQL Adapter using Slonik - -[![npm version](https://img.shields.io/npm/v/@rineex/pg-slonik)](https://www.npmjs.com/package/@rineex/pg-slonik) - -NestJS module wrapper for [Slonik](https://github.com/gajus/slonik), providing -type-safe PostgreSQL database access with dependency injection, automatic retry -logic, and graceful shutdown handling. - -#### Key Features - -- **Type-Safe Database Access** - Full TypeScript support with Slonik's type - system -- **Multiple Connections** - Support for multiple named database connections -- **Automatic Retry Logic** - Built-in connection retry with configurable - attempts and delays -- **SQL Safety** - Built-in SQL injection protection via Slonik's tagged - template literals -- **Transaction Support** - First-class transaction handling - -#### Quick Start - -```bash -npm install @rineex/pg-slonik -``` - -```typescript -import { Module, Injectable } from '@nestjs/common'; -import { InjectPool, SlonikModule } from '@rineex/pg-slonik'; -import type { DatabasePool } from 'slonik'; - -@Module({ - imports: [ - SlonikModule.register({ - connections: [ - { - name: 'DEFAULT', - dsn: process.env.DATABASE_URL!, - }, - ], - }), - ], -}) -export class AppModule {} - -@Injectable() -export class UserService { - constructor(@InjectPool() private readonly pool: DatabasePool) {} - - async findUser(id: string) { - const result = await this.pool.query( - this.pool.sql`SELECT * FROM users WHERE id = ${id}`, - ); - return result.rows[0]; - } -} -``` - -For complete documentation, see the -[@rineex/pg-slonik README](./packages/pg-slonik/README.md). - ---- - -### [`@rineex/auth-core`](./packages/authentication/core) – Authentication Core - -[![npm version](https://img.shields.io/npm/v/@rineex/auth-core)](https://www.npmjs.com/package/@rineex/auth-core) - -Core authentication package providing foundational abstractions for building -authentication systems. Designed following DDD principles with support for -multiple authentication methods. - -#### Key Features - -- **Domain-Driven Design** - Built on `@rineex/ddd` for maintainable - authentication logic -- **Extensible Architecture** - OTP and passwordless method packages; - `AuthMethodPort` plugin model -- **Type-Safe** - Full TypeScript support with strict typing -- **Framework Agnostic** - Core domain logic independent of framework specifics - -See [Architecture.md](./packages/authentication/core/Architecture.md) for what -is implemented vs in-tree. - -For complete documentation, see the -[@rineex/auth-core README](./packages/authentication/core/README.md). - ---- - -### [`@rineex/libs`](./packages/libs) – Utility Libraries - -[![npm version](https://img.shields.io/npm/v/@rineex/libs)](https://www.npmjs.com/package/@rineex/libs) - -Shared utility libraries and helper functions for Rineex core modules. - ---- - -### NestJS Middleware Modules - -Production-ready NestJS middleware modules for common web application needs: - -#### [`@rineex/helmet-mw-module`](./packages/nest/helmet-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/helmet-mw-module)](https://www.npmjs.com/package/@rineex/helmet-mw-module) - -Helmet middleware module for securing HTTP headers in NestJS applications. - -#### [`@rineex/cors-mw-module`](./packages/nest/cors-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/cors-mw-module)](https://www.npmjs.com/package/@rineex/cors-mw-module) - -CORS middleware module for handling cross-origin resource sharing in NestJS -applications. - -#### [`@rineex/cookie-parser-mw-module`](./packages/nest/cookie-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/cookie-parser-mw-module)](https://www.npmjs.com/package/@rineex/cookie-parser-mw-module) - -Cookie parser middleware module for parsing cookies in NestJS applications. - -#### [`@rineex/response-time-mw-module`](./packages/nest/response-time-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/response-time-mw-module)](https://www.npmjs.com/package/@rineex/response-time-mw-module) - -Response time middleware module for measuring and reporting response times in -NestJS applications. - -#### [`@rineex/favicon-ignore-mw-module`](./packages/nest/no-favicon-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/favicon-ignore-mw-module)](https://www.npmjs.com/package/@rineex/favicon-ignore-mw-module) - -Favicon ignore middleware module to prevent favicon requests from cluttering -logs in NestJS applications. - ---- - -### Shared Configuration Packages - -#### [`@rineex/eslint-config`](./packages/eslint-config) - -Shared ESLint configuration for Rineex packages. Provides consistent linting -rules across the monorepo. - -**Exports:** - -- `@rineex/eslint-config/base` - Base ESLint configuration -- `@rineex/eslint-config/next-js` - Next.js specific configuration -- `@rineex/eslint-config/react-internal` - React internal configuration -- `@rineex/eslint-config/db` - Database/SQL linting configuration - -#### [`@rineex/typescript-config`](./packages/typescript-config) - -Shared TypeScript configuration for Rineex packages. Provides consistent -TypeScript compiler settings. - -**Exports:** - -- `@rineex/typescript-config/base` - Base TypeScript configuration -- `@rineex/typescript-config/react-library` - React library configuration - ---- - -## Project Structure - -``` -. -├── packages/ -│ ├── ddd/ # Domain-Driven Design framework -│ │ ├── src/ -│ │ │ ├── domain/ # Core DDD building blocks -│ │ │ ├── application/ # Application service ports -│ │ │ ├── gateway/ # HTTP constants and utilities -│ │ │ └── utils/ # Helper utilities -│ │ ├── README.md # Complete documentation -│ │ └── package.json -│ ├── ioredis/ # Redis adapter for NestJS -│ ├── pg-slonik/ # PostgreSQL adapter using Slonik -│ ├── libs/ # Utility libraries -│ ├── authentication/ # Authentication packages -│ │ ├── core/ # @rineex/auth-core -│ │ └── methods/ # OTP, passwordless method packages -│ ├── nest/ # NestJS middleware modules -│ │ ├── helmet-middleware-module/ -│ │ ├── cors-middleware-module/ -│ │ ├── cookie-middleware-module/ -│ │ ├── response-time-middleware-module/ -│ │ └── no-favicon-middleware-module/ -│ ├── eslint-config/ # Shared ESLint configuration -│ └── typescript-config/ # Shared TypeScript configuration -├── scripts/ # Build and utility scripts -├── turbo.json # Turborepo configuration -├── pnpm-workspace.yaml # PNPM workspace configuration -└── README.md -``` - -## Development - -### Prerequisites - -- **Node.js**: 18.0 or higher (Volta pins Node 24 in this repo) -- **pnpm**: 10.0 or higher (workspace uses pnpm 10.22) -- **TypeScript**: 5.9 or higher - -### Getting Started +## Quick start ```bash -# Install dependencies +corepack enable pnpm install - -# Build all packages pnpm build - -# Run tests -pnpm test - -# Run linter -pnpm lint - -# Check types -pnpm check-types -``` - -### Workspace Scripts - -The monorepo uses [Turborepo](https://turbo.build/) for efficient task execution -across packages. - -```bash -# Run command in all packages -pnpm turbo run build - -# Run command in specific package -pnpm --filter @rineex/ddd build - -# Watch mode for development -pnpm turbo run dev --parallel -``` - -## Publishing - -Packages are published automatically through GitHub Actions when changes are -merged to the main branch. Version management is handled via -[Changesets](https://github.com/changesets/changesets). - -### Creating a Changeset - -```bash -pnpm changeset -``` - -Follow the prompts to document your changes. This will create a changeset file -that will be used to automatically bump versions and generate changelogs. - -## Architecture Principles - -This monorepo follows these key principles: - -1. **Domain-Driven Design** - Clear separation of domain logic from - infrastructure -2. **Type Safety** - Strict TypeScript configuration for compile-time safety -3. **Immutability** - Default-immutable data structures to prevent bugs -4. **Composition** - Small, focused packages that can be used independently -5. **Zero Dependencies** - Minimal external dependencies for reliability -6. **Testing** - Comprehensive test coverage with Vitest - -## Contributing - -We welcome contributions! Please follow these guidelines when contributing: - -### Quick Start for Contributors - -```bash -# Clone and setup -git clone https://github.com/rineex/core.git -cd core -pnpm install - -# Create a feature branch -git checkout -b feature/your-feature - -# Make changes and test pnpm test -pnpm lint - -# Push and open a PR -git push origin feature/your-feature -``` - -### Development Guidelines - -- **Code Style**: Follow the ESLint configuration provided by - `@rineex/eslint-config` -- **Type Safety**: Ensure all code passes TypeScript type checking - (`pnpm check-types`) -- **Testing**: Write tests for new functionality and ensure all tests pass -- **Documentation**: Update relevant README files and add JSDoc comments for - public APIs -- **Changesets**: Create a changeset for any package changes using - `pnpm changeset` - -### Package Generator - -Use the package generator to scaffold new packages following the established -structure: - -```bash -pnpm g:pkg [description] ``` -For more details, see [PACKAGE_GENERATOR.md](./PACKAGE_GENERATOR.md). - -## Documentation Map - -| Package / area | Primary docs | -| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Monorepo | [README.md](./README.md), [PACKAGE_GENERATOR.md](./PACKAGE_GENERATOR.md) | -| `@rineex/ddd` | [packages/ddd/README.md](./packages/ddd/README.md) | -| `@rineex/auth-core` | [packages/authentication/core/README.md](./packages/authentication/core/README.md), [Architecture.md](./packages/authentication/core/Architecture.md) | -| Auth methods | [passwordless README](./packages/authentication/methods/passwordless/README.md), [OTP README](./packages/authentication/methods/otp/README.md) | -| `@rineex/pg-slonik` | [README](./packages/pg-slonik/README.md), [API](./packages/pg-slonik/docs/API.md) | -| `@rineex/ioredis` | [README](./packages/ioredis/README.md) | -| Nest middleware | `packages/nest/*/README.md` | -| Domain sub-docs | [auth errors](./packages/authentication/core/src/domain/errors/README.md), [auth VOs](./packages/authentication/core/src/domain/value-objects/README.md) | - -## License - -This project is licensed under the Apache License 2.0 - see the -[LICENSE](LICENSE) file for details. - -## Support - -- 📖 **Documentation** - See individual package READMEs -- 🐛 **Issues** - Report bugs on - [GitHub Issues](https://github.com/rineex/core/issues) -- 💬 **Discussions** - Ask questions on - [GitHub Discussions](https://github.com/rineex/core/discussions) - -## Related Resources - -- [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://www.domainlanguage.com/ddd/) - by Eric Evans -- [Implementing Domain-Driven Design](https://vaughnvernon.com/books/) by Vaughn - Vernon -- [Turborepo Documentation](https://turbo.build/repo/docs) -- [TypeScript Handbook](https://www.typescriptlang.org/docs/) - ---- - -**Made with ❤️ by the Rineex Team** +The workspace pins pnpm 10.22.0 and specifies Node 24.12.0 through Volta. +`@rineex/pg-slonik` supports Node 18 or later; use the root toolchain while +developing this repository. + +## Package areas + +| Area | Packages | +| ---------------------- | --------------------------------------------------------- | +| Domain building blocks | `@rineex/ddd`, `@rineex/decision-engine` | +| Authentication | `@rineex/auth-core`, OTP and passwordless method packages | +| NestJS integrations | Redis, Slonik/PostgreSQL, and HTTP middleware modules | +| Tooling | shared ESLint and TypeScript configurations | + +## Documentation + +- [Development workflow](docs/development.md) — prerequisites, workspace + commands, testing, releases, and package generation. +- [Architecture](docs/architecture.md) — module boundaries, dependency + direction, and public API rules. +- [Package catalog](docs/packages.md) — package names, dependencies, exports, + and supported status. +- [API reference](docs/api-reference.md) — public export inventory and + package-specific contracts. +- [Decision engine](docs/decision-engine.md) — pipeline behavior, complete + selection example, events, and extension points. +- [Authentication](docs/authentication.md) — core contracts, policy evaluation, + OTP integration, and current passwordless boundary. +- [NestJS integrations](docs/nestjs-integrations.md) — Redis, Slonik, middleware + configuration, and lifecycle behavior. +- [DDD primitives](docs/ddd.md) — value objects, entities, aggregates, events, + results, errors, and mappers. +- [LLM and agent reference](llms.txt) — a single machine-oriented entry point + that links to the authoritative human guides. + +Every package has a focused README beside its source. Package release history +remains in its `CHANGELOG.md`. diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..ea9a172 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,90 @@ +# API reference + +This reference inventories the supported package-root API. Names are grouped by +responsibility so readers can navigate the source without treating internal file +paths as public contracts. + +## `@rineex/ddd` + +| Area | Public API | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Base modeling | `ValueObject`, `PrimitiveValueObject`, `Entity`, `AggregateRoot`, `DomainEvent` | +| IDs and value objects | `AggregateId`, `DomainID`, `Email`, `IPAddress`, `Timestamp`, `Url`, `UserAgent`, `UserAgentProps` | +| Errors and outcomes | `DomainError`, `Result`, `Ok`, `Err`, `UseCaseError`, core error registry/types, internal/invalid-state/invalid-value/timeout errors, entity-validation and invalid-value-object errors | +| Types | deep primitive/immutable, entity brand/ID, mapper, and value-object-like types | +| Services and mapping | application-service and logger ports, clock port, `BaseMapper` | +| Utilities | `deepFreeze`, HTTP status constants | + +`Result` is a discriminated union with `kind: 'ok' | 'err'`. Use `Result.ok`, +`Result.err`, `Result.void`, `Result.match`, `Result.map`, `Result.mapError`, +`Result.flatMap`, `Result.isOk`, `Result.isErr`, and `Result.isResult`. + +## `@rineex/decision-engine` + +| Area | Public API | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Execution | `DecisionEngine`, `DecisionValidator`, `DefaultDecisionValidator` | +| Definition and input | `DecisionDefinition`, `DecisionRequest`, `DecisionPolicy`, `DecisionContext`, `CandidateRefResolver` | +| Pipeline records | `CandidateEvaluation`, `DecisionResult`, `DecisionExecution`, `DecisionEvent` | +| Constraints/features | `Constraint`, `ConstraintResult`, `Feature`, `FeatureValue`, `FeatureObjective` | +| Strategies | `Normalizer`, `MinMaxNormalizer`, `ScoringStrategy`, `WeightedSumScoringStrategy`, `FeatureWeightResolver`, `RankingStrategy`, `DescendingScoreRankingStrategy`, `SelectionStrategy`, `SelectFirstStrategy`, `SelectTopNStrategy`, `SelectAboveThresholdStrategy` | +| Events/publication | `CandidateRejectedEvent`, `DecisionCompletedEvent`, `DecisionEventPublisher`, `DecisionEventHandler`, `InMemoryDecisionEventPublisher` | +| Errors | `DecisionError`, `DecisionExecutionError`, `InvalidDecisionDefinitionError` | + +Use only an implementation of the exported strategy contracts. The engine +validates collaborator outputs, so custom strategies must preserve the +configured candidate/feature semantics described in the +[decision guide](decision-engine.md). + +## `@rineex/auth-core` + +The root groups exports into `domain`, `ports`, and `types`. + +- **Identity domain:** identity and authentication-attempt aggregates; identity + entity; identity, auth-attempt, factor, method, policy, provider, and status + value objects; authentication started/failed/succeeded and identity + created/disabled events. +- **Policy domain:** `AuthPolicyEvaluator`, `AuthPolicyEngine`, + `AuthPolicyContext`, and `AuthPolicyDecision`. +- **Inbound port:** `AuthMethodPort` and `AuthMethodOutcome`. +- **Outbound ports:** identity, authentication-attempt, session, token, and + OAuth-authorization repositories; session, identity, and domain-event + publisher adapters; logging, MFA, and observability ports. +- **Types:** `AuthContext`, `AuthFactor`, `AuthMethod`, `AuthPolicy`, + `IdentityProvider`, `RiskSignal`, and observability event types. + +Only exports re-exported by those three root barrels are supported. In +particular, service classes and OAuth/MFA implementation files are not public +root API. + +## Authentication method packages + +`@rineex/authentication-method-otp` exports `OtpAuthMethod` and +`OtpChannelPort`. Its current constructor type depends on an internal, +non-root-exported `OtpCode`, so do not treat it as a complete constructible +public integration yet. + +`@rineex/authentication-method-passwordless` exports nothing from its root. Its +implementation is intentionally not documented as a stable API. + +## NestJS adapters + +| Package | Public API | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@rineex/ioredis` | `RedisModule`, `RedisModuleAsyncOptions`, single/cluster/module option types, `InjectRedis`, `getRedisConnectionToken`, `getRedisOptionsToken`, `createRedisConnection`, `RedisHealthModule`, `RedisHealthIndicator` | +| `@rineex/pg-slonik` | `SlonikModule`, `InjectPool`, configurable-module exports (`MODULE_OPTIONS_TOKEN`, `OPTIONS_TYPE`, `ASYNC_OPTIONS_TYPE`), `createSlonikToken` | +| Cookie parser module | `CookieParserModule`, `CookieParserMiddleware`, `CookieParseOptions` | +| CORS module | `CorsModule`, `CorsMiddleware`, its configurable-module exports | +| Helmet module | `HelmetModule`, `HelmetMiddleware` | +| Favicon-ignore module | `IgnoreFaviconModule`, `IgnoreFaviconMiddleware` | +| Response-time module | `ResponseTimeModule`, `ResponseTimeMiddleware`, `ResponseTimeOptions` | + +Nest configurable modules expose generated registration APIs. Use their +documented `register`/`registerAsync` calls rather than manually constructing +module providers. + +## Configuration and reserved packages + +`@rineex/eslint-config` publishes the `base`, `next-js`, `react-internal`, and +`db` configuration entry points. `@rineex/typescript-config` supplies +`base.json` and `react-library.json`. `@rineex/libs` currently exports no API. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..bba712c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,68 @@ +# Architecture + +Rineex Core is a library monorepo, not a runnable application. The root package +owns repository-wide tasks. Each library owns source, tests, build +configuration, package metadata, and a single public entry point. + +## Layers and dependency direction + +```text +@rineex/ddd + └─ generic domain modeling, errors, results, mapper support + └─ @rineex/auth-core + └─ OTP and passwordless method packages + +@rineex/decision-engine + └─ standalone deterministic decision pipeline + +Application composition root + └─ NestJS adapters + ├─ @rineex/ioredis + ├─ @rineex/pg-slonik + └─ HTTP middleware modules + └─ third-party infrastructure +``` + +The dependency direction is intentional. Core domain packages should not import +NestJS, a database driver, or a transport. Nest adapters sit at the application +boundary and manage framework life cycles. `@rineex/decision-engine` is +independent of both groups. + +## Public API contract + +The supported API of a package is exactly the set of exports from its +`src/index.ts`. Do not import a package's other source files through a deep +path; those files may move, become private, or disappear without a compatibility +guarantee. The documentation identifies packages with deliberately empty entry +points so consumers do not mistake internal source for a supported API. + +## Package output + +Publishable libraries generally declare these fields: + +```json +{ + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts" +} +``` + +tsup builds these outputs from `src/index.ts`. Configuration packages are +private workspace dependencies and intentionally expose configuration files +instead of runtime APIs. + +## Choosing a package + +- Model domain concepts and use explicit success/failure values with + `@rineex/ddd`. +- Select candidates by constraints and weighted preferences with + `@rineex/decision-engine`. +- Build authentication domain state/contracts with `@rineex/auth-core`; add OTP + through its dedicated method package. +- Wire Redis or PostgreSQL into a Nest application with the matching Nest + adapter. +- Apply common Express middleware globally in Nest with the middleware modules. + +The [package catalog](packages.md) is the complete index; detailed guides +explain each integration boundary. diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..c768a15 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,134 @@ +# Authentication + +The authentication packages separate core domain contracts from method +implementations. `@rineex/auth-core` is the stable public foundation. +Applications provide persistence, session, event-publishing, logging, and +transport adapters through its exported ports. + +## Install + +```bash +pnpm add @rineex/auth-core @rineex/ddd +pnpm add @rineex/authentication-method-otp +``` + +## Core public API + +`@rineex/auth-core` exports identity aggregates/entities/value objects and +lifecycle events; policy contracts and `AuthPolicyEngine`; inbound and outbound +ports; and auth context, factor, method, policy, provider, and risk types. Its +root entry point does **not** export application services, MFA/OAuth +implementation files, repositories, or infrastructure adapters. Treat those +source files as internal. + +The public `AuthMethodPort` is the plugin boundary: + +```ts +type AuthMethodPort = { + readonly method: AuthMethodName; + start(params: { + authAttemptId: AuthAttemptId; + ctx: unknown; + }): AuthMethodOutcome | Promise; + verify(params: { + authAttemptId: AuthAttemptId; + payload: unknown; + }): AuthMethodOutcome | Promise; +}; +``` + +An outcome is `{ ok: true }` or `{ ok: false, violation: UseCaseError }`. An +authentication method should validate the transport-shaped `ctx`/`payload`, +translate it to domain values, and return a violation for expected invalid input +rather than leaking transport concerns into the core domain. + +## Implement persistence and event ports + +The core exports ports for identity, authentication-attempt, session, and +OAuth-authorization persistence, session/token lookup, event publication, +logging, MFA time/ID/repository support, and observability events. Implement +them in the application infrastructure layer. + +```ts +import type { IdentityRepository } from '@rineex/auth-core'; + +class PostgresIdentityRepository implements IdentityRepository { + async get(id: IdentityId): Promise { + // Load persistence data and map it to Identity. + return null; + } + + async save(identity: Identity): Promise { + // Persist identity state and its pulled domain events atomically. + } +} +``` + +Repositories operate on domain types, never on request/response DTOs. Publish +aggregate events through `DomainEventPublisherPort` after the persistence +transaction succeeds. + +## Authentication policy + +`AuthPolicyEvaluator` evaluates immutable facts in an `AuthPolicyContext`. +Return `null` for no opinion, a deny decision for a hard block, or an allow +decision that may require step-up authentication. `AuthPolicyEngine` evaluates +policies in order; the first hard deny wins and any allow decision can set +`requiresStepUp`. + +```ts +import { + AuthPolicyEngine, + AuthPolicyEvaluator, + type AuthPolicyContext, +} from '@rineex/auth-core'; + +class RiskPolicy extends AuthPolicyEvaluator { + evaluate(context: AuthPolicyContext) { + if (context.isBlocked) + return { allowed: false as const, reason: 'IDENTITY_BLOCKED' }; + if ((context.riskScore ?? 0) >= 70) + return { allowed: true as const, requiresStepUp: true }; + return null; + } +} + +const decision = new AuthPolicyEngine([new RiskPolicy()]).evaluate({ + method: AuthMethod.create('otp'), + riskScore: 80, +}); +``` + +Policies are pure decision logic. Risk scoring, device reputation, IP +intelligence, and other external facts must be calculated upstream and supplied +in the context. + +## OTP method status and contract + +`OtpAuthMethod` and `OtpChannelPort` are exported from +`@rineex/authentication-method-otp`. The port sends and verifies an internal +`OtpCode` for an `IdentityId`; `start` reads an identity from `ctx.identityId` +or `ctx.metadata.identityId`, while `verify` requires +`{ identityId: IdentityId, code: string }`. Invalid context, malformed payloads, +invalid code values, and failed channel verification return +`{ ok: false, violation }`. + +> [!WARNING] The exported `OtpAuthMethod` constructor requires a generator +> returning `OtpCode`, but `OtpCode` itself is not exported from the package +> root. A consumer cannot construct that generator solely through the supported +> public API. Avoid deep imports: this package needs a public `OtpCode` export +> or a public generator abstraction before it is a complete supported +> integration surface. + +When that boundary is made public, the channel should own transmission, storage, +retry policy, rate limiting, and provider failures; the method should own only +authentication-method orchestration. + +## Passwordless package status + +`@rineex/authentication-method-passwordless` contains passwordless challenge +aggregates, value objects, services, channel registries, repository/channel/ID +ports, and email/SMS channel implementations. Its current package root imports +only a declaration file and exports no runtime or type API. Do not deep-import +those files from an application; there is no compatibility contract until a +deliberate public entry point is added. diff --git a/docs/ddd.md b/docs/ddd.md new file mode 100644 index 0000000..a1b2abd --- /dev/null +++ b/docs/ddd.md @@ -0,0 +1,159 @@ +# DDD primitives + +`@rineex/ddd` provides framework-independent building blocks for modeling a +domain. It does not prescribe a database, HTTP framework, message bus, or +dependency-injection container. + +## Install + +```bash +pnpm add @rineex/ddd +``` + +## Value objects + +`PrimitiveValueObject` represents one validated primitive. `ValueObject` +represents validated structured data, deep-freezes its props, compares by +concrete class and value, and serializes through `toJSON()`. Both expose +`value`, `equals`, `toJSON`, and `toString`. + +```ts +import { Email, PrimitiveValueObject } from '@rineex/ddd'; + +class OrderNumber extends PrimitiveValueObject { + static from(value: string) { + return new OrderNumber(value); + } + + protected validate(value: string): void { + if (!/^ORD-\d+$/.test(value)) throw new Error('Invalid order number'); + } +} + +const email = Email.fromString('Person@Example.com'); +console.log(email.value); // person@example.com +console.log(OrderNumber.from('ORD-42').equals(OrderNumber.from('ORD-42'))); // true +``` + +Built-in value objects are `AggregateId`, `DomainID`, `Email`, `IPAddress`, +`Timestamp`, `Url`, and `UserAgent`. + +- `DomainID.generate()` produces a UUID v7-backed ID; `fromString()` validates + an existing ID. +- `AggregateId` is the general aggregate identifier. +- `Email.fromString()` lowercases and validates email input. +- `IPAddress.create()` accepts valid IPv4 or IPv6 input. +- `Timestamp` accepts non-negative integer Unix milliseconds and has `toDate()`. +- `Url.create()` validates a URL and exposes `href`. +- `UserAgent` accepts a raw string and parsed metadata; its metadata includes + bot/mobile information. + +Invalid built-in values throw a typed domain validation error. Validate raw +input at the boundary, then pass value objects through the domain instead of raw +strings. + +## Entities and aggregates + +`Entity` compares identity, not properties. Its protected `props` are +deeply immutable. Subclasses implement `validateProps` and `toObject`; use +protected `mutate` for an invariant-checked state transition. + +```ts +import { AggregateId, Entity, type Immutable } from '@rineex/ddd'; + +type CustomerProps = { name: string; active: boolean }; + +class Customer extends Entity { + static create(name: string) { + return new Customer({ + id: AggregateId.generate(), + props: { name, active: true }, + }); + } + + deactivate() { + this.mutate(current => ({ ...current, active: false })); + } + + protected validateProps(props: Immutable) { + if (!props.name.trim()) throw new Error('Customer name is required'); + } + + toObject() { + return { + id: this.id.value, + name: this.props.name, + active: this.props.active, + }; + } +} +``` + +`AggregateRoot` extends `Entity` and records domain events. `recordEvent` +requires the event aggregate ID to equal the root's ID. `domainEvents` returns a +read-only copy; `pullDomainEvents()` returns and clears the pending event +buffer. Persist the aggregate and publish its pulled events through an +application-owned transaction boundary. + +## Domain events + +Extend `DomainEvent` for facts that occurred in the domain. Its constructor +validates ID, event name, schema version, timestamp, and a JSON-safe payload; +payloads are deeply frozen. `toPrimitives()` returns a transport-safe record. + +```ts +import { AggregateId, DomainEvent } from '@rineex/ddd'; + +class CustomerCreated extends DomainEvent {} + +const event = new CustomerCreated({ + id: crypto.randomUUID(), + eventName: 'customer.created', + aggregateId: AggregateId.generate(), + schemaVersion: 1, + occurredAt: Date.now(), + payload: { email: 'person@example.com' }, +}); +``` + +## Results and errors + +Use exceptions for broken domain invariants and `Result` for expected +application outcomes. + +```ts +import { + Result, + type Result as ResultType, + type UseCaseError, +} from '@rineex/ddd'; + +type RegisterResult = ResultType<{ id: string }, UseCaseError>; + +function respond(result: RegisterResult) { + return Result.match(result, { + ok: value => ({ status: 201, body: value }), + err: error => ({ status: 400, body: error.toObject?.() ?? error }), + }); +} +``` + +`Result.ok`, `Result.err`, `Result.void`, `isOk`, `isErr`, `isResult`, `map`, +`mapError`, `flatMap`, and `match` are available. `AsyncResult` is +`Promise>`. + +Extend `DomainError` for a typed, machine-readable error. Its `code` follows +`NAMESPACE.ERROR_NAME`, metadata is immutable and limited to primitives, and +`toObject()` returns code/message/metadata. Use `InferErrorCodes` and an error +registry to derive valid codes. Core errors include internal, invalid-state, +invalid-value, and timeout cases. + +## Application and infrastructure helpers + +The package exports application-service and logger ports, a clock port, HTTP +status constants, deep immutable/entity/mapper types, and `deepFreeze`. + +`BaseMapper` provides `toDomainList`, `toDTOList`, and `toPersistenceList` +around the abstract `toDomain`, `toDomainProps`, `toDTO`, and `toPersistence` +operations. Keep database models and framework DTOs outside domain entities; map +at the infrastructure boundary. diff --git a/docs/decision-engine.md b/docs/decision-engine.md new file mode 100644 index 0000000..306c968 --- /dev/null +++ b/docs/decision-engine.md @@ -0,0 +1,208 @@ +# Decision engine + +`@rineex/decision-engine` is a framework-independent, deterministic, synchronous +decision pipeline. Give it a reusable definition and one runtime request; it +returns an immutable decision result and the events that describe the completed +decision. It never performs I/O, persistence, logging, telemetry, or event +publication. + +## Install + +```bash +pnpm add @rineex/decision-engine +``` + +## Mental model + +A definition is the reusable algorithm. A request contains a specific candidate +set, context, policy, and optional correlation ID. The policy is versioned so +completed decisions can be audited and reproduced. + +```text +definition + request + → validate + → constraints (all candidates) + → features (eligible candidates only) + → normalize + → score + → rank + → select + → immutable result + domain events +``` + +Constraints are hard eligibility rules. A failed constraint does not stop +evaluation of the remaining constraints: the output preserves every failure for +explainability. Features are numeric preferences. Their objective declares +whether larger (`MAXIMIZE`) or smaller (`MINIMIZE`) raw values are better. + +## Complete example + +```ts +import { + DecisionEngine, + DefaultDecisionValidator, + DescendingScoreRankingStrategy, + type DecisionDefinition, + type DecisionPolicy, + FeatureObjective, + InMemoryDecisionEventPublisher, + MinMaxNormalizer, + SelectTopNStrategy, + WeightedSumScoringStrategy, +} from '@rineex/decision-engine'; + +type Supplier = { + readonly id: string; + readonly cost: number; + readonly quality: number; + readonly approved: boolean; +}; + +interface SupplierPolicy extends DecisionPolicy { + readonly values: { + readonly qualityWeight: number; + readonly costWeight: number; + }; +} + +const definition: DecisionDefinition = { + id: 'supplier-selection', + version: 'v1', + candidateRefResolver: { resolve: supplier => supplier.id }, + constraints: [ + { + id: 'approved', + evaluate: supplier => + supplier.approved + ? { constraintId: 'approved', satisfied: true } + : { + constraintId: 'approved', + satisfied: false, + reasonCode: 'NOT_APPROVED', + }, + }, + ], + features: [ + { + key: 'quality', + objective: FeatureObjective.MAXIMIZE, + evaluate: supplier => ({ + key: 'quality', + objective: FeatureObjective.MAXIMIZE, + rawValue: supplier.quality, + }), + }, + { + key: 'cost', + objective: FeatureObjective.MINIMIZE, + evaluate: supplier => ({ + key: 'cost', + objective: FeatureObjective.MINIMIZE, + rawValue: supplier.cost, + }), + }, + ], + normalizer: new MinMaxNormalizer(), + scoringStrategy: new WeightedSumScoringStrategy({ + resolve: (key, policy) => + key === 'quality' + ? policy.values.qualityWeight + : policy.values.costWeight, + }), + rankingStrategy: new DescendingScoreRankingStrategy(), + selectionStrategy: new SelectTopNStrategy(1), +}; + +const engine = new DecisionEngine(new DefaultDecisionValidator()); +const execution = engine.execute(definition, { + candidates: [ + { id: 'low-cost', quality: 60, cost: 10, approved: true }, + { id: 'high-quality', quality: 100, cost: 90, approved: true }, + { id: 'unapproved', quality: 99, cost: 20, approved: false }, + ], + context: undefined, + policy: { + id: 'default', + version: 'v1', + values: { qualityWeight: 0.75, costWeight: 0.25 }, + }, + correlationId: 'supplier-request-42', +}); + +console.log(execution.result.selected); +console.log(execution.events); + +const publisher = new InMemoryDecisionEventPublisher(); +publisher.subscribe('decision.completed', { + handle: async event => console.log(event.payload.selectedCandidateRefs), +}); +await publisher.publish(execution.events); +``` + +## Definition contract + +| Field | Requirement | +| ---------------------- | ---------------------------------------------------------------------------- | +| `id`, `version` | non-blank stable identifiers | +| `candidateRefResolver` | resolves a non-blank, unique reference for each candidate | +| `constraints` | deterministic eligibility rules; results must use the declared constraint ID | +| `features` | deterministic numeric measurements; feature keys must be stable and unique | +| `normalizer` | preserves the configured feature set and produces valid normalized values | +| `scoringStrategy` | returns a finite score | +| `rankingStrategy` | produces a valid ranking over eligible candidates | +| `selectionStrategy` | returns only candidates from the ranked eligible set | + +`DefaultDecisionValidator` checks the definition and request before processing. +The engine then validates runtime outputs from every collaborator. Invalid +candidate references, non-finite raw values or scores, feature-set changes, and +selections outside the ranked set cause an `InvalidDecisionDefinitionError` or +`DecisionExecutionError`. + +## Built-in strategies + +`MinMaxNormalizer` maps feature values to a comparable range and honors their +objective. When every raw value for a feature is equal, it normalizes that +feature to `1` for all eligible candidates. + +`WeightedSumScoringStrategy` asks a `FeatureWeightResolver` for each feature +weight and sums normalized value × weight. Weights must be finite. +`DescendingScoreRankingStrategy` ranks highest scores first and preserves input +order for ties. + +Choose selection by outcome: + +```ts +new SelectFirstStrategy(); +new SelectTopNStrategy(3); // positive safe integer only +new SelectAboveThresholdStrategy(0.8); // finite threshold only +``` + +All built-in selectors require already ranked, eligible evaluations. +`SelectAboveThresholdStrategy` requires finite candidate scores. + +## Result and events + +`execution.result` includes definition and policy IDs/versions, optional +correlation ID, every evaluation, and the selected subset ordered by rank. Each +`CandidateEvaluation` contains constraint results, eligibility, feature values, +optional score/rank, and `selected` status. + +The engine emits, in order: + +1. One `candidate.rejected` event per ineligible candidate, including its + reference and failed constraints. +2. One `decision.completed` event, including candidate, eligible, and rejected + counts plus selected references. + +The result, evaluations, features, events, and execution container are frozen +before return. Treat the output as a decision record, not a mutable working +object. Publish events only after the appropriate application transaction or +persistence boundary succeeds. + +## Extension points + +Implement `Constraint`, `Feature`, `Normalizer`, `ScoringStrategy`, +`RankingStrategy`, or `SelectionStrategy` for custom behavior. Keep +implementations deterministic and side-effect free. Put HTTP calls, database +access, random input, time acquisition, and message publication outside the +engine; pass those facts into the typed request context or policy first. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..d78c82d --- /dev/null +++ b/docs/development.md @@ -0,0 +1,81 @@ +# Development workflow + +This repository is a pnpm workspace orchestrated by Turborepo. The root +`package.json` pins pnpm 10.22.0 and sets Node 24.12.0 through Volta. Use that +toolchain for development; `@rineex/pg-slonik` itself supports Node 18 or later. + +## First checkout + +```bash +corepack enable +pnpm install +pnpm build +pnpm test +``` + +`pnpm build` runs every package build in dependency order. `pnpm test` first +builds required packages, then runs each package test task. A clean test run can +emit expected test-double logs from Redis and Slonik tests; the command result +is authoritative. + +## Workspace commands + +| Command | What it runs | +| --------------------- | -------------------------------------------- | +| `pnpm build` | `turbo run build` for all workspace packages | +| `pnpm test` | `turbo run test`, after required builds | +| `pnpm test:types` | declared `tsd` type tests | +| `pnpm check-types` | TypeScript no-emit checks | +| `pnpm lint` | package lint tasks | +| `pnpm lint:fix` | package lint fixes where implemented | +| `pnpm prettier` | checks formatting without writing | +| `pnpm prettier:write` | writes formatting changes | + +Run an individual package task with a package filter: + +```bash +pnpm --filter @rineex/ddd test +pnpm --filter @rineex/decision-engine check-types +pnpm --filter @rineex/pg-slonik build +``` + +## Test and build layout + +Most libraries use tsup for CJS, ESM, and declaration output, plus Vitest for +tests. The cookie-parser, CORS, favicon-ignore, and response-time middleware +modules declare Jest test commands. Tests may live next to implementation code +(`*.spec.ts`) or in a package-level `tests` directory. + +Do not commit generated `dist` output unless a package explicitly tracks it. The +root pre-commit hook formats and spell-checks staged Markdown, JSON, YAML, HTML, +TypeScript, and TSX files. The pre-push hook runs `pnpm test`. + +## Add or change a package + +The package generator creates a standard library skeleton: + +```bash +pnpm g:pkg my-package "Short package description" +``` + +The name accepts only lowercase letters, numbers, and hyphens. The generator +creates package metadata, TypeScript and tsup configs, Vitest config, ESLint +config, and an empty `src/index.ts`. It does not decide the public API: add +deliberate exports to `src/index.ts`, write tests, and document the package +README before publishing. + +## Publishing + +This repository uses Changesets. + +```bash +pnpm changeset +pnpm changeset:version +pnpm build +pnpm changeset:publish +``` + +Create a changeset for every consumer-visible package change. +`changeset:version` also formats the repository. Published packages expose +artifacts from `dist`; validate the package entry point rather than relying on +internal source paths. diff --git a/docs/nestjs-integrations.md b/docs/nestjs-integrations.md new file mode 100644 index 0000000..dcc88b0 --- /dev/null +++ b/docs/nestjs-integrations.md @@ -0,0 +1,119 @@ +# NestJS integrations + +These packages adapt third-party infrastructure to NestJS dependency injection +and lifecycle hooks. Install a module together with the peer dependencies +declared in its package metadata. The HTTP middleware packages configure all +routes with `forRoutes('*')`. + +## Redis + +```bash +pnpm add @rineex/ioredis ioredis @nestjs/common @nestjs/terminus +``` + +Register a single connection with a URL or ioredis options: + +```ts +import { Module } from '@nestjs/common'; +import { InjectRedis, RedisModule } from '@rineex/ioredis'; +import type Redis from 'ioredis'; + +@Module({ + imports: [ + RedisModule.register( + { type: 'single', url: process.env.REDIS_URL }, + { connection: 'cache' }, + ), + ], +}) +export class AppModule {} + +class CacheService { + constructor(@InjectRedis('cache') private readonly redis: Redis) {} +} +``` + +For a cluster, use `{ type: 'cluster', nodes, options }`. +`getRedisConnectionToken(connection?)` and `getRedisOptionsToken(connection?)` +expose the generated injection-token strings. `RedisModule` is global, tracks +created clients, pings them during application bootstrap, and attempts `quit()` +for every tracked client on shutdown. + +`RedisHealthModule` exports `RedisHealthIndicator`. Its `isHealthy(key)` pings +the configured health client and returns the Terminus `up` or `down` result. The +health client is supplied through the `REDIS_HEALTH_INDICATOR` provider token. + +## PostgreSQL through Slonik + +```bash +pnpm add @rineex/pg-slonik slonik @nestjs/common +``` + +```ts +import { Module } from '@nestjs/common'; +import { InjectPool, SlonikModule } from '@rineex/pg-slonik'; +import type { DatabasePool } from 'slonik'; + +@Module({ + imports: [ + SlonikModule.register({ + connections: [ + { name: 'DEFAULT', dsn: process.env.DATABASE_URL! }, + { name: 'replica', dsn: process.env.REPLICA_DATABASE_URL! }, + ], + }), + ], +}) +export class AppModule {} + +class UserRepository { + constructor( + @InjectPool() private readonly writer: DatabasePool, + @InjectPool('replica') private readonly reader: DatabasePool, + ) {} +} +``` + +Every configured connection has `name`, `dsn`, optional Slonik `options`, and +optional `tags`. `InjectPool(name?)` injects the matching pool; +`createSlonikToken(name?)` produces its token. The module is global by default; +set the configurable-module extra `isGlobal: false` when module-local scope is +required. + +`createSlonikConnection` creates a pool, verifies it through `pool.connect`, and +retries failures. `handleRetry` defaults to nine retries and a three-second +delay; it accepts alternative attempt count, delay, pool name, verbose logging, +and retry predicate. `SlonikModule` ends registered pools during shutdown. The +current public `SlonikModule.register` override accepts only the connection +options, so its configurable-module `isGlobal` extra remains at its default +`true` through that public method. + +## Global HTTP middleware + +Each middleware module supports Nest's generated `register` and `registerAsync` +methods. Import one module once at an appropriate application level. + +```ts +@Module({ + imports: [ + CookieParserModule.register({ secret: process.env.COOKIE_SECRET }), + CorsModule.register({ origin: ['https://app.example.com'] }), + HelmetModule.register(), + IgnoreFaviconModule.register(), + ResponseTimeModule.register({ header: 'X-Response-Time', digits: 1 }), + ], +}) +export class HttpModule {} +``` + +| Package/module | Options and behavior | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CookieParserModule` | Passes `CookieParseOptions` to `cookie-parser` for every request. | +| `CorsModule` | Passes `CorsOptions` to `cors`. Defaults: credentials enabled, module allowed methods/headers, `204` preflight status; supplied options override defaults. | +| `HelmetModule` | Passes `HelmetOptions` to `helmet`. | +| `IgnoreFaviconModule` | No meaningful options; returns `204` if `req.originalUrl` contains `favicon.ico`, otherwise calls next. | +| `ResponseTimeModule` | Passes `digits`, `header`, and `suffix` options to `response-time`. | + +These modules are Express-oriented: their middleware types use Express +request/response objects, so install the listed Express peer dependency and use +them with a compatible Nest HTTP adapter. diff --git a/docs/packages.md b/docs/packages.md new file mode 100644 index 0000000..e6eb444 --- /dev/null +++ b/docs/packages.md @@ -0,0 +1,26 @@ +# Package catalog + +The table lists every workspace package and its consumer-facing status. “Public +exports” refers only to the package root entry point. + +| Package | Purpose | Runtime dependencies | Public exports | Guide | +| -------------------------------------------- | --------------------------------------- | -------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------- | +| `@rineex/ddd` | Domain primitives | `zod`, `uuid`, `rxjs`, utilities | Domain model, `Result`, errors, value objects, mapper | [README](../packages/ddd/README.md) | +| `@rineex/decision-engine` | Candidate selection pipeline | none | Engine, models, strategies, events, in-memory publisher | [README](../packages/decision-engine/README.md) | +| `@rineex/auth-core` | Authentication domain contracts | `@rineex/ddd` | domain aggregates/types, policy engine, ports | [README](../packages/authentication/core/README.md) | +| `@rineex/authentication-method-otp` | OTP auth method | auth core, DDD | `OtpAuthMethod`, `OtpChannelPort` | [README](../packages/authentication/methods/otp/README.md) | +| `@rineex/authentication-method-passwordless` | Passwordless implementation in progress | auth core, DDD | none | [README](../packages/authentication/methods/passwordless/README.md) | +| `@rineex/ioredis` | Nest Redis adapter | Nest, ioredis, Terminus | module, decorator, options, helpers, health indicator | [README](../packages/ioredis/README.md) | +| `@rineex/pg-slonik` | Nest Slonik adapter | Nest, Slonik, RxJS | module, injection decorator, module-definition types, token helper | [README](../packages/pg-slonik/README.md) | +| `@rineex/cookie-parser-mw-module` | Global cookie middleware | Nest, Express, cookie-parser | module, middleware, `CookieParseOptions` | [README](../packages/nest/cookie-middleware-module/README.md) | +| `@rineex/cors-mw-module` | Global CORS middleware | Nest, Express, cors | module, middleware | [README](../packages/nest/cors-middleware-module/README.md) | +| `@rineex/helmet-mw-module` | Global Helmet middleware | Nest, Express, helmet | module, middleware | [README](../packages/nest/helmet-middleware-module/README.md) | +| `@rineex/favicon-ignore-mw-module` | Ignore favicon requests | Nest, Express | module, middleware | [README](../packages/nest/no-favicon-middleware-module/README.md) | +| `@rineex/response-time-mw-module` | Global response-time middleware | Nest, Express, response-time | module, middleware, options type | [README](../packages/nest/response-time-middleware-module/README.md) | +| `@rineex/libs` | Reserved shared-library package | none | none | [README](../packages/libs/README.md) | +| `@rineex/eslint-config` | Private ESLint configs | ESLint plugins | configuration entry points | [README](../packages/eslint-config/README.md) | +| `@rineex/typescript-config` | Private TypeScript configs | none | JSON configuration files | [README](../packages/typescript-config/README.md) | + +The [authentication](authentication.md), [decision engine](decision-engine.md), +[NestJS integrations](nestjs-integrations.md), and [DDD](ddd.md) guides contain +usage examples and behavior details. diff --git a/docs/superpowers/specs/2026-08-08-ddd-primitives-design.md b/docs/superpowers/specs/2026-08-08-ddd-primitives-design.md deleted file mode 100644 index 21cdb2e..0000000 --- a/docs/superpowers/specs/2026-08-08-ddd-primitives-design.md +++ /dev/null @@ -1,55 +0,0 @@ -# DDD Primitive Hardening Design - -## Goal - -Strengthen `packages/ddd` around entity invariants, aggregate boundaries, -domain-event correctness, and explicit mapper-based rehydration. This is an -intentional breaking change. - -## Entity contract - -`Entity` stores identity and state but does not invoke overridable validation -from its base constructor. Concrete types validate after `super(...)` through -their creation/rehydration factory. Validation accepts candidate props so -`mutate` can validate before committing the replacement. A failed mutation -leaves the previous valid state intact. - -Entity metadata and state must not expose mutable internal `Date` instances. -Serialization remains explicit: `toObject()` is the domain-specific object -projection; `toJSON()` is the JSON-safe primitive projection. - -## Aggregate-root contract - -An aggregate root is the only public entry point for changes to its consistency -boundary. Child entities are not exposed for independent persistence. Event -recording is protected and verifies that the event aggregate ID equals the root -ID. Events are returned in insertion order and pulled atomically after -successful application work. - -## Domain-event contract - -Events are immutable facts. Event ID, stable event name, positive schema -version, aggregate ID, occurrence timestamp, and JSON-safe payload are required -at construction. The base class does not depend on Node-specific random-ID -generation. Event payload serialization returns a detached primitive structure. - -## Rehydration - -Mappers translate persistence data into value objects and invoke an explicit -rehydration factory. Rehydration restores state without emitting creation -events. Creation factories may emit creation events only after the new aggregate -is valid. - -## Documentation - -Update the package README with a quick start and links to -`packages/ddd/docs/DDD-GUIDE.md` and `packages/ddd/docs/MIGRATION.md`. The guide -explains where each primitive belongs, when to use it, and when not to use it. -Migration notes list the removed/changed APIs and before/after examples. - -## Testing - -Add tests for constructor validation safety, atomic failed mutations, immutable -metadata and payloads, aggregate/event identity matching, event metadata -validation, event ordering, and mapper rehydration without creation events. Run -package type tests, unit tests, lint, and build after migration. diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..24c9765 --- /dev/null +++ b/llms.txt @@ -0,0 +1,113 @@ +# Rineex Core complete agent reference + +Rineex Core is a pnpm/Turborepo monorepo of TypeScript libraries. This document is self-contained documentation for coding agents and LLMs. Read it before changing the repository. Source code and each package's `src/index.ts` are authoritative if they conflict with this file. + +## Toolchain and workflow + +- Use pnpm 10.22.0. The workspace Volta configuration specifies Node 24.12.0. +- Install with `corepack enable && pnpm install`. +- Run repository checks: `pnpm build`, `pnpm test`, `pnpm test:types`, `pnpm check-types`, `pnpm lint`, and `pnpm prettier`. +- Run a package check with `pnpm --filter