├── tests ├── lib │ ├── xaven-purchase-optimizer.spec.ts │ └── xaven-ai-engine.spec.ts └── core │ ├── xaven-factory.spec.ts │ └── xaven-context.spec.ts ├── packages ├── core │ ├── middleware │ │ ├── index.ts │ │ ├── resolver.ts │ │ └── builder.ts │ ├── index.ts │ ├── tsconfig.json │ ├── interfaces │ │ ├── module-override.ts │ │ ├── module-definition.ts │ │ └── dependency-injector.ts │ ├── package.json │ ├── xaven-context.ts │ └── xaven-factory.ts ├── lib │ ├── index.ts │ ├── tsconfig.json │ ├── package.json │ ├── xaven-ai-engine.ts │ └── xaven-purchase-optimizer.ts └── utils │ ├── tsconfig.json │ └── package.json ├── scripts ├── test.sh ├── run-integration.sh ├── update-samples.sh └── prepare.sh ├── vitest.config.ts ├── package.json ├── tsconfig.json ├── .gitignore ├── LICENSE └── README.md /tests/lib/xaven-purchase-optimizer.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './builder'; -------------------------------------------------------------------------------- /packages/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './xaven-context'; 2 | export * from './xaven-factory'; -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | # Run both unit and integration tests 2 | npm run test 3 | npm run test:integration -------------------------------------------------------------------------------- /packages/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './xaven-ai-engine'; 2 | export * from './xaven-purchase-optimizer'; -------------------------------------------------------------------------------- /scripts/run-integration.sh: -------------------------------------------------------------------------------- 1 | npm run build &>/dev/null 2 | 3 | npm run test:docker:up 4 | 5 | npm run test:integration -------------------------------------------------------------------------------- /scripts/update-samples.sh: -------------------------------------------------------------------------------- 1 | for d in ./sample/*/ ; do (cd "$d" && printf $d\\n && ncu -u && npm i --package-lock-only); done -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /scripts/prepare.sh: -------------------------------------------------------------------------------- 1 | # 1. Build fresh packages and move them to sample and integration directories 2 | npm run build &>/dev/null 3 | 4 | # 2. Start docker containers to perform integration tests 5 | npm run test:docker:up -------------------------------------------------------------------------------- /packages/core/interfaces/module-override.ts: -------------------------------------------------------------------------------- 1 | import { ModuleDefinition } from './module-definition.interface'; 2 | 3 | export interface ModuleOverride { 4 | moduleToReplace: ModuleDefinition; 5 | newModule: ModuleDefinition; 6 | } -------------------------------------------------------------------------------- /packages/core/interfaces/module-definition.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, ForwardReference } from '@/common'; 2 | import { Type } from '@/common/interfaces'; 3 | 4 | export type ModuleDefinition = 5 | | ForwardReference 6 | | Type 7 | | DynamicModule 8 | | Promise; -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | // vitest.config.ts 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, // Use global test functions like `describe`, `it`, and `expect` without importing them. 7 | environment: 'node', // Since we're testing Node-specific code (AsyncLocalStorage) 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xaven-sdk", 3 | "version": "1.0.0", 4 | "description": "", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "scripts": { 9 | "build": "npm run build --workspaces", 10 | "test": "npm run test --workspaces", 11 | "lint": "npm run lint --workspaces" 12 | }, 13 | "keywords": [ 14 | "xaven", 15 | "xaven-sdk" 16 | ], 17 | "author": "Xaven AI Labs", 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xaven-sdk/utils", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": ">= 18" 6 | }, 7 | "scripts": { 8 | "build": "tsc -p ./tsconfig.json", 9 | "dev": "ts-node-dev --respawn --transpileOnly src/index.ts", 10 | "start": "node dist/index.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.7.9", 14 | "body-parser": "^1.20.2", 15 | "express": "^4.21.2", 16 | "node-cache": "^5.1.2", 17 | "p-limit": "^6.2.0", 18 | "winston": "^3.17.0", 19 | "zod": "^3.24.1" 20 | }, 21 | "devDependencies": { 22 | "ts-node-dev": "^2.0.0", 23 | "typescript": "^4.9.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xaven-sdk/lib", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": ">= 18" 6 | }, 7 | "scripts": { 8 | "build": "tsc -p ./tsconfig.json", 9 | "dev": "ts-node-dev --respawn --transpileOnly src/index.ts", 10 | "start": "node dist/index.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.7.9", 14 | "uuid": "11.0.5", 15 | "body-parser": "^1.20.2", 16 | "express": "^4.21.2", 17 | "node-cache": "^5.1.2", 18 | "p-limit": "^6.2.0", 19 | "winston": "^3.17.0", 20 | "zod": "^3.24.1" 21 | }, 22 | "devDependencies": { 23 | "ts-node-dev": "^2.0.0", 24 | "typescript": "^4.9.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xaven-sdk/core", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": ">= 18" 6 | }, 7 | "scripts": { 8 | "build": "tsc -p ./tsconfig.json", 9 | "dev": "ts-node-dev --respawn --transpileOnly src/index.ts", 10 | "test": "vitest", 11 | "start": "node dist/index.js" 12 | }, 13 | "dependencies": { 14 | "axios": "^1.7.9", 15 | "body-parser": "^1.20.2", 16 | "express": "^4.21.2", 17 | "node-cache": "^5.1.2", 18 | "p-limit": "^6.2.0", 19 | "winston": "^3.17.0", 20 | "zod": "^3.24.1" 21 | }, 22 | "devDependencies": { 23 | "supertest": "^7.0.0", 24 | "ts-node-dev": "^2.0.0", 25 | "typescript": "^4.9.5", 26 | "vitest": "^3.0.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": false, 5 | "noUnusedLocals": false, 6 | "removeComments": true, 7 | "strictNullChecks": true, 8 | "strictPropertyInitialization": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noLib": false, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "useUnknownInCatchVariables": false, 14 | "target": "ES2021", 15 | "sourceMap": true, 16 | "allowJs": false, 17 | "outDir": "dist", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@xaven-sdk/core": ["./packages/core"], 21 | "@xaven-sdk/core/*": ["./packages/core/*"] 22 | } 23 | }, 24 | "include": ["packages/**/*", "integration/**/*"], 25 | "exclude": ["node_modules", "**/*.spec.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/*/package-lock.json 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # IDE 7 | /.idea 8 | /.awcache 9 | /.vscode 10 | /.devcontainer 11 | /.classpath 12 | /.project 13 | /.settings 14 | *.code-workspace 15 | 16 | # Vim 17 | [._]*.s[a-v][a-z] 18 | [._]*.sw[a-p] 19 | [._]s[a-rt-v][a-z] 20 | [._]ss[a-gi-z] 21 | [._]sw[a-p] 22 | 23 | # bundle 24 | packages/**/*.d.ts 25 | packages/**/*.js 26 | 27 | # misc 28 | .DS_Store 29 | lerna-debug.log 30 | npm-debug.log 31 | yarn-error.log 32 | /**/npm-debug.log 33 | /packages/**/.npmignore 34 | /packages/**/LICENSE 35 | *.tsbuildinfo 36 | 37 | # example 38 | /quick-start 39 | /example_dist 40 | /example 41 | 42 | # tests 43 | /test 44 | /benchmarks/memory 45 | /coverage 46 | /.nyc_output 47 | /packages/graphql 48 | /benchmarks/memory 49 | build/config\.gypi 50 | 51 | .npmrc 52 | pnpm-lock.yaml 53 | /.history -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017-2025 Kamil Mysliwiec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/core/middleware/resolver.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@common'; 2 | import { Injector } from '../injector/injector'; 3 | import { InstanceWrapper } from '../injector/instance-wrapper'; 4 | import { Module } from '../injector/module'; 5 | import { MiddlewareContainer } from './container'; 6 | 7 | export class MiddlewareResolver { 8 | constructor( 9 | private readonly middlewareContainer: MiddlewareContainer, 10 | private readonly injector: Injector, 11 | ) { } 12 | 13 | public async resolveInstances(moduleRef: Module, moduleName: string) { 14 | const middlewareMap = 15 | this.middlewareContainer.getMiddlewareCollection(moduleName); 16 | const resolveInstance = async (wrapper: InstanceWrapper) => 17 | this.resolveMiddlewareInstance(wrapper, middlewareMap, moduleRef); 18 | await Promise.all([...middlewareMap.values()].map(resolveInstance)); 19 | } 20 | 21 | private async resolveMiddlewareInstance( 22 | wrapper: InstanceWrapper, 23 | middlewareMap: Map, 24 | moduleRef: Module, 25 | ) { 26 | await this.injector.loadMiddleware(wrapper, middlewareMap, moduleRef); 27 | } 28 | } -------------------------------------------------------------------------------- /packages/core/interfaces/dependency-injector.ts: -------------------------------------------------------------------------------- 1 | type Token = string | symbol; 2 | 3 | interface DependencyContainer { 4 | register(token: Token, provider: DependencyProvider): void; 5 | resolve(token: Token): T; 6 | } 7 | 8 | interface DependencyProvider { 9 | get(): T; 10 | } 11 | 12 | class SimpleInjector implements DependencyContainer { 13 | private providers = new Map, DependencyProvider>(); 14 | 15 | register(token: Token, provider: DependencyProvider): void { 16 | if (this.providers.has(token)) { 17 | throw new Error(`Provider for token ${String(token)} is already registered.`); 18 | } 19 | this.providers.set(token, provider); 20 | } 21 | 22 | resolve(token: Token): T { 23 | const provider = this.providers.get(token); 24 | if (!provider) { 25 | throw new Error(`No provider found for token: ${String(token)}`); 26 | } 27 | return provider.get(); 28 | } 29 | } 30 | 31 | class ValueProvider implements DependencyProvider { 32 | constructor(private value: T) { } 33 | get(): T { 34 | return this.value; 35 | } 36 | } 37 | 38 | class SingletonProvider implements DependencyProvider { 39 | private instance: T | null = null; 40 | constructor(private factory: () => T) { } 41 | get(): T { 42 | if (!this.instance) { 43 | this.instance = this.factory(); 44 | } 45 | return this.instance; 46 | } 47 | } 48 | 49 | class FactoryProvider implements DependencyProvider { 50 | constructor(private factory: () => T) { } 51 | get(): T { 52 | return this.factory(); 53 | } 54 | } 55 | 56 | class ScopedProvider implements DependencyProvider { 57 | private instances = new Map(); 58 | constructor(private factory: (scope: string) => T) { } 59 | get(scope: string): T { 60 | if (!this.instances.has(scope)) { 61 | this.instances.set(scope, this.factory(scope)); 62 | } 63 | return this.instances.get(scope)!; 64 | } 65 | } 66 | 67 | export { 68 | Token, 69 | DependencyContainer, 70 | DependencyProvider, 71 | SimpleInjector, 72 | ValueProvider, 73 | SingletonProvider, 74 | FactoryProvider, 75 | ScopedProvider 76 | }; 77 | -------------------------------------------------------------------------------- /packages/core/xaven-context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * xaven-context.ts 3 | * 4 | */ 5 | 6 | import { AsyncLocalStorage } from "async_hooks"; 7 | import { Request, Response, NextFunction } from "express"; 8 | import { randomUUID } from "crypto"; 9 | 10 | /** 11 | * Shape of data we want to store in our context. 12 | * Extend or modify as needed. 13 | */ 14 | export interface XavenContextData { 15 | correlationId: string; 16 | userId?: string; 17 | /** More fields possible, e.g. locale, tenantId, etc. */ 18 | } 19 | 20 | /** 21 | * A specialized AsyncLocalStorage for our Xaven context. 22 | */ 23 | const xavenContextStore = new AsyncLocalStorage(); 24 | 25 | /** 26 | * Express middleware to initialize Xaven context. 27 | * - If the request has a correlation ID header, we use that. 28 | * - Otherwise, we generate one. 29 | * - Optionally, we might also attach a user ID from a JWT or session. 30 | */ 31 | export function xavenContextMiddleware(req: Request, _res: Response, next: NextFunction) { 32 | // Attempt to read an existing correlation ID from headers 33 | const incomingCorrelationId = req.headers["x-correlation-id"]; 34 | 35 | // If it’s not a string, we generate a new UUID 36 | let correlationId: string; 37 | if (typeof incomingCorrelationId === "string" && incomingCorrelationId.trim().length > 0) { 38 | correlationId = incomingCorrelationId.trim(); 39 | } else { 40 | correlationId = randomUUID(); 41 | } 42 | 43 | // Example: read a user ID from the request (placeholder logic) 44 | // In real code, you might parse a JWT or session data here. 45 | const userId = (req as any).userId || undefined; 46 | 47 | // Prepare the context data 48 | const contextData: XavenContextData = { 49 | correlationId, 50 | userId 51 | }; 52 | 53 | // Run the rest of the request inside the AsyncLocalStorage context 54 | xavenContextStore.run(contextData, () => { 55 | next(); 56 | }); 57 | } 58 | 59 | /** 60 | * Retrieve the current context. Returns undefined if called outside an async context. 61 | */ 62 | export function getXavenContext(): XavenContextData | undefined { 63 | return xavenContextStore.getStore(); 64 | } 65 | 66 | /** 67 | * Helper to fetch correlationId from context quickly. 68 | */ 69 | export function getCorrelationId(): string | undefined { 70 | return xavenContextStore.getStore()?.correlationId; 71 | } 72 | 73 | /** 74 | * Helper to fetch userId from context quickly. 75 | */ 76 | export function getUserId(): string | undefined { 77 | return xavenContextStore.getStore()?.userId; 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xaven SDK · ![License](https://img.shields.io/badge/license-MIT-green) 2 | 3 | 4 | **Xaven SDK** is a TypeScript library that streamlines **AI-driven e-commerce** recommendations by **optimizing user purchases** across multiple partner platforms. Through a combination of **machine learning inference**, **concurrency-limited fetching**, and **price-shipping balancing**, developers can embed a optimized shopping experience into their applications. 5 | 6 | --- 7 | 8 | ## Core Features 9 | 10 | 1. **AI Query Understanding** 11 | - Parse user input with advanced ML-based textual analysis to extract product, brand, and budget constraints. 12 | - Automatically refine ambiguous queries into actionable search parameters. 13 | 14 | 2. **Concurrent Data Fetch & Orchestration** 15 | - Perform parallel retrieval of product details from partner APIs, throttled by concurrency guards (e.g., p-limit). 16 | - Resolve potential rate-limit issues via dynamic scheduling, ensuring stable high-load performance. 17 | 18 | 3. **Purchase Optimization Engine** 19 | - Combine real-time price checks, shipping cost evaluation, and historical preference data to rank offers. 20 | - Minimize “total cost of ownership” by factoring in discount codes, membership perks, or seasonal deals. 21 | 22 | 4. **Context-Aware Caching** 23 | - Reduce redundant calls with integrated in-memory caching or pluggable Redis layers. 24 | - Context-based strategy: store correlation IDs, user sessions, or ephemeral queries for multi-step purchases. 25 | 26 | 27 | ## Usage 28 | 29 | ```ts 30 | import { xavenAiEngine, xavenPurchaseOptimizer } from "xaven-sdk"; 31 | 32 | // 1. AI parse user desire 33 | const userQuery = "Need a high-end laptop under 1500"; 34 | const aiParsed = await xavenAiEngine.parse(userQuery); 35 | 36 | // 2. Optimize purchase 37 | const bestOffer = await xavenPurchaseOptimizer.getBestRecommendation(aiParsed); 38 | 39 | console.log("Optimal e-commerce partner:", bestOffer.partnerName); 40 | console.log("Price (including shipping):", bestOffer.totalPrice); 41 | ``` 42 | 43 | ## Advanced Configuration 44 | 45 | | Env Variable | Default | Description | 46 | |--------------------------------|----------|-------------------------------------------------------| 47 | | \`XAVEN_CONCURRENCY_LIMIT\` | \`5\` | Maximum parallel queries to partner APIs. | 48 | | \`XAVEN_CACHE_TTL\` | \`120\` | Caching duration (seconds) for repeated requests. | 49 | | \`XAVEN_AI_MODEL\` | \`qwen2.5\` | Preferred reasoning model to interpret user queries. | 50 | | \`XAVEN_LOG_LEVEL\` | \`info\` | Logging verbosity for debugging aggregator flows. | 51 | 52 | --- 53 | 54 | ## Getting Involved 55 | 56 | We welcome feedback and pull requests! Please adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md). 57 | Feel free to open issues, propose new features, or share your ideas on optimizing the user’s purchasing journey with **Xaven SDK**. 58 | 59 | © 2025 Xaven Labs. Released under the MIT License. 60 | -------------------------------------------------------------------------------- /packages/lib/xaven-ai-engine.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * xavenAiEngine.ts 3 | * 4 | */ 5 | 6 | import axios, { AxiosInstance } from "axios"; 7 | import { v4 as uuidv4 } from "uuid"; 8 | 9 | // Overly complex config 10 | interface XavenAiEngineConfig { 11 | endpointUrl: string; // e.g. https://api.xaven.com/ai-parse 12 | requestTimeoutMs: number; 13 | defaultLanguage: string; 14 | retryCount: number; 15 | additionalHeaders?: Record; 16 | } 17 | 18 | interface XavenAiEngineInternalState { 19 | client: AxiosInstance; 20 | config: XavenAiEngineConfig; 21 | } 22 | 23 | export interface XavenAiParsedResult { 24 | category: string; // e.g. "laptop" 25 | brand?: string; // e.g. "Dell" 26 | maxBudget?: number; // e.g. 1500 27 | shippingUrgency?: string; // e.g. "standard" or "expedited" 28 | // ... 29 | } 30 | 31 | export class XavenAiEngine { 32 | private state: XavenAiEngineInternalState; 33 | 34 | constructor(config?: Partial) { 35 | // Merge defaults 36 | const finalConfig: XavenAiEngineConfig = { 37 | endpointUrl: config?.endpointUrl ?? "https://api.xaven.com/ai-parse", 38 | requestTimeoutMs: config?.requestTimeoutMs ?? 5000, 39 | defaultLanguage: config?.defaultLanguage ?? "en", 40 | retryCount: config?.retryCount ?? 2, 41 | additionalHeaders: config?.additionalHeaders ?? {}, 42 | }; 43 | 44 | // Create an Axios instance with overengineered interceptors 45 | const client = axios.create({ 46 | baseURL: finalConfig.endpointUrl, 47 | timeout: finalConfig.requestTimeoutMs, 48 | headers: { 49 | "X-Requested-With": "XavenAiEngine", 50 | ...finalConfig.additionalHeaders, 51 | }, 52 | }); 53 | 54 | // Interceptor for logging 55 | client.interceptors.request.use((req) => { 56 | console.log("[XavenAiEngine] Request:", req); 57 | return req; 58 | }); 59 | client.interceptors.response.use( 60 | (res) => { 61 | console.log("[XavenAiEngine] Response:", res.status, res.data); 62 | return res; 63 | }, 64 | (error) => { 65 | console.error("[XavenAiEngine] Error:", error.message); 66 | throw error; 67 | } 68 | ); 69 | 70 | this.state = { 71 | config: finalConfig, 72 | client, 73 | }; 74 | } 75 | 76 | /** 77 | * parseUserQuery 78 | * 79 | * Overly complicated method that calls the backend's /parse endpoint, 80 | * possibly reattempting on failure. We also generate a correlation ID for logging. 81 | */ 82 | public async parseUserQuery( 83 | userQuery: string, 84 | preferredLanguage?: string 85 | ): Promise { 86 | const correlationId = uuidv4(); 87 | const language = preferredLanguage || this.state.config.defaultLanguage; 88 | 89 | let attempts = 0; 90 | let lastError: any; 91 | 92 | while (attempts < this.state.config.retryCount) { 93 | try { 94 | const response = await this.state.client.post("/", { 95 | correlationId, 96 | userQuery, 97 | language, 98 | }); 99 | // Expecting the backend to return a structure like: 100 | // { category: ..., brand: ..., maxBudget: ..., shippingUrgency: ... } 101 | return response.data as XavenAiParsedResult; 102 | } catch (err) { 103 | attempts++; 104 | lastError = err; 105 | console.warn( 106 | `[XavenAiEngine] Attempt ${attempts} failed for parseUserQuery: ${err.message}` 107 | ); 108 | } 109 | } 110 | 111 | // If we exhaust retries: 112 | throw new Error( 113 | `[XavenAiEngine] parseUserQuery failed after ${this.state.config.retryCount} attempts. Last error: ${lastError?.message 114 | }` 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/lib/xaven-ai-engine.spec.ts: -------------------------------------------------------------------------------- 1 | // xavenAiEngine.spec.ts 2 | 3 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 4 | import { XavenAiEngine, XavenAiParsedResult } from "../../packages/lib/xaven-ai-engine"; 5 | import axios from "axios"; 6 | 7 | // --- Mock uuid so that all calls to uuidv4 return a fixed value --- 8 | vi.mock("uuid", () => ({ 9 | v4: vi.fn(() => "test-correlation-id"), 10 | })); 11 | 12 | // We'll create a fake Axios client that will be used by our engine. 13 | let fakePost: ReturnType; 14 | let fakeClient: any; 15 | 16 | beforeEach(() => { 17 | fakePost = vi.fn(); 18 | fakeClient = { 19 | post: fakePost, 20 | interceptors: { 21 | request: { use: vi.fn() }, 22 | response: { use: vi.fn() }, 23 | }, 24 | }; 25 | 26 | // Override axios.create so that our engine uses the fake client. 27 | vi.spyOn(axios, "create").mockReturnValue(fakeClient); 28 | }); 29 | 30 | afterEach(() => { 31 | vi.restoreAllMocks(); 32 | }); 33 | 34 | describe("XavenAiEngine", () => { 35 | it("should successfully parse user query on first attempt", async () => { 36 | const engine = new XavenAiEngine(); 37 | const expectedResult: XavenAiParsedResult = { 38 | category: "laptop", 39 | brand: "Dell", 40 | maxBudget: 1500, 41 | shippingUrgency: "standard", 42 | }; 43 | 44 | // Simulate a successful response on first call. 45 | fakePost.mockResolvedValueOnce({ data: expectedResult }); 46 | 47 | const userQuery = "I need a new laptop"; 48 | const result = await engine.parseUserQuery(userQuery); 49 | 50 | expect(result).toEqual(expectedResult); 51 | expect(fakePost).toHaveBeenCalledTimes(1); 52 | 53 | // Verify that the payload sent to the backend is as expected. 54 | // Note: Our engine sends { correlationId, userQuery, language }. 55 | const payload = fakePost.mock.calls[0][1]; // second argument of post() 56 | expect(payload).toEqual({ 57 | correlationId: "test-correlation-id", 58 | userQuery, 59 | language: "en", // default language from config 60 | }); 61 | }); 62 | 63 | it("should use provided preferredLanguage if given", async () => { 64 | const engine = new XavenAiEngine({ defaultLanguage: "en" }); 65 | const expectedResult: XavenAiParsedResult = { 66 | category: "phone", 67 | brand: "Apple", 68 | maxBudget: 1000, 69 | shippingUrgency: "expedited", 70 | }; 71 | 72 | fakePost.mockResolvedValueOnce({ data: expectedResult }); 73 | 74 | const userQuery = "I want an iPhone"; 75 | const preferredLanguage = "es"; 76 | const result = await engine.parseUserQuery(userQuery, preferredLanguage); 77 | 78 | expect(result).toEqual(expectedResult); 79 | expect(fakePost).toHaveBeenCalledTimes(1); 80 | 81 | const payload = fakePost.mock.calls[0][1]; 82 | expect(payload).toEqual({ 83 | correlationId: "test-correlation-id", 84 | userQuery, 85 | language: preferredLanguage, 86 | }); 87 | }); 88 | 89 | it("should retry on failure and succeed on a subsequent attempt", async () => { 90 | // Configure the engine to allow up to 3 attempts. 91 | const engine = new XavenAiEngine({ retryCount: 3 }); 92 | const expectedResult: XavenAiParsedResult = { 93 | category: "tablet", 94 | brand: "Samsung", 95 | maxBudget: 800, 96 | shippingUrgency: "standard", 97 | }; 98 | 99 | // First call fails, second call succeeds. 100 | const error = new Error("Network error"); 101 | fakePost 102 | .mockRejectedValueOnce(error) 103 | .mockResolvedValueOnce({ data: expectedResult }); 104 | 105 | const userQuery = "I need a tablet"; 106 | const result = await engine.parseUserQuery(userQuery); 107 | 108 | expect(result).toEqual(expectedResult); 109 | expect(fakePost).toHaveBeenCalledTimes(2); 110 | }); 111 | 112 | it("should throw an error after exhausting all retry attempts", async () => { 113 | // Set retryCount to 2 for this test. 114 | const retryCount = 2; 115 | const engine = new XavenAiEngine({ retryCount }); 116 | const error = new Error("Service unavailable"); 117 | 118 | // Make all attempts fail. 119 | fakePost.mockRejectedValue(error); 120 | 121 | const userQuery = "I need a smartwatch"; 122 | await expect(engine.parseUserQuery(userQuery)).rejects.toThrow( 123 | `[XavenAiEngine] parseUserQuery failed after ${retryCount} attempts. Last error: ${error.message}` 124 | ); 125 | expect(fakePost).toHaveBeenCalledTimes(retryCount); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /tests/core/xaven-factory.spec.ts: -------------------------------------------------------------------------------- 1 | // xaven-factory.spec.ts 2 | 3 | import request from "supertest"; 4 | import { describe, it, expect, beforeEach, vi } from "vitest"; 5 | import { app } from "../../packages/core/xaven-factory"; 6 | import axios from "axios"; 7 | 8 | // Mock axios so that external HTTP calls are intercepted. 9 | vi.mock("axios"); 10 | 11 | // Reset mocks between tests. 12 | beforeEach(() => { 13 | vi.clearAllMocks(); 14 | }); 15 | 16 | describe("GET /post-by-id", () => { 17 | it("returns post data when a valid postId is provided", async () => { 18 | const postId = "123"; 19 | const numericId = 123; 20 | const postData = { id: numericId, title: "Test Post" }; 21 | 22 | // When getPostById is called, it constructs the URL as `${EXTERNAL_API_URL}/${id}`. 23 | // We simulate a successful API response. 24 | (axios.get as any).mockResolvedValueOnce({ data: postData }); 25 | 26 | const response = await request(app).get(`/post-by-id?postId=${postId}`); 27 | expect(response.status).toBe(200); 28 | expect(response.body.success).toBe(true); 29 | expect(response.body.data).toEqual(postData); 30 | expect(axios.get).toHaveBeenCalledWith(`https://api.xavenai.com/${numericId}`); 31 | }); 32 | 33 | it("returns 400 when an invalid postId is provided", async () => { 34 | const response = await request(app).get("/post-by-id?postId=abc"); 35 | expect(response.status).toBe(400); 36 | expect(response.body.success).toBe(false); 37 | // Our error handler returns "Validation failed" for Zod errors. 38 | expect(response.body.error).toBe("Validation failed"); 39 | expect(response.body.issues).toBeDefined(); 40 | }); 41 | 42 | it("returns 404 when the post is not found", async () => { 43 | const postId = "456"; 44 | const numericId = 456; 45 | // Simulate a response with a null data (post not found) 46 | (axios.get as any).mockResolvedValueOnce({ data: null }); 47 | 48 | const response = await request(app).get(`/post-by-id?postId=${postId}`); 49 | expect(response.status).toBe(404); 50 | expect(response.body.success).toBe(false); 51 | expect(response.body.error).toBe(`Post with ID ${postId} not found`); 52 | }); 53 | }); 54 | 55 | describe("POST /bulk-fetch", () => { 56 | it("returns posts for valid postIds", async () => { 57 | const postIds = [1, 2, 3]; 58 | // Create sample post data for each id. 59 | const postsData = postIds.map((id) => ({ id, title: `Post ${id}` })); 60 | 61 | // Since getPostsInBulk calls getPostById for each id, we need to intercept each axios.get call. 62 | // Here we mock axios.get to return the proper post based on the URL. 63 | (axios.get as any).mockImplementation((url: string) => { 64 | // The URL is expected to be "https://api.xavenai.com/" 65 | const idStr = url.split("/").pop(); 66 | const id = Number(idStr); 67 | const post = postsData.find((p) => p.id === id); 68 | return Promise.resolve({ data: post }); 69 | }); 70 | 71 | const response = await request(app) 72 | .post("/bulk-fetch") 73 | .send({ postIds }); 74 | expect(response.status).toBe(200); 75 | expect(response.body.success).toBe(true); 76 | expect(response.body.count).toBe(postIds.length); 77 | expect(response.body.data).toEqual(postsData); 78 | }); 79 | 80 | it("returns 400 when postIds array is empty", async () => { 81 | const response = await request(app) 82 | .post("/bulk-fetch") 83 | .send({ postIds: [] }); 84 | expect(response.status).toBe(400); 85 | expect(response.body.success).toBe(false); 86 | expect(response.body.error).toBe("Validation failed"); 87 | expect(response.body.issues).toBeDefined(); 88 | }); 89 | }); 90 | 91 | describe("Caching behavior", () => { 92 | it("should cache the response for GET /post-by-id", async () => { 93 | const postId = "789"; 94 | const numericId = 789; 95 | const postData = { id: numericId, title: "Cached Post" }; 96 | 97 | // For the first call, simulate a cache miss. 98 | (axios.get as any).mockResolvedValueOnce({ data: postData }); 99 | 100 | // First request: should call axios.get. 101 | const res1 = await request(app).get(`/post-by-id?postId=${postId}`); 102 | expect(res1.status).toBe(200); 103 | expect(res1.body.data).toEqual(postData); 104 | 105 | // Second request: should hit the cache so axios.get is not called again. 106 | const res2 = await request(app).get(`/post-by-id?postId=${postId}`); 107 | expect(res2.status).toBe(200); 108 | expect(res2.body.data).toEqual(postData); 109 | 110 | // Verify that axios.get was called only once. 111 | expect(axios.get).toHaveBeenCalledTimes(1); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /packages/core/middleware/builder.ts: -------------------------------------------------------------------------------- 1 | import { stripEndSlash } from '@nestjs/common/utils/shared.utils'; 2 | import { iterate } from 'iterare'; 3 | import { RouteInfoPathExtractor } from './route-info-path-extractor'; 4 | import { RoutesMapper } from './routes-mapper'; 5 | import { filterMiddleware } from './utils'; 6 | 7 | export class MiddlewareBuilder implements MiddlewareConsumer { 8 | private readonly middlewareCollection = new Set(); 9 | 10 | constructor( 11 | private readonly routesMapper: RoutesMapper, 12 | private readonly httpAdapter: HttpServer, 13 | private readonly routeInfoPathExtractor: RouteInfoPathExtractor, 14 | ) { } 15 | 16 | public apply( 17 | ...middleware: Array | Function | Array | Function>> 18 | ): MiddlewareConfigProxy { 19 | return new MiddlewareBuilder.ConfigProxy( 20 | this, 21 | middleware.flat(), 22 | this.routeInfoPathExtractor, 23 | ); 24 | } 25 | 26 | public build(): MiddlewareConfiguration[] { 27 | return [...this.middlewareCollection]; 28 | } 29 | 30 | public getHttpAdapter(): HttpServer { 31 | return this.httpAdapter; 32 | } 33 | 34 | private static readonly ConfigProxy = class implements MiddlewareConfigProxy { 35 | private excludedRoutes: RouteInfo[] = []; 36 | 37 | constructor( 38 | private readonly builder: MiddlewareBuilder, 39 | private readonly middleware: Array | Function>, 40 | private routeInfoPathExtractor: RouteInfoPathExtractor, 41 | ) { } 42 | 43 | public getExcludedRoutes(): RouteInfo[] { 44 | return this.excludedRoutes; 45 | } 46 | 47 | public exclude( 48 | ...routes: Array 49 | ): MiddlewareConfigProxy { 50 | this.excludedRoutes = [ 51 | ...this.excludedRoutes, 52 | ...this.getRoutesFlatList(routes).reduce((excludedRoutes, route) => { 53 | for (const routePath of this.routeInfoPathExtractor.extractPathFrom( 54 | route, 55 | )) { 56 | excludedRoutes.push({ 57 | ...route, 58 | path: routePath, 59 | }); 60 | } 61 | 62 | return excludedRoutes; 63 | }, [] as RouteInfo[]), 64 | ]; 65 | 66 | return this; 67 | } 68 | 69 | public forRoutes( 70 | ...routes: Array | RouteInfo> 71 | ): MiddlewareConsumer { 72 | const { middlewareCollection } = this.builder; 73 | 74 | const flattedRoutes = this.getRoutesFlatList(routes); 75 | const forRoutes = this.removeOverlappedRoutes(flattedRoutes); 76 | const configuration = { 77 | middleware: filterMiddleware( 78 | this.middleware, 79 | this.excludedRoutes, 80 | this.builder.getHttpAdapter(), 81 | ), 82 | forRoutes, 83 | }; 84 | middlewareCollection.add(configuration); 85 | return this.builder; 86 | } 87 | 88 | private getRoutesFlatList( 89 | routes: Array | RouteInfo>, 90 | ): RouteInfo[] { 91 | const { routesMapper } = this.builder; 92 | 93 | return iterate(routes) 94 | .map(route => routesMapper.mapRouteToRouteInfo(route)) 95 | .flatten() 96 | .toArray(); 97 | } 98 | 99 | private removeOverlappedRoutes(routes: RouteInfo[]) { 100 | const regexMatchParams = /(:[^/]*)/g; 101 | const wildcard = '([^/]*)'; 102 | const routesWithRegex = routes 103 | .filter(route => route.path.includes(':')) 104 | .map(route => ({ 105 | method: route.method, 106 | path: route.path, 107 | regex: new RegExp( 108 | '^(' + route.path.replace(regexMatchParams, wildcard) + ')$', 109 | 'g', 110 | ), 111 | })); 112 | 113 | return routes.filter(route => { 114 | const isOverlapped = (item: { regex: RegExp } & RouteInfo): boolean => { 115 | if (route.method !== item.method) { 116 | return false; 117 | } 118 | const normalizedRoutePath = stripEndSlash(route.path); 119 | return ( 120 | normalizedRoutePath !== item.path && 121 | item.regex.test(normalizedRoutePath) 122 | ); 123 | }; 124 | const routeMatch = routesWithRegex.find(isOverlapped); 125 | return routeMatch === undefined; 126 | }); 127 | } 128 | }; 129 | } -------------------------------------------------------------------------------- /packages/lib/xaven-purchase-optimizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * xavenPurchaseOptimizer.ts 3 | * 4 | */ 5 | 6 | import axios, { AxiosInstance } from "axios"; 7 | import { v4 as uuidv4 } from "uuid"; 8 | import { XavenAiParsedResult } from "./xaven-ai-engine"; 9 | 10 | // Overly complex config 11 | interface XavenPurchaseOptimizerConfig { 12 | endpointUrl: string; // e.g. https://api.xaven.com/purchase-optimize 13 | requestTimeoutMs: number; 14 | concurrencyOverride?: number; // If we want to force concurrency from the client side 15 | enableCacheInvalidation?: boolean; 16 | retryCount: number; 17 | additionalHeaders?: Record; 18 | } 19 | 20 | interface XavenPurchaseOptimizerInternalState { 21 | client: AxiosInstance; 22 | config: XavenPurchaseOptimizerConfig; 23 | } 24 | 25 | // The shape we expect from the backend aggregator 26 | export interface XavenOptimizedOffer { 27 | partnerName: string; 28 | productName: string; 29 | price: number; 30 | shippingCost: number; 31 | shippingDays: number; 32 | brand?: string; 33 | totalCost: number; // e.g. price + shipping 34 | link: string; 35 | } 36 | 37 | export interface XavenOptimizationResult { 38 | bestOffer?: XavenOptimizedOffer; 39 | offers: XavenOptimizedOffer[]; 40 | } 41 | 42 | export class XavenPurchaseOptimizer { 43 | private state: XavenPurchaseOptimizerInternalState; 44 | 45 | constructor(config?: Partial) { 46 | const finalConfig: XavenPurchaseOptimizerConfig = { 47 | endpointUrl: 48 | config?.endpointUrl ?? "https://api.xaven.com/purchase-optimize", 49 | requestTimeoutMs: config?.requestTimeoutMs ?? 7000, 50 | concurrencyOverride: config?.concurrencyOverride ?? undefined, 51 | enableCacheInvalidation: config?.enableCacheInvalidation ?? false, 52 | retryCount: config?.retryCount ?? 2, 53 | additionalHeaders: config?.additionalHeaders ?? {}, 54 | }; 55 | 56 | const client = axios.create({ 57 | baseURL: finalConfig.endpointUrl, 58 | timeout: finalConfig.requestTimeoutMs, 59 | headers: { 60 | "X-Requested-With": "XavenPurchaseOptimizer", 61 | ...finalConfig.additionalHeaders, 62 | }, 63 | }); 64 | 65 | // Overengineer interceptors for demonstration 66 | client.interceptors.request.use((req) => { 67 | console.log("[XavenPurchaseOptimizer] Request:", req); 68 | return req; 69 | }); 70 | client.interceptors.response.use( 71 | (res) => { 72 | console.log("[XavenPurchaseOptimizer] Response:", res.status, res.data); 73 | return res; 74 | }, 75 | (error) => { 76 | console.error("[XavenPurchaseOptimizer] Error:", error.message); 77 | throw error; 78 | } 79 | ); 80 | 81 | this.state = { 82 | config: finalConfig, 83 | client, 84 | }; 85 | } 86 | 87 | /** 88 | * optimizePurchase 89 | * 90 | * Calls to optimize the user’s purchase, 91 | * passing in the AI parse result plus optional overrides. 92 | */ 93 | public async optimizePurchase( 94 | aiParsed: XavenAiParsedResult, 95 | userId?: string 96 | ): Promise { 97 | const correlationId = uuidv4(); 98 | let attempts = 0; 99 | let lastError: any; 100 | 101 | while (attempts < this.state.config.retryCount) { 102 | try { 103 | const response = await this.state.client.post("/", { 104 | correlationId, 105 | userId, 106 | payload: { 107 | aiParsed, // the structured info from XavenAiEngine 108 | concurrency: this.state.config.concurrencyOverride, 109 | cacheInvalidation: this.state.config.enableCacheInvalidation, 110 | }, 111 | }); 112 | 113 | // Expecting the backend to return something like: 114 | // { 115 | // bestOffer: { partnerName, productName, ... }, 116 | // offers: [...] 117 | // } 118 | return response.data as XavenOptimizationResult; 119 | } catch (err) { 120 | attempts++; 121 | lastError = err; 122 | console.warn( 123 | `[XavenPurchaseOptimizer] Attempt ${attempts} failed for optimizePurchase: ${err.message}` 124 | ); 125 | } 126 | } 127 | 128 | throw new Error( 129 | `[XavenPurchaseOptimizer] optimizePurchase failed after ${this.state.config.retryCount} attempts. Last error: ${lastError?.message 130 | }` 131 | ); 132 | } 133 | 134 | /** 135 | * forceCacheInvalidation 136 | */ 137 | public async forceCacheInvalidation(reason: string) { 138 | if (!this.state.config.enableCacheInvalidation) { 139 | console.log( 140 | "[XavenPurchaseOptimizer] Cache invalidation is disabled in config." 141 | ); 142 | return; 143 | } 144 | 145 | try { 146 | const response = await this.state.client.delete("/cache", { 147 | data: { reason }, 148 | }); 149 | console.log("[XavenPurchaseOptimizer] Cache invalidation response:", response.data); 150 | } catch (err) { 151 | console.error("[XavenPurchaseOptimizer] Could not invalidate cache:", err); 152 | // maybe re-throw or handle 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /packages/core/xaven-factory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * xaven-factory.ts 3 | * 4 | */ 5 | 6 | import express, { Request, Response, NextFunction } from "express"; 7 | import axios from "axios"; 8 | import NodeCache from "node-cache"; 9 | import { z } from "zod"; 10 | import pLimit from "p-limit"; 11 | import winston from "winston"; 12 | 13 | // -------------------- 1) CONFIG & SETUP -------------------- 14 | 15 | // Winston logger setup 16 | const logger = winston.createLogger({ 17 | level: "info", 18 | format: winston.format.json(), 19 | transports: [ 20 | new winston.transports.Console({ 21 | format: winston.format.simple() 22 | }) 23 | ] 24 | }); 25 | 26 | // Simple in-memory cache (time-to-live: 10 seconds, check every 20) 27 | const cache = new NodeCache({ stdTTL: 10, checkperiod: 20 }); 28 | 29 | // Concurrency limiter allowing up to 3 parallel external requests 30 | const limit = pLimit(3); 31 | 32 | // Express initialization 33 | const app = express(); 34 | app.use(express.json()); 35 | 36 | const EXTERNAL_API_URL = "https://api.xavenai.com/"; 37 | 38 | // -------------------- 2) CUSTOM ERROR CLASS -------------------- 39 | 40 | /** Custom error for domain-level or business-logic failures. */ 41 | class XavenError extends Error { 42 | public statusCode: number; 43 | public details: any; 44 | 45 | constructor(message: string, statusCode = 400, details?: any) { 46 | super(message); 47 | this.name = "XavenError"; 48 | this.statusCode = statusCode; 49 | this.details = details; 50 | } 51 | } 52 | 53 | // -------------------- 3) REQUEST VALIDATION SCHEMAS -------------------- 54 | 55 | // Zod schema for GET /post-by-id 56 | const postByIdQuerySchema = z.object({ 57 | postId: z.string().regex(/^\d+$/, "postId must be a numeric string") 58 | }); 59 | 60 | // Zod schema for POST /bulk-fetch 61 | const bulkFetchSchema = z.object({ 62 | postIds: z.array(z.number()).nonempty("Must provide at least one postId.") 63 | }); 64 | 65 | // -------------------- 4) UTILITY & SERVICE FUNCTIONS -------------------- 66 | 67 | /** 68 | * A generic typed fetch function, respecting concurrency limits and caching. 69 | * @param url The URL to fetch. 70 | * @returns Parsed JSON response. 71 | */ 72 | async function fetchWithCache(url: string): Promise { 73 | // 1. Check in cache 74 | const cached = cache.get(url); 75 | if (cached) { 76 | logger.info(`Cache HIT for ${url}`); 77 | return cached; 78 | } 79 | 80 | // 2. Otherwise, do a concurrency-limited request 81 | return limit(async () => { 82 | logger.info(`Cache MISS, fetching ${url}`); 83 | const response = await axios.get(url); 84 | // Cache the response data 85 | cache.set(url, response.data); 86 | return response.data; 87 | }); 88 | } 89 | 90 | /** 91 | * Fetches one post by ID from the external API. 92 | */ 93 | async function getPostById(id: number) { 94 | const url = `${EXTERNAL_API_URL}/${id}`; 95 | const data = await fetchWithCache(url); 96 | // In real production code, define a Post type: e.g., `interface Post { userId: number; id: number; ... }` 97 | return data; 98 | } 99 | 100 | /** 101 | * Fetches multiple posts at once (in parallel) using concurrency limit. 102 | */ 103 | async function getPostsInBulk(ids: number[]): Promise { 104 | const promises = ids.map((id) => getPostById(id)); 105 | return Promise.all(promises); 106 | } 107 | 108 | // -------------------- 5) ROUTES -------------------- 109 | 110 | // GET /post-by-id?postId=123 111 | app.get("/post-by-id", async (req: Request, res: Response, next: NextFunction) => { 112 | try { 113 | // Validate query 114 | const { postId } = postByIdQuerySchema.parse(req.query); 115 | const numericId = parseInt(postId, 10); 116 | 117 | // Business logic: fetch external data 118 | const post = await getPostById(numericId); 119 | if (!post) { 120 | throw new XavenError(`Post with ID ${postId} not found`, 404); 121 | } 122 | 123 | return res.json({ success: true, data: post }); 124 | } catch (error) { 125 | next(error); 126 | } 127 | }); 128 | 129 | // POST /bulk-fetch 130 | // { "postIds": [1, 2, 3] } 131 | app.post("/bulk-fetch", async (req: Request, res: Response, next: NextFunction) => { 132 | try { 133 | // Validate request body 134 | const { postIds } = bulkFetchSchema.parse(req.body); 135 | 136 | // In production, you might do further checks here (permissions, rate-limits, etc.) 137 | const posts = await getPostsInBulk(postIds); 138 | 139 | return res.json({ success: true, count: posts.length, data: posts }); 140 | } catch (error) { 141 | next(error); 142 | } 143 | }); 144 | 145 | // -------------------- 6) ERROR HANDLING MIDDLEWARE -------------------- 146 | 147 | app.use((error: any, _req: Request, res: Response, _next: NextFunction) => { 148 | if (error instanceof XavenError) { 149 | logger.error(`XavenError: ${error.message}`, { details: error.details }); 150 | return res.status(error.statusCode).json({ 151 | success: false, 152 | error: error.message, 153 | details: error.details 154 | }); 155 | } 156 | 157 | if (error?.name === "ZodError") { 158 | // Validation error from zod 159 | logger.error(`ValidationError: ${error.message}`, { issues: error.issues }); 160 | return res.status(400).json({ 161 | success: false, 162 | error: "Validation failed", 163 | issues: error.issues 164 | }); 165 | } 166 | 167 | logger.error(`UnhandledError: ${error?.message || "Unknown error"}`, { error }); 168 | return res.status(500).json({ 169 | success: false, 170 | error: "Internal Server Error" 171 | }); 172 | }); 173 | 174 | // -------------------- 7) SERVER STARTUP -------------------- 175 | 176 | const PORT = process.env.PORT || 3000; 177 | app.listen(PORT, () => { 178 | logger.info(`Xaven Factory service started on port ${PORT}`); 179 | }); 180 | 181 | export { app } -------------------------------------------------------------------------------- /tests/core/xaven-context.spec.ts: -------------------------------------------------------------------------------- 1 | // xaven-context.spec.ts 2 | 3 | import type { Request, Response, NextFunction } from "express"; 4 | import { 5 | xavenContextMiddleware, 6 | getXavenContext, 7 | getCorrelationId, 8 | getUserId, 9 | XavenContextData, 10 | } from "../../packages/core"; 11 | import { describe, it, expect } from "vitest"; 12 | 13 | describe("xavenContextMiddleware", () => { 14 | it("should use the provided correlation id from the header", () => { 15 | const testCorrelationId = "test-correlation-id"; 16 | const req = { 17 | headers: { 18 | "x-correlation-id": testCorrelationId, 19 | }, 20 | } as Partial as Request; 21 | const res = {} as Response; 22 | 23 | const next: NextFunction = () => { 24 | const context = getXavenContext(); 25 | expect(context).toBeDefined(); 26 | expect(context?.correlationId).toBe(testCorrelationId); 27 | }; 28 | 29 | xavenContextMiddleware(req, res, next); 30 | }); 31 | 32 | it("should trim the correlation id header and use it if non-empty", () => { 33 | const trimmedCorrelationId = "trimmed-correlation-id"; 34 | const req = { 35 | headers: { 36 | "x-correlation-id": ` ${trimmedCorrelationId} `, 37 | }, 38 | } as Partial as Request; 39 | const res = {} as Response; 40 | 41 | const next: NextFunction = () => { 42 | const context = getXavenContext(); 43 | expect(context).toBeDefined(); 44 | // The middleware trims the header value 45 | expect(context?.correlationId).toBe(trimmedCorrelationId); 46 | }; 47 | 48 | xavenContextMiddleware(req, res, next); 49 | }); 50 | 51 | it("should generate a new correlation id when the header is missing", () => { 52 | const req = { 53 | headers: {}, // No correlation id header provided 54 | } as Partial as Request; 55 | const res = {} as Response; 56 | 57 | let capturedContext: XavenContextData | undefined; 58 | const next: NextFunction = () => { 59 | capturedContext = getXavenContext(); 60 | }; 61 | 62 | xavenContextMiddleware(req, res, next); 63 | 64 | expect(capturedContext).toBeDefined(); 65 | expect(capturedContext?.correlationId).toBeDefined(); 66 | // Verify that the generated ID matches a UUID pattern. 67 | const uuidRegex = 68 | /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 69 | expect(capturedContext?.correlationId).toMatch(uuidRegex); 70 | }); 71 | 72 | it("should generate a new correlation id when the header is empty or whitespace", () => { 73 | const req = { 74 | headers: { 75 | "x-correlation-id": " ", // Empty/whitespace value 76 | }, 77 | } as Partial as Request; 78 | const res = {} as Response; 79 | 80 | let capturedContext: XavenContextData | undefined; 81 | const next: NextFunction = () => { 82 | capturedContext = getXavenContext(); 83 | }; 84 | 85 | xavenContextMiddleware(req, res, next); 86 | 87 | expect(capturedContext).toBeDefined(); 88 | // Since the header was not valid, a new UUID should be generated. 89 | const uuidRegex = 90 | /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 91 | expect(capturedContext?.correlationId).toMatch(uuidRegex); 92 | }); 93 | 94 | it("should set the userId from the request if provided", () => { 95 | const testCorrelationId = "user-test-correlation-id"; 96 | const testUserId = "user-123"; 97 | const req = { 98 | headers: { 99 | "x-correlation-id": testCorrelationId, 100 | }, 101 | // Adding userId to the request (this property is not standard to Express) 102 | userId: testUserId, 103 | } as any as Request; 104 | const res = {} as Response; 105 | 106 | let capturedContext: XavenContextData | undefined; 107 | const next: NextFunction = () => { 108 | capturedContext = getXavenContext(); 109 | }; 110 | 111 | xavenContextMiddleware(req, res, next); 112 | 113 | expect(capturedContext).toBeDefined(); 114 | expect(capturedContext?.correlationId).toBe(testCorrelationId); 115 | expect(capturedContext?.userId).toBe(testUserId); 116 | }); 117 | 118 | it("should allow helper functions to access the context", () => { 119 | const testCorrelationId = "helper-test-correlation-id"; 120 | const testUserId = "helper-user-456"; 121 | const req = { 122 | headers: { 123 | "x-correlation-id": testCorrelationId, 124 | }, 125 | userId: testUserId, 126 | } as any as Request; 127 | const res = {} as Response; 128 | 129 | let capturedCorrelationId: string | undefined; 130 | let capturedUserId: string | undefined; 131 | 132 | const next: NextFunction = () => { 133 | // Use helper functions inside the async context 134 | capturedCorrelationId = getCorrelationId(); 135 | capturedUserId = getUserId(); 136 | }; 137 | 138 | xavenContextMiddleware(req, res, next); 139 | 140 | expect(capturedCorrelationId).toBe(testCorrelationId); 141 | expect(capturedUserId).toBe(testUserId); 142 | }); 143 | 144 | it("should propagate context into asynchronous operations", (done) => { 145 | const testCorrelationId = "async-test-correlation-id"; 146 | const req = { 147 | headers: { 148 | "x-correlation-id": testCorrelationId, 149 | }, 150 | } as Partial as Request; 151 | const res = {} as Response; 152 | 153 | const next: NextFunction = () => { 154 | // Simulate an asynchronous operation 155 | setTimeout(() => { 156 | const context = getXavenContext(); 157 | expect(context).toBeDefined(); 158 | expect(context?.correlationId).toBe(testCorrelationId); 159 | // done(); 160 | }, 10); 161 | }; 162 | 163 | xavenContextMiddleware(req, res, next); 164 | }); 165 | }); 166 | --------------------------------------------------------------------------------