diff --git a/.gitignore b/.gitignore index b62507c..11cf3d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ network.txt node_modules/ dist/ -system.txt -.DS_Store \ No newline at end of file +.DS_Store +coverage/ \ No newline at end of file diff --git a/.lintstagedrc b/.lintstagedrc index dae4adf..970fbfb 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,5 +1,5 @@ { - "*.{js,ts}": [ + "src/**/*.{js,ts}": [ "prettier --write", "eslint --fix", "npm run build" diff --git a/coverage/base.css b/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/favicon.png b/coverage/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/favicon.png differ diff --git a/coverage/framework/DebrosFramework.ts.html b/coverage/framework/DebrosFramework.ts.html new file mode 100644 index 0000000..26507d0 --- /dev/null +++ b/coverage/framework/DebrosFramework.ts.html @@ -0,0 +1,2386 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * DebrosFramework - Main Framework Class + * + * This is the primary entry point for the DebrosFramework, providing a unified + * API that integrates all framework components: + * - Model system with decorators and validation + * - Database management and sharding + * - Query system with optimization + * - Relationship management with lazy/eager loading + * - Automatic pinning and PubSub features + * - Migration system for schema evolution + * - Configuration and lifecycle management + */ + +import { BaseModel } from './models/BaseModel'; +import { ModelRegistry } from './core/ModelRegistry'; +import { DatabaseManager } from './core/DatabaseManager'; +import { ShardManager } from './sharding/ShardManager'; +import { ConfigManager } from './core/ConfigManager'; +import { FrameworkOrbitDBService, FrameworkIPFSService } from './services/OrbitDBService'; +import { QueryCache } from './query/QueryCache'; +import { RelationshipManager } from './relationships/RelationshipManager'; +import { PinningManager } from './pinning/PinningManager'; +import { PubSubManager } from './pubsub/PubSubManager'; +import { MigrationManager } from './migrations/MigrationManager'; +import { FrameworkConfig } from './types/framework'; + +export interface DebrosFrameworkConfig extends FrameworkConfig { + // Environment settings + environment?: 'development' | 'production' | 'test'; + + // Service configurations + orbitdb?: { + directory?: string; + options?: any; + }; + + ipfs?: { + config?: any; + options?: any; + }; + + // Feature toggles + features?: { + autoMigration?: boolean; + automaticPinning?: boolean; + pubsub?: boolean; + queryCache?: boolean; + relationshipCache?: boolean; + }; + + // Performance settings + performance?: { + queryTimeout?: number; + migrationTimeout?: number; + maxConcurrentOperations?: number; + batchSize?: number; + }; + + // Monitoring and logging + monitoring?: { + enableMetrics?: boolean; + logLevel?: 'error' | 'warn' | 'info' | 'debug'; + metricsInterval?: number; + }; +} + +export interface FrameworkMetrics { + uptime: number; + totalModels: number; + totalDatabases: number; + totalShards: number; + queriesExecuted: number; + migrationsRun: number; + cacheHitRate: number; + averageQueryTime: number; + memoryUsage: { + queryCache: number; + relationshipCache: number; + total: number; + }; + performance: { + slowQueries: number; + failedOperations: number; + averageResponseTime: number; + }; +} + +export interface FrameworkStatus { + initialized: boolean; + healthy: boolean; + version: string; + environment: string; + services: { + orbitdb: 'connected' | 'disconnected' | 'error'; + ipfs: 'connected' | 'disconnected' | 'error'; + pinning: 'active' | 'inactive' | 'error'; + pubsub: 'active' | 'inactive' | 'error'; + }; + lastHealthCheck: number; +} + +export class DebrosFramework { + private config: DebrosFrameworkConfig; + private configManager: ConfigManager; + + // Core services + private orbitDBService: FrameworkOrbitDBService | null = null; + private ipfsService: FrameworkIPFSService | null = null; + + // Framework components + private databaseManager: DatabaseManager | null = null; + private shardManager: ShardManager | null = null; + private queryCache: QueryCache | null = null; + private relationshipManager: RelationshipManager | null = null; + private pinningManager: PinningManager | null = null; + private pubsubManager: PubSubManager | null = null; + private migrationManager: MigrationManager | null = null; + + // Framework state + private initialized: boolean = false; + private startTime: number = 0; + private healthCheckInterval: any = null; + private metricsCollector: any = null; + private status: FrameworkStatus; + private metrics: FrameworkMetrics; + + constructor(config: DebrosFrameworkConfig = {}) { + this.config = this.mergeDefaultConfig(config); + this.configManager = new ConfigManager(this.config); + + this.status = { + initialized: false, + healthy: false, + version: '1.0.0', // This would come from package.json + environment: this.config.environment || 'development', + services: { + orbitdb: 'disconnected', + ipfs: 'disconnected', + pinning: 'inactive', + pubsub: 'inactive', + }, + lastHealthCheck: 0, + }; + + this.metrics = { + uptime: 0, + totalModels: 0, + totalDatabases: 0, + totalShards: 0, + queriesExecuted: 0, + migrationsRun: 0, + cacheHitRate: 0, + averageQueryTime: 0, + memoryUsage: { + queryCache: 0, + relationshipCache: 0, + total: 0, + }, + performance: { + slowQueries: 0, + failedOperations: 0, + averageResponseTime: 0, + }, + }; + } + + // Main initialization method + async initialize( + existingOrbitDBService?: any, + existingIPFSService?: any, + overrideConfig?: Partial<DebrosFrameworkConfig>, + ): Promise<void> { + if (this.initialized) { + throw new Error('Framework is already initialized'); + } + + try { + this.startTime = Date.now(); + console.log('🚀 Initializing DebrosFramework...'); + + // Apply config overrides + if (overrideConfig) { + this.config = { ...this.config, ...overrideConfig }; + this.configManager = new ConfigManager(this.config); + } + + // Initialize services + await this.initializeServices(existingOrbitDBService, existingIPFSService); + + // Initialize core components + await this.initializeCoreComponents(); + + // Initialize feature components + await this.initializeFeatureComponents(); + + // Setup global framework access + this.setupGlobalAccess(); + + // Start background processes + await this.startBackgroundProcesses(); + + // Run automatic migrations if enabled + if (this.config.features?.autoMigration && this.migrationManager) { + await this.runAutomaticMigrations(); + } + + this.initialized = true; + this.status.initialized = true; + this.status.healthy = true; + + console.log('✅ DebrosFramework initialized successfully'); + this.logFrameworkInfo(); + } catch (error) { + console.error('❌ Framework initialization failed:', error); + await this.cleanup(); + throw error; + } + } + + // Service initialization + private async initializeServices( + existingOrbitDBService?: any, + existingIPFSService?: any, + ): Promise<void> { + console.log('📡 Initializing core services...'); + + try { + // Initialize IPFS service + if (existingIPFSService) { + this.ipfsService = new FrameworkIPFSService(existingIPFSService); + } else { + // In a real implementation, create IPFS instance + throw new Error('IPFS service is required. Please provide an existing IPFS instance.'); + } + + await this.ipfsService.init(); + this.status.services.ipfs = 'connected'; + console.log('✅ IPFS service initialized'); + + // Initialize OrbitDB service + if (existingOrbitDBService) { + this.orbitDBService = new FrameworkOrbitDBService(existingOrbitDBService); + } else { + // In a real implementation, create OrbitDB instance + throw new Error( + 'OrbitDB service is required. Please provide an existing OrbitDB instance.', + ); + } + + await this.orbitDBService.init(); + this.status.services.orbitdb = 'connected'; + console.log('✅ OrbitDB service initialized'); + } catch (error) { + this.status.services.ipfs = 'error'; + this.status.services.orbitdb = 'error'; + throw new Error(`Service initialization failed: ${error}`); + } + } + + // Core component initialization + private async initializeCoreComponents(): Promise<void> { + console.log('🔧 Initializing core components...'); + + // Database Manager + this.databaseManager = new DatabaseManager(this.orbitDBService!); + await this.databaseManager.initializeAllDatabases(); + console.log('✅ DatabaseManager initialized'); + + // Shard Manager + this.shardManager = new ShardManager(); + this.shardManager.setOrbitDBService(this.orbitDBService!); + + // Initialize shards for registered models + const globalModels = ModelRegistry.getGlobalModels(); + for (const model of globalModels) { + if (model.sharding) { + await this.shardManager.createShards(model.modelName, model.sharding, model.dbType); + } + } + console.log('✅ ShardManager initialized'); + + // Query Cache + if (this.config.features?.queryCache !== false) { + const cacheConfig = this.configManager.cacheConfig; + this.queryCache = new QueryCache(cacheConfig?.maxSize || 1000, cacheConfig?.ttl || 300000); + console.log('✅ QueryCache initialized'); + } + + // Relationship Manager + this.relationshipManager = new RelationshipManager({ + databaseManager: this.databaseManager, + shardManager: this.shardManager, + queryCache: this.queryCache, + }); + console.log('✅ RelationshipManager initialized'); + } + + // Feature component initialization + private async initializeFeatureComponents(): Promise<void> { + console.log('🎛️ Initializing feature components...'); + + // Pinning Manager + if (this.config.features?.automaticPinning !== false) { + this.pinningManager = new PinningManager(this.ipfsService!.getHelia(), { + maxTotalPins: this.config.performance?.maxConcurrentOperations || 10000, + cleanupIntervalMs: 60000, + }); + + // Setup default pinning rules based on config + if (this.config.defaultPinning) { + const globalModels = ModelRegistry.getGlobalModels(); + for (const model of globalModels) { + this.pinningManager.setPinningRule(model.modelName, this.config.defaultPinning); + } + } + + this.status.services.pinning = 'active'; + console.log('✅ PinningManager initialized'); + } + + // PubSub Manager + if (this.config.features?.pubsub !== false) { + this.pubsubManager = new PubSubManager(this.ipfsService!.getHelia(), { + enabled: true, + autoPublishModelEvents: true, + autoPublishDatabaseEvents: true, + topicPrefix: `debros-${this.config.environment || 'dev'}`, + }); + + await this.pubsubManager.initialize(); + this.status.services.pubsub = 'active'; + console.log('✅ PubSubManager initialized'); + } + + // Migration Manager + this.migrationManager = new MigrationManager( + this.databaseManager, + this.shardManager, + this.createMigrationLogger(), + ); + console.log('✅ MigrationManager initialized'); + } + + // Setup global framework access for models + private setupGlobalAccess(): void { + (globalThis as any).__debrosFramework = { + databaseManager: this.databaseManager, + shardManager: this.shardManager, + configManager: this.configManager, + queryCache: this.queryCache, + relationshipManager: this.relationshipManager, + pinningManager: this.pinningManager, + pubsubManager: this.pubsubManager, + migrationManager: this.migrationManager, + framework: this, + }; + } + + // Start background processes + private async startBackgroundProcesses(): Promise<void> { + console.log('⚙️ Starting background processes...'); + + // Health check interval + this.healthCheckInterval = setInterval(() => { + this.performHealthCheck(); + }, 30000); // Every 30 seconds + + // Metrics collection + if (this.config.monitoring?.enableMetrics !== false) { + this.metricsCollector = setInterval(() => { + this.collectMetrics(); + }, this.config.monitoring?.metricsInterval || 60000); // Every minute + } + + console.log('✅ Background processes started'); + } + + // Automatic migration execution + private async runAutomaticMigrations(): Promise<void> { + if (!this.migrationManager) return; + + try { + console.log('🔄 Running automatic migrations...'); + + const pendingMigrations = this.migrationManager.getPendingMigrations(); + if (pendingMigrations.length > 0) { + console.log(`Found ${pendingMigrations.length} pending migrations`); + + const results = await this.migrationManager.runPendingMigrations({ + stopOnError: true, + batchSize: this.config.performance?.batchSize || 100, + }); + + const successful = results.filter((r) => r.success).length; + console.log(`✅ Completed ${successful}/${results.length} migrations`); + + this.metrics.migrationsRun += successful; + } else { + console.log('No pending migrations found'); + } + } catch (error) { + console.error('❌ Automatic migration failed:', error); + if (this.config.environment === 'production') { + // In production, don't fail initialization due to migration errors + console.warn('Continuing initialization despite migration failure'); + } else { + throw error; + } + } + } + + // Public API methods + + // Model registration + registerModel(modelClass: typeof BaseModel, config?: any): void { + ModelRegistry.register(modelClass.name, modelClass, config || {}); + console.log(`📝 Registered model: ${modelClass.name}`); + + this.metrics.totalModels = ModelRegistry.getModelNames().length; + } + + // Get model instance + getModel(modelName: string): typeof BaseModel | null { + return ModelRegistry.get(modelName) || null; + } + + // Database operations + async createUserDatabase(userId: string): Promise<void> { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + await this.databaseManager.createUserDatabases(userId); + this.metrics.totalDatabases++; + } + + async getUserDatabase(userId: string, modelName: string): Promise<any> { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + return await this.databaseManager.getUserDatabase(userId, modelName); + } + + async getGlobalDatabase(modelName: string): Promise<any> { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + return await this.databaseManager.getGlobalDatabase(modelName); + } + + // Migration operations + async runMigration(migrationId: string, options?: any): Promise<any> { + if (!this.migrationManager) { + throw new Error('MigrationManager not initialized'); + } + + const result = await this.migrationManager.runMigration(migrationId, options); + this.metrics.migrationsRun++; + return result; + } + + async registerMigration(migration: any): Promise<void> { + if (!this.migrationManager) { + throw new Error('MigrationManager not initialized'); + } + + this.migrationManager.registerMigration(migration); + } + + getPendingMigrations(modelName?: string): any[] { + if (!this.migrationManager) { + return []; + } + + return this.migrationManager.getPendingMigrations(modelName); + } + + // Cache management + clearQueryCache(): void { + if (this.queryCache) { + this.queryCache.clear(); + } + } + + clearRelationshipCache(): void { + if (this.relationshipManager) { + this.relationshipManager.clearRelationshipCache(); + } + } + + async warmupCaches(): Promise<void> { + console.log('🔥 Warming up caches...'); + + if (this.queryCache) { + // Warm up common queries + const commonQueries: any[] = []; // Would be populated with actual queries + await this.queryCache.warmup(commonQueries); + } + + if (this.relationshipManager && this.pinningManager) { + // Warm up relationship cache for popular content + // Implementation would depend on actual models + } + + console.log('✅ Cache warmup completed'); + } + + // Health and monitoring + performHealthCheck(): void { + try { + this.status.lastHealthCheck = Date.now(); + + // Check service health + this.status.services.orbitdb = this.orbitDBService ? 'connected' : 'disconnected'; + this.status.services.ipfs = this.ipfsService ? 'connected' : 'disconnected'; + this.status.services.pinning = this.pinningManager ? 'active' : 'inactive'; + this.status.services.pubsub = this.pubsubManager ? 'active' : 'inactive'; + + // Overall health check + const allServicesHealthy = Object.values(this.status.services).every( + (status) => status === 'connected' || status === 'active', + ); + + this.status.healthy = this.initialized && allServicesHealthy; + } catch (error) { + console.error('Health check failed:', error); + this.status.healthy = false; + } + } + + collectMetrics(): void { + try { + this.metrics.uptime = Date.now() - this.startTime; + this.metrics.totalModels = ModelRegistry.getModelNames().length; + + if (this.queryCache) { + const cacheStats = this.queryCache.getStats(); + this.metrics.cacheHitRate = cacheStats.hitRate; + this.metrics.averageQueryTime = 0; // Would need to be calculated from cache stats + this.metrics.memoryUsage.queryCache = cacheStats.size * 1024; // Estimate + } + + if (this.relationshipManager) { + const relStats = this.relationshipManager.getRelationshipCacheStats(); + this.metrics.memoryUsage.relationshipCache = relStats.cache.memoryUsage; + } + + this.metrics.memoryUsage.total = + this.metrics.memoryUsage.queryCache + this.metrics.memoryUsage.relationshipCache; + } catch (error) { + console.error('Metrics collection failed:', error); + } + } + + getStatus(): FrameworkStatus { + return { ...this.status }; + } + + getMetrics(): FrameworkMetrics { + this.collectMetrics(); // Ensure fresh metrics + return { ...this.metrics }; + } + + getConfig(): DebrosFrameworkConfig { + return { ...this.config }; + } + + // Component access + getDatabaseManager(): DatabaseManager | null { + return this.databaseManager; + } + + getShardManager(): ShardManager | null { + return this.shardManager; + } + + getRelationshipManager(): RelationshipManager | null { + return this.relationshipManager; + } + + getPinningManager(): PinningManager | null { + return this.pinningManager; + } + + getPubSubManager(): PubSubManager | null { + return this.pubsubManager; + } + + getMigrationManager(): MigrationManager | null { + return this.migrationManager; + } + + // Framework lifecycle + async stop(): Promise<void> { + if (!this.initialized) { + return; + } + + console.log('🛑 Stopping DebrosFramework...'); + + try { + await this.cleanup(); + this.initialized = false; + this.status.initialized = false; + this.status.healthy = false; + + console.log('✅ DebrosFramework stopped successfully'); + } catch (error) { + console.error('❌ Error during framework shutdown:', error); + throw error; + } + } + + async restart(newConfig?: Partial<DebrosFrameworkConfig>): Promise<void> { + console.log('🔄 Restarting DebrosFramework...'); + + const orbitDB = this.orbitDBService?.getOrbitDB(); + const ipfs = this.ipfsService?.getHelia(); + + await this.stop(); + + if (newConfig) { + this.config = { ...this.config, ...newConfig }; + } + + await this.initialize(orbitDB, ipfs); + } + + // Cleanup method + private async cleanup(): Promise<void> { + // Stop background processes + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + this.healthCheckInterval = null; + } + + if (this.metricsCollector) { + clearInterval(this.metricsCollector); + this.metricsCollector = null; + } + + // Cleanup components + if (this.pubsubManager) { + await this.pubsubManager.shutdown(); + } + + if (this.pinningManager) { + await this.pinningManager.shutdown(); + } + + if (this.migrationManager) { + await this.migrationManager.cleanup(); + } + + if (this.queryCache) { + this.queryCache.clear(); + } + + if (this.relationshipManager) { + this.relationshipManager.clearRelationshipCache(); + } + + if (this.databaseManager) { + await this.databaseManager.stop(); + } + + if (this.shardManager) { + await this.shardManager.stop(); + } + + // Clear global access + delete (globalThis as any).__debrosFramework; + } + + // Utility methods + private mergeDefaultConfig(config: DebrosFrameworkConfig): DebrosFrameworkConfig { + return { + environment: 'development', + features: { + autoMigration: true, + automaticPinning: true, + pubsub: true, + queryCache: true, + relationshipCache: true, + }, + performance: { + queryTimeout: 30000, + migrationTimeout: 300000, + maxConcurrentOperations: 100, + batchSize: 100, + }, + monitoring: { + enableMetrics: true, + logLevel: 'info', + metricsInterval: 60000, + }, + ...config, + }; + } + + private createMigrationLogger(): any { + const logLevel = this.config.monitoring?.logLevel || 'info'; + + return { + info: (message: string, meta?: any) => { + if (['info', 'debug'].includes(logLevel)) { + console.log(`[MIGRATION INFO] ${message}`, meta || ''); + } + }, + warn: (message: string, meta?: any) => { + if (['warn', 'info', 'debug'].includes(logLevel)) { + console.warn(`[MIGRATION WARN] ${message}`, meta || ''); + } + }, + error: (message: string, meta?: any) => { + console.error(`[MIGRATION ERROR] ${message}`, meta || ''); + }, + debug: (message: string, meta?: any) => { + if (logLevel === 'debug') { + console.log(`[MIGRATION DEBUG] ${message}`, meta || ''); + } + }, + }; + } + + private logFrameworkInfo(): void { + console.log('\n📋 DebrosFramework Information:'); + console.log('=============================='); + console.log(`Version: ${this.status.version}`); + console.log(`Environment: ${this.status.environment}`); + console.log(`Models registered: ${this.metrics.totalModels}`); + console.log( + `Services: ${Object.entries(this.status.services) + .map(([name, status]) => `${name}:${status}`) + .join(', ')}`, + ); + console.log( + `Features enabled: ${Object.entries(this.config.features || {}) + .filter(([, enabled]) => enabled) + .map(([feature]) => feature) + .join(', ')}`, + ); + console.log(''); + } + + // Static factory methods + static async create(config: DebrosFrameworkConfig = {}): Promise<DebrosFramework> { + const framework = new DebrosFramework(config); + return framework; + } + + static async createWithServices( + orbitDBService: any, + ipfsService: any, + config: DebrosFrameworkConfig = {}, + ): Promise<DebrosFramework> { + const framework = new DebrosFramework(config); + await framework.initialize(orbitDBService, ipfsService); + return framework; + } +} + +// Export the main framework class as default +export default DebrosFramework; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { FrameworkConfig, CacheConfig, PinningConfig } from '../types/framework'; + +export interface DatabaseConfig { + userDirectoryShards?: number; + defaultGlobalShards?: number; + cacheSize?: number; +} + +export interface ExtendedFrameworkConfig extends FrameworkConfig { + database?: DatabaseConfig; + debug?: boolean; + logLevel?: 'error' | 'warn' | 'info' | 'debug'; +} + +export class ConfigManager { + private config: ExtendedFrameworkConfig; + private defaults: ExtendedFrameworkConfig = { + cache: { + enabled: true, + maxSize: 1000, + ttl: 300000, // 5 minutes + }, + defaultPinning: { + strategy: 'fixed' as const, + factor: 2, + }, + database: { + userDirectoryShards: 4, + defaultGlobalShards: 8, + cacheSize: 100, + }, + autoMigration: true, + debug: false, + logLevel: 'info', + }; + + constructor(config: ExtendedFrameworkConfig = {}) { + this.config = this.mergeWithDefaults(config); + this.validateConfig(); + } + + private mergeWithDefaults(config: ExtendedFrameworkConfig): ExtendedFrameworkConfig { + return { + ...this.defaults, + ...config, + cache: { + ...this.defaults.cache, + ...config.cache, + }, + defaultPinning: { + ...this.defaults.defaultPinning, + ...(config.defaultPinning || {}), + }, + database: { + ...this.defaults.database, + ...config.database, + }, + }; + } + + private validateConfig(): void { + // Validate cache configuration + if (this.config.cache) { + if (this.config.cache.maxSize && this.config.cache.maxSize < 1) { + throw new Error('Cache maxSize must be at least 1'); + } + if (this.config.cache.ttl && this.config.cache.ttl < 1000) { + throw new Error('Cache TTL must be at least 1000ms'); + } + } + + // Validate pinning configuration + if (this.config.defaultPinning) { + if (this.config.defaultPinning.factor && this.config.defaultPinning.factor < 1) { + throw new Error('Pinning factor must be at least 1'); + } + } + + // Validate database configuration + if (this.config.database) { + if ( + this.config.database.userDirectoryShards && + this.config.database.userDirectoryShards < 1 + ) { + throw new Error('User directory shards must be at least 1'); + } + if ( + this.config.database.defaultGlobalShards && + this.config.database.defaultGlobalShards < 1 + ) { + throw new Error('Default global shards must be at least 1'); + } + } + } + + // Getters for configuration values + get cacheConfig(): CacheConfig | undefined { + return this.config.cache; + } + + get defaultPinningConfig(): PinningConfig | undefined { + return this.config.defaultPinning; + } + + get databaseConfig(): DatabaseConfig | undefined { + return this.config.database; + } + + get autoMigration(): boolean { + return this.config.autoMigration || false; + } + + get debug(): boolean { + return this.config.debug || false; + } + + get logLevel(): string { + return this.config.logLevel || 'info'; + } + + // Update configuration at runtime + updateConfig(newConfig: Partial<ExtendedFrameworkConfig>): void { + this.config = this.mergeWithDefaults({ + ...this.config, + ...newConfig, + }); + this.validateConfig(); + } + + // Get full configuration + getConfig(): ExtendedFrameworkConfig { + return { ...this.config }; + } + + // Configuration presets + static developmentConfig(): ExtendedFrameworkConfig { + return { + debug: true, + logLevel: 'debug', + cache: { + enabled: true, + maxSize: 100, + ttl: 60000, // 1 minute for development + }, + database: { + userDirectoryShards: 2, + defaultGlobalShards: 2, + cacheSize: 50, + }, + defaultPinning: { + strategy: 'fixed' as const, + factor: 1, // Minimal pinning for development + }, + }; + } + + static productionConfig(): ExtendedFrameworkConfig { + return { + debug: false, + logLevel: 'warn', + cache: { + enabled: true, + maxSize: 10000, + ttl: 600000, // 10 minutes + }, + database: { + userDirectoryShards: 16, + defaultGlobalShards: 32, + cacheSize: 1000, + }, + defaultPinning: { + strategy: 'popularity' as const, + factor: 5, // Higher redundancy for production + }, + }; + } + + static testConfig(): ExtendedFrameworkConfig { + return { + debug: true, + logLevel: 'error', // Minimal logging during tests + cache: { + enabled: false, // Disable caching for predictable tests + }, + database: { + userDirectoryShards: 1, + defaultGlobalShards: 1, + cacheSize: 10, + }, + defaultPinning: { + strategy: 'fixed', + factor: 1, + }, + autoMigration: false, // Manual migration control in tests + }; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ModelRegistry } from './ModelRegistry'; +import { FrameworkOrbitDBService } from '../services/OrbitDBService'; +import { StoreType } from '../types/framework'; +import { UserMappings } from '../types/models'; + +export class UserMappingsData implements UserMappings { + constructor( + public userId: string, + public databases: Record<string, string>, + ) {} +} + +export class DatabaseManager { + private orbitDBService: FrameworkOrbitDBService; + private databases: Map<string, any> = new Map(); + private userMappings: Map<string, any> = new Map(); + private globalDatabases: Map<string, any> = new Map(); + private globalDirectoryShards: any[] = []; + private initialized: boolean = false; + + constructor(orbitDBService: FrameworkOrbitDBService) { + this.orbitDBService = orbitDBService; + } + + async initializeAllDatabases(): Promise<void> { + if (this.initialized) { + return; + } + + console.log('🚀 Initializing DebrosFramework databases...'); + + // Initialize global databases first + await this.initializeGlobalDatabases(); + + // Initialize system databases (user directory, etc.) + await this.initializeSystemDatabases(); + + this.initialized = true; + console.log('✅ Database initialization complete'); + } + + private async initializeGlobalDatabases(): Promise<void> { + const globalModels = ModelRegistry.getGlobalModels(); + + console.log(`📊 Creating ${globalModels.length} global databases...`); + + for (const model of globalModels) { + const dbName = `global-${model.modelName.toLowerCase()}`; + + try { + const db = await this.createDatabase(dbName, model.dbType, 'global'); + this.globalDatabases.set(model.modelName, db); + + console.log(`✓ Created global database: ${dbName} (${model.dbType})`); + } catch (error) { + console.error(`❌ Failed to create global database ${dbName}:`, error); + throw error; + } + } + } + + private async initializeSystemDatabases(): Promise<void> { + console.log('🔧 Creating system databases...'); + + // Create global user directory shards + const DIRECTORY_SHARD_COUNT = 4; // Configurable + + for (let i = 0; i < DIRECTORY_SHARD_COUNT; i++) { + const shardName = `global-user-directory-shard-${i}`; + try { + const shard = await this.createDatabase(shardName, 'keyvalue', 'system'); + this.globalDirectoryShards.push(shard); + + console.log(`✓ Created directory shard: ${shardName}`); + } catch (error) { + console.error(`❌ Failed to create directory shard ${shardName}:`, error); + throw error; + } + } + + console.log(`✅ Created ${this.globalDirectoryShards.length} directory shards`); + } + + async createUserDatabases(userId: string): Promise<UserMappingsData> { + console.log(`👤 Creating databases for user: ${userId}`); + + const userScopedModels = ModelRegistry.getUserScopedModels(); + const databases: Record<string, string> = {}; + + // Create mappings database first + const mappingsDBName = `${userId}-mappings`; + const mappingsDB = await this.createDatabase(mappingsDBName, 'keyvalue', 'user'); + + // Create database for each user-scoped model + for (const model of userScopedModels) { + const dbName = `${userId}-${model.modelName.toLowerCase()}`; + + try { + const db = await this.createDatabase(dbName, model.dbType, 'user'); + databases[`${model.modelName.toLowerCase()}DB`] = db.address.toString(); + + console.log(`✓ Created user database: ${dbName} (${model.dbType})`); + } catch (error) { + console.error(`❌ Failed to create user database ${dbName}:`, error); + throw error; + } + } + + // Store mappings in the mappings database + await mappingsDB.set('mappings', databases); + console.log(`✓ Stored database mappings for user ${userId}`); + + // Register in global directory + await this.registerUserInDirectory(userId, mappingsDB.address.toString()); + + const userMappings = new UserMappingsData(userId, databases); + + // Cache for future use + this.userMappings.set(userId, userMappings); + + console.log(`✅ User databases created successfully for ${userId}`); + return userMappings; + } + + async getUserDatabase(userId: string, modelName: string): Promise<any> { + const mappings = await this.getUserMappings(userId); + const dbKey = `${modelName.toLowerCase()}DB`; + const dbAddress = mappings.databases[dbKey]; + + if (!dbAddress) { + throw new Error(`Database not found for user ${userId} and model ${modelName}`); + } + + // Check if we have this database cached + const cacheKey = `${userId}-${modelName}`; + if (this.databases.has(cacheKey)) { + return this.databases.get(cacheKey); + } + + // Open the database + const db = await this.openDatabase(dbAddress); + this.databases.set(cacheKey, db); + + return db; + } + + async getUserMappings(userId: string): Promise<UserMappingsData> { + // Check cache first + if (this.userMappings.has(userId)) { + return this.userMappings.get(userId); + } + + // Get from global directory + const shardIndex = this.getShardIndex(userId, this.globalDirectoryShards.length); + const shard = this.globalDirectoryShards[shardIndex]; + + if (!shard) { + throw new Error('Global directory not initialized'); + } + + const mappingsAddress = await shard.get(userId); + if (!mappingsAddress) { + throw new Error(`User ${userId} not found in directory`); + } + + const mappingsDB = await this.openDatabase(mappingsAddress); + const mappings = await mappingsDB.get('mappings'); + + if (!mappings) { + throw new Error(`No database mappings found for user ${userId}`); + } + + const userMappings = new UserMappingsData(userId, mappings); + + // Cache for future use + this.userMappings.set(userId, userMappings); + + return userMappings; + } + + async getGlobalDatabase(modelName: string): Promise<any> { + const db = this.globalDatabases.get(modelName); + if (!db) { + throw new Error(`Global database not found for model: ${modelName}`); + } + return db; + } + + async getGlobalDirectoryShards(): Promise<any[]> { + return this.globalDirectoryShards; + } + + private async createDatabase(name: string, type: StoreType, _scope: string): Promise<any> { + try { + const db = await this.orbitDBService.openDatabase(name, type); + + // Store database reference + this.databases.set(name, db); + + return db; + } catch (error) { + console.error(`Failed to create database ${name}:`, error); + throw new Error(`Database creation failed for ${name}: ${error}`); + } + } + + private async openDatabase(address: string): Promise<any> { + try { + // Check if we already have this database cached by address + if (this.databases.has(address)) { + return this.databases.get(address); + } + + // Open database by address (implementation may vary based on OrbitDB version) + const orbitdb = this.orbitDBService.getOrbitDB(); + const db = await orbitdb.open(address); + + // Cache the database + this.databases.set(address, db); + + return db; + } catch (error) { + console.error(`Failed to open database at address ${address}:`, error); + throw new Error(`Database opening failed: ${error}`); + } + } + + private async registerUserInDirectory(userId: string, mappingsAddress: string): Promise<void> { + const shardIndex = this.getShardIndex(userId, this.globalDirectoryShards.length); + const shard = this.globalDirectoryShards[shardIndex]; + + if (!shard) { + throw new Error('Global directory shards not initialized'); + } + + try { + await shard.set(userId, mappingsAddress); + console.log(`✓ Registered user ${userId} in directory shard ${shardIndex}`); + } catch (error) { + console.error(`Failed to register user ${userId} in directory:`, error); + throw error; + } + } + + private getShardIndex(key: string, shardCount: number): number { + // Simple hash-based sharding + let hash = 0; + for (let i = 0; i < key.length; i++) { + hash = ((hash << 5) - hash + key.charCodeAt(i)) & 0xffffffff; + } + return Math.abs(hash) % shardCount; + } + + // Database operation helpers + async getAllDocuments(database: any, dbType: StoreType): Promise<any[]> { + try { + switch (dbType) { + case 'eventlog': + const iterator = database.iterator(); + return iterator.collect(); + + case 'keyvalue': + return Object.values(database.all()); + + case 'docstore': + return database.query(() => true); + + case 'feed': + const feedIterator = database.iterator(); + return feedIterator.collect(); + + case 'counter': + return [{ value: database.value, id: database.id }]; + + default: + throw new Error(`Unsupported database type: ${dbType}`); + } + } catch (error) { + console.error(`Error fetching documents from ${dbType} database:`, error); + throw error; + } + } + + async addDocument(database: any, dbType: StoreType, data: any): Promise<string> { + try { + switch (dbType) { + case 'eventlog': + return await database.add(data); + + case 'keyvalue': + await database.set(data.id, data); + return data.id; + + case 'docstore': + return await database.put(data); + + case 'feed': + return await database.add(data); + + case 'counter': + await database.inc(data.amount || 1); + return database.id; + + default: + throw new Error(`Unsupported database type: ${dbType}`); + } + } catch (error) { + console.error(`Error adding document to ${dbType} database:`, error); + throw error; + } + } + + async updateDocument(database: any, dbType: StoreType, id: string, data: any): Promise<void> { + try { + switch (dbType) { + case 'keyvalue': + await database.set(id, data); + break; + + case 'docstore': + await database.put(data); + break; + + default: + // For append-only stores, we add a new entry + await this.addDocument(database, dbType, data); + } + } catch (error) { + console.error(`Error updating document in ${dbType} database:`, error); + throw error; + } + } + + async deleteDocument(database: any, dbType: StoreType, id: string): Promise<void> { + try { + switch (dbType) { + case 'keyvalue': + await database.del(id); + break; + + case 'docstore': + await database.del(id); + break; + + default: + // For append-only stores, we might add a deletion marker + await this.addDocument(database, dbType, { _deleted: true, id, deletedAt: Date.now() }); + } + } catch (error) { + console.error(`Error deleting document from ${dbType} database:`, error); + throw error; + } + } + + // Cleanup methods + async stop(): Promise<void> { + console.log('🛑 Stopping DatabaseManager...'); + + // Clear caches + this.databases.clear(); + this.userMappings.clear(); + this.globalDatabases.clear(); + this.globalDirectoryShards = []; + + this.initialized = false; + console.log('✅ DatabaseManager stopped'); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { ModelConfig } from '../types/models'; +import { StoreType } from '../types/framework'; + +export class ModelRegistry { + private static models: Map<string, typeof BaseModel> = new Map(); + private static configs: Map<string, ModelConfig> = new Map(); + + static register(name: string, modelClass: typeof BaseModel, config: ModelConfig): void { + this.models.set(name, modelClass); + this.configs.set(name, config); + + // Validate model configuration + this.validateModel(modelClass, config); + + console.log(`Registered model: ${name} with scope: ${config.scope || 'global'}`); + } + + static get(name: string): typeof BaseModel | undefined { + return this.models.get(name); + } + + static getConfig(name: string): ModelConfig | undefined { + return this.configs.get(name); + } + + static getAllModels(): Map<string, typeof BaseModel> { + return new Map(this.models); + } + + static getUserScopedModels(): Array<typeof BaseModel> { + return Array.from(this.models.values()).filter((model) => model.scope === 'user'); + } + + static getGlobalModels(): Array<typeof BaseModel> { + return Array.from(this.models.values()).filter((model) => model.scope === 'global'); + } + + static getModelNames(): string[] { + return Array.from(this.models.keys()); + } + + static clear(): void { + this.models.clear(); + this.configs.clear(); + } + + private static validateModel(modelClass: typeof BaseModel, config: ModelConfig): void { + // Validate model name + if (!modelClass.name) { + throw new Error('Model class must have a name'); + } + + // Validate database type + if (config.type && !this.isValidStoreType(config.type)) { + throw new Error(`Invalid store type: ${config.type}`); + } + + // Validate scope + if (config.scope && !['user', 'global'].includes(config.scope)) { + throw new Error(`Invalid scope: ${config.scope}. Must be 'user' or 'global'`); + } + + // Validate sharding configuration + if (config.sharding) { + this.validateShardingConfig(config.sharding); + } + + // Validate pinning configuration + if (config.pinning) { + this.validatePinningConfig(config.pinning); + } + + console.log(`✓ Model ${modelClass.name} configuration validated`); + } + + private static isValidStoreType(type: StoreType): boolean { + return ['eventlog', 'keyvalue', 'docstore', 'counter', 'feed'].includes(type); + } + + private static validateShardingConfig(config: any): void { + if (!config.strategy || !['hash', 'range', 'user'].includes(config.strategy)) { + throw new Error('Sharding strategy must be one of: hash, range, user'); + } + + if (!config.count || config.count < 1) { + throw new Error('Sharding count must be a positive number'); + } + + if (!config.key) { + throw new Error('Sharding key is required'); + } + } + + private static validatePinningConfig(config: any): void { + if (config.strategy && !['fixed', 'popularity', 'tiered'].includes(config.strategy)) { + throw new Error('Pinning strategy must be one of: fixed, popularity, tiered'); + } + + if (config.factor && (typeof config.factor !== 'number' || config.factor < 1)) { + throw new Error('Pinning factor must be a positive number'); + } + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
ConfigManager.ts | +
+
+ |
+ 0% | +0/29 | +0% | +0/35 | +0% | +0/14 | +0% | +0/29 | +
DatabaseManager.ts | +
+
+ |
+ 0% | +0/168 | +0% | +0/40 | +0% | +0/20 | +0% | +0/165 | +
ModelRegistry.ts | +
+
+ |
+ 0% | +0/38 | +0% | +0/35 | +0% | +0/14 | +0% | +0/36 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
DebrosFramework.ts | +
+
+ |
+ 0% | +0/249 | +0% | +0/129 | +0% | +0/49 | +0% | +0/247 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * MigrationBuilder - Fluent API for Creating Migrations + * + * This class provides a convenient fluent interface for creating migration objects + * with built-in validation and common operation patterns. + */ + +import { Migration, MigrationOperation, MigrationValidator } from './MigrationManager'; +import { FieldConfig } from '../types/models'; + +export class MigrationBuilder { + private migration: Partial<Migration>; + private upOperations: MigrationOperation[] = []; + private downOperations: MigrationOperation[] = []; + private validators: MigrationValidator[] = []; + + constructor(id: string, version: string, name: string) { + this.migration = { + id, + version, + name, + description: '', + targetModels: [], + createdAt: Date.now(), + tags: [], + }; + } + + // Basic migration metadata + description(desc: string): this { + this.migration.description = desc; + return this; + } + + author(author: string): this { + this.migration.author = author; + return this; + } + + tags(...tags: string[]): this { + this.migration.tags = tags; + return this; + } + + targetModels(...models: string[]): this { + this.migration.targetModels = models; + return this; + } + + dependencies(...migrationIds: string[]): this { + this.migration.dependencies = migrationIds; + return this; + } + + // Field operations + addField(modelName: string, fieldName: string, fieldConfig: FieldConfig): this { + this.upOperations.push({ + type: 'add_field', + modelName, + fieldName, + fieldConfig, + }); + + // Auto-generate reverse operation + this.downOperations.unshift({ + type: 'remove_field', + modelName, + fieldName, + }); + + this.ensureTargetModel(modelName); + return this; + } + + removeField(modelName: string, fieldName: string, preserveData: boolean = false): this { + this.upOperations.push({ + type: 'remove_field', + modelName, + fieldName, + }); + + if (!preserveData) { + // Cannot auto-reverse field removal without knowing the original config + this.downOperations.unshift({ + type: 'custom', + modelName, + customOperation: async (context) => { + context.logger.warn(`Cannot reverse removal of field ${fieldName} - data may be lost`); + }, + }); + } + + this.ensureTargetModel(modelName); + return this; + } + + modifyField( + modelName: string, + fieldName: string, + newFieldConfig: FieldConfig, + oldFieldConfig?: FieldConfig, + ): this { + this.upOperations.push({ + type: 'modify_field', + modelName, + fieldName, + fieldConfig: newFieldConfig, + }); + + if (oldFieldConfig) { + this.downOperations.unshift({ + type: 'modify_field', + modelName, + fieldName, + fieldConfig: oldFieldConfig, + }); + } + + this.ensureTargetModel(modelName); + return this; + } + + renameField(modelName: string, oldFieldName: string, newFieldName: string): this { + this.upOperations.push({ + type: 'rename_field', + modelName, + fieldName: oldFieldName, + newFieldName, + }); + + // Auto-generate reverse operation + this.downOperations.unshift({ + type: 'rename_field', + modelName, + fieldName: newFieldName, + newFieldName: oldFieldName, + }); + + this.ensureTargetModel(modelName); + return this; + } + + // Data transformation operations + transformData( + modelName: string, + transformer: (data: any) => any, + reverseTransformer?: (data: any) => any, + ): this { + this.upOperations.push({ + type: 'transform_data', + modelName, + transformer, + }); + + if (reverseTransformer) { + this.downOperations.unshift({ + type: 'transform_data', + modelName, + transformer: reverseTransformer, + }); + } + + this.ensureTargetModel(modelName); + return this; + } + + // Custom operations + customOperation( + modelName: string, + operation: (context: any) => Promise<void>, + rollbackOperation?: (context: any) => Promise<void>, + ): this { + this.upOperations.push({ + type: 'custom', + modelName, + customOperation: operation, + }); + + if (rollbackOperation) { + this.downOperations.unshift({ + type: 'custom', + modelName, + customOperation: rollbackOperation, + }); + } + + this.ensureTargetModel(modelName); + return this; + } + + // Common patterns + addTimestamps(modelName: string): this { + this.addField(modelName, 'createdAt', { + type: 'number', + required: false, + default: Date.now(), + }); + + this.addField(modelName, 'updatedAt', { + type: 'number', + required: false, + default: Date.now(), + }); + + return this; + } + + addSoftDeletes(modelName: string): this { + this.addField(modelName, 'deletedAt', { + type: 'number', + required: false, + default: null, + }); + + return this; + } + + addUuid(modelName: string, fieldName: string = 'uuid'): this { + this.addField(modelName, fieldName, { + type: 'string', + required: true, + unique: true, + default: () => this.generateUuid(), + }); + + return this; + } + + renameModel(oldModelName: string, newModelName: string): this { + // This would require more complex operations across the entire system + this.customOperation( + oldModelName, + async (context) => { + context.logger.info(`Renaming model ${oldModelName} to ${newModelName}`); + // Implementation would involve updating model registry, database names, etc. + }, + async (context) => { + context.logger.info(`Reverting model rename ${newModelName} to ${oldModelName}`); + }, + ); + + return this; + } + + // Migration patterns for common scenarios + createIndex(modelName: string, fieldNames: string[], options: any = {}): this { + this.upOperations.push({ + type: 'add_index', + modelName, + indexConfig: { + fields: fieldNames, + ...options, + }, + }); + + this.downOperations.unshift({ + type: 'remove_index', + modelName, + indexConfig: { + fields: fieldNames, + ...options, + }, + }); + + this.ensureTargetModel(modelName); + return this; + } + + // Data migration helpers + migrateData( + fromModel: string, + toModel: string, + fieldMapping: Record<string, string>, + options: { + batchSize?: number; + condition?: (data: any) => boolean; + transform?: (data: any) => any; + } = {}, + ): this { + this.customOperation(fromModel, async (context) => { + context.logger.info(`Migrating data from ${fromModel} to ${toModel}`); + + const records = await context.databaseManager.getAllRecords(fromModel); + const batchSize = options.batchSize || 100; + + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (options.condition && !options.condition(record)) { + continue; + } + + const newRecord: any = {}; + + // Map fields + for (const [oldField, newField] of Object.entries(fieldMapping)) { + if (oldField in record) { + newRecord[newField] = record[oldField]; + } + } + + // Apply transformation if provided + if (options.transform) { + Object.assign(newRecord, options.transform(newRecord)); + } + + await context.databaseManager.createRecord(toModel, newRecord); + } + } + }); + + this.ensureTargetModel(fromModel); + this.ensureTargetModel(toModel); + return this; + } + + // Validation + addValidator( + name: string, + description: string, + validateFn: (context: any) => Promise<any>, + ): this { + this.validators.push({ + name, + description, + validate: validateFn, + }); + return this; + } + + validateFieldExists(modelName: string, fieldName: string): this { + return this.addValidator( + `validate_${fieldName}_exists`, + `Ensure field ${fieldName} exists in ${modelName}`, + async (_context) => { + // Implementation would check if field exists + return { valid: true, errors: [], warnings: [] }; + }, + ); + } + + validateDataIntegrity(modelName: string, checkFn: (records: any[]) => any): this { + return this.addValidator( + `validate_${modelName}_integrity`, + `Validate data integrity for ${modelName}`, + async (context) => { + const records = await context.databaseManager.getAllRecords(modelName); + return checkFn(records); + }, + ); + } + + // Build the final migration + build(): Migration { + if (!this.migration.targetModels || this.migration.targetModels.length === 0) { + throw new Error('Migration must have at least one target model'); + } + + if (this.upOperations.length === 0) { + throw new Error('Migration must have at least one operation'); + } + + return { + id: this.migration.id!, + version: this.migration.version!, + name: this.migration.name!, + description: this.migration.description!, + targetModels: this.migration.targetModels!, + up: this.upOperations, + down: this.downOperations, + dependencies: this.migration.dependencies, + validators: this.validators.length > 0 ? this.validators : undefined, + createdAt: this.migration.createdAt!, + author: this.migration.author, + tags: this.migration.tags, + }; + } + + // Helper methods + private ensureTargetModel(modelName: string): void { + if (!this.migration.targetModels!.includes(modelName)) { + this.migration.targetModels!.push(modelName); + } + } + + private generateUuid(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } + + // Static factory methods for common migration types + static create(id: string, version: string, name: string): MigrationBuilder { + return new MigrationBuilder(id, version, name); + } + + static addFieldMigration( + id: string, + version: string, + modelName: string, + fieldName: string, + fieldConfig: FieldConfig, + ): Migration { + return new MigrationBuilder(id, version, `Add ${fieldName} to ${modelName}`) + .description(`Add new field ${fieldName} to ${modelName} model`) + .addField(modelName, fieldName, fieldConfig) + .build(); + } + + static removeFieldMigration( + id: string, + version: string, + modelName: string, + fieldName: string, + ): Migration { + return new MigrationBuilder(id, version, `Remove ${fieldName} from ${modelName}`) + .description(`Remove field ${fieldName} from ${modelName} model`) + .removeField(modelName, fieldName) + .build(); + } + + static renameFieldMigration( + id: string, + version: string, + modelName: string, + oldFieldName: string, + newFieldName: string, + ): Migration { + return new MigrationBuilder( + id, + version, + `Rename ${oldFieldName} to ${newFieldName} in ${modelName}`, + ) + .description(`Rename field ${oldFieldName} to ${newFieldName} in ${modelName} model`) + .renameField(modelName, oldFieldName, newFieldName) + .build(); + } + + static dataTransformMigration( + id: string, + version: string, + modelName: string, + description: string, + transformer: (data: any) => any, + reverseTransformer?: (data: any) => any, + ): Migration { + return new MigrationBuilder(id, version, `Transform data in ${modelName}`) + .description(description) + .transformData(modelName, transformer, reverseTransformer) + .build(); + } +} + +// Export convenience function for creating migrations +export function createMigration(id: string, version: string, name: string): MigrationBuilder { + return MigrationBuilder.create(id, version, name); +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * MigrationManager - Schema Migration and Data Transformation System + * + * This class handles: + * - Schema version management across distributed databases + * - Automatic data migration and transformation + * - Rollback capabilities for failed migrations + * - Conflict resolution during migration + * - Migration validation and integrity checks + * - Cross-shard migration coordination + */ + +import { FieldConfig } from '../types/models'; + +export interface Migration { + id: string; + version: string; + name: string; + description: string; + targetModels: string[]; + up: MigrationOperation[]; + down: MigrationOperation[]; + dependencies?: string[]; // Migration IDs that must run before this one + validators?: MigrationValidator[]; + createdAt: number; + author?: string; + tags?: string[]; +} + +export interface MigrationOperation { + type: + | 'add_field' + | 'remove_field' + | 'modify_field' + | 'rename_field' + | 'add_index' + | 'remove_index' + | 'transform_data' + | 'custom'; + modelName: string; + fieldName?: string; + newFieldName?: string; + fieldConfig?: FieldConfig; + indexConfig?: any; + transformer?: (data: any) => any; + customOperation?: (context: MigrationContext) => Promise<void>; + rollbackOperation?: (context: MigrationContext) => Promise<void>; + options?: { + batchSize?: number; + parallel?: boolean; + skipValidation?: boolean; + }; +} + +export interface MigrationValidator { + name: string; + description: string; + validate: (context: MigrationContext) => Promise<ValidationResult>; +} + +export interface MigrationContext { + migration: Migration; + modelName: string; + databaseManager: any; + shardManager: any; + currentData?: any[]; + operation: MigrationOperation; + progress: MigrationProgress; + logger: MigrationLogger; +} + +export interface MigrationProgress { + migrationId: string; + status: 'pending' | 'running' | 'completed' | 'failed' | 'rolled_back'; + startedAt?: number; + completedAt?: number; + totalRecords: number; + processedRecords: number; + errorCount: number; + warnings: string[]; + errors: string[]; + currentOperation?: string; + estimatedTimeRemaining?: number; +} + +export interface MigrationResult { + migrationId: string; + success: boolean; + duration: number; + recordsProcessed: number; + recordsModified: number; + warnings: string[]; + errors: string[]; + rollbackAvailable: boolean; +} + +export interface MigrationLogger { + info: (message: string, meta?: any) => void; + warn: (message: string, meta?: any) => void; + error: (message: string, meta?: any) => void; + debug: (message: string, meta?: any) => void; +} + +export interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} + +export class MigrationManager { + private databaseManager: any; + private shardManager: any; + private migrations: Map<string, Migration> = new Map(); + private migrationHistory: Map<string, MigrationResult[]> = new Map(); + private activeMigrations: Map<string, MigrationProgress> = new Map(); + private migrationOrder: string[] = []; + private logger: MigrationLogger; + + constructor(databaseManager: any, shardManager: any, logger?: MigrationLogger) { + this.databaseManager = databaseManager; + this.shardManager = shardManager; + this.logger = logger || this.createDefaultLogger(); + } + + // Register a new migration + registerMigration(migration: Migration): void { + // Validate migration structure + this.validateMigrationStructure(migration); + + // Check for version conflicts + const existingMigration = Array.from(this.migrations.values()).find( + (m) => m.version === migration.version, + ); + + if (existingMigration && existingMigration.id !== migration.id) { + throw new Error(`Migration version ${migration.version} already exists with different ID`); + } + + this.migrations.set(migration.id, migration); + this.updateMigrationOrder(); + + this.logger.info(`Registered migration: ${migration.name} (${migration.version})`, { + migrationId: migration.id, + targetModels: migration.targetModels, + }); + } + + // Get all registered migrations + getMigrations(): Migration[] { + return Array.from(this.migrations.values()).sort((a, b) => + this.compareVersions(a.version, b.version), + ); + } + + // Get migration by ID + getMigration(migrationId: string): Migration | null { + return this.migrations.get(migrationId) || null; + } + + // Get pending migrations for a model or all models + getPendingMigrations(modelName?: string): Migration[] { + const allMigrations = this.getMigrations(); + const appliedMigrations = this.getAppliedMigrations(modelName); + const appliedIds = new Set(appliedMigrations.map((m) => m.migrationId)); + + return allMigrations.filter((migration) => { + if (!appliedIds.has(migration.id)) { + return modelName ? migration.targetModels.includes(modelName) : true; + } + return false; + }); + } + + // Run a specific migration + async runMigration( + migrationId: string, + options: { + dryRun?: boolean; + batchSize?: number; + parallelShards?: boolean; + skipValidation?: boolean; + } = {}, + ): Promise<MigrationResult> { + const migration = this.migrations.get(migrationId); + if (!migration) { + throw new Error(`Migration ${migrationId} not found`); + } + + // Check if migration is already running + if (this.activeMigrations.has(migrationId)) { + throw new Error(`Migration ${migrationId} is already running`); + } + + // Check dependencies + await this.validateDependencies(migration); + + const startTime = Date.now(); + const progress: MigrationProgress = { + migrationId, + status: 'running', + startedAt: startTime, + totalRecords: 0, + processedRecords: 0, + errorCount: 0, + warnings: [], + errors: [], + }; + + this.activeMigrations.set(migrationId, progress); + + try { + this.logger.info(`Starting migration: ${migration.name}`, { + migrationId, + dryRun: options.dryRun, + options, + }); + + if (options.dryRun) { + return await this.performDryRun(migration, options); + } + + // Pre-migration validation + if (!options.skipValidation) { + await this.runPreMigrationValidation(migration); + } + + // Execute migration operations + const result = await this.executeMigration(migration, options, progress); + + // Post-migration validation + if (!options.skipValidation) { + await this.runPostMigrationValidation(migration); + } + + // Record successful migration + progress.status = 'completed'; + progress.completedAt = Date.now(); + + await this.recordMigrationResult(result); + + this.logger.info(`Migration completed: ${migration.name}`, { + migrationId, + duration: result.duration, + recordsProcessed: result.recordsProcessed, + }); + + return result; + } catch (error: any) { + progress.status = 'failed'; + progress.errors.push(error.message); + + this.logger.error(`Migration failed: ${migration.name}`, { + migrationId, + error: error.message, + stack: error.stack, + }); + + // Attempt rollback if possible + const rollbackResult = await this.attemptRollback(migration, progress); + + const result: MigrationResult = { + migrationId, + success: false, + duration: Date.now() - startTime, + recordsProcessed: progress.processedRecords, + recordsModified: 0, + warnings: progress.warnings, + errors: progress.errors, + rollbackAvailable: rollbackResult.success, + }; + + await this.recordMigrationResult(result); + throw error; + } finally { + this.activeMigrations.delete(migrationId); + } + } + + // Run all pending migrations + async runPendingMigrations( + options: { + modelName?: string; + dryRun?: boolean; + stopOnError?: boolean; + batchSize?: number; + } = {}, + ): Promise<MigrationResult[]> { + const pendingMigrations = this.getPendingMigrations(options.modelName); + const results: MigrationResult[] = []; + + this.logger.info(`Running ${pendingMigrations.length} pending migrations`, { + modelName: options.modelName, + dryRun: options.dryRun, + }); + + for (const migration of pendingMigrations) { + try { + const result = await this.runMigration(migration.id, { + dryRun: options.dryRun, + batchSize: options.batchSize, + }); + results.push(result); + + if (!result.success && options.stopOnError) { + this.logger.warn('Stopping migration run due to error', { + failedMigration: migration.id, + stopOnError: options.stopOnError, + }); + break; + } + } catch (error) { + if (options.stopOnError) { + throw error; + } + this.logger.error(`Skipping failed migration: ${migration.id}`, { error }); + } + } + + return results; + } + + // Rollback a migration + async rollbackMigration(migrationId: string): Promise<MigrationResult> { + const migration = this.migrations.get(migrationId); + if (!migration) { + throw new Error(`Migration ${migrationId} not found`); + } + + const appliedMigrations = this.getAppliedMigrations(); + const isApplied = appliedMigrations.some((m) => m.migrationId === migrationId && m.success); + + if (!isApplied) { + throw new Error(`Migration ${migrationId} has not been applied`); + } + + const startTime = Date.now(); + const progress: MigrationProgress = { + migrationId, + status: 'running', + startedAt: startTime, + totalRecords: 0, + processedRecords: 0, + errorCount: 0, + warnings: [], + errors: [], + }; + + try { + this.logger.info(`Starting rollback: ${migration.name}`, { migrationId }); + + const result = await this.executeRollback(migration, progress); + + result.rollbackAvailable = false; + await this.recordMigrationResult(result); + + this.logger.info(`Rollback completed: ${migration.name}`, { + migrationId, + duration: result.duration, + }); + + return result; + } catch (error: any) { + this.logger.error(`Rollback failed: ${migration.name}`, { + migrationId, + error: error.message, + }); + throw error; + } + } + + // Execute migration operations + private async executeMigration( + migration: Migration, + options: any, + progress: MigrationProgress, + ): Promise<MigrationResult> { + const startTime = Date.now(); + let totalProcessed = 0; + let totalModified = 0; + + for (const modelName of migration.targetModels) { + for (const operation of migration.up) { + if (operation.modelName !== modelName) continue; + + progress.currentOperation = `${operation.type} on ${operation.modelName}.${operation.fieldName || 'N/A'}`; + + this.logger.debug(`Executing operation: ${progress.currentOperation}`, { + migrationId: migration.id, + operation: operation.type, + }); + + const context: MigrationContext = { + migration, + modelName, + databaseManager: this.databaseManager, + shardManager: this.shardManager, + operation, + progress, + logger: this.logger, + }; + + const operationResult = await this.executeOperation(context, options); + totalProcessed += operationResult.processed; + totalModified += operationResult.modified; + progress.processedRecords = totalProcessed; + } + } + + return { + migrationId: migration.id, + success: true, + duration: Date.now() - startTime, + recordsProcessed: totalProcessed, + recordsModified: totalModified, + warnings: progress.warnings, + errors: progress.errors, + rollbackAvailable: migration.down.length > 0, + }; + } + + // Execute a single migration operation + private async executeOperation( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + switch (operation.type) { + case 'add_field': + return await this.executeAddField(context, options); + + case 'remove_field': + return await this.executeRemoveField(context, options); + + case 'modify_field': + return await this.executeModifyField(context, options); + + case 'rename_field': + return await this.executeRenameField(context, options); + + case 'transform_data': + return await this.executeDataTransformation(context, options); + + case 'custom': + return await this.executeCustomOperation(context, options); + + default: + throw new Error(`Unsupported operation type: ${operation.type}`); + } + } + + // Execute add field operation + private async executeAddField( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.fieldName || !operation.fieldConfig) { + throw new Error('Add field operation requires fieldName and fieldConfig'); + } + + // Update model metadata (in a real implementation, this would update the model registry) + this.logger.info(`Adding field ${operation.fieldName} to ${operation.modelName}`, { + fieldConfig: operation.fieldConfig, + }); + + // Get all records for this model + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + // Add default value to existing records + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (!(operation.fieldName in record)) { + record[operation.fieldName] = operation.fieldConfig.default || null; + await this.updateRecord(operation.modelName, record); + modified++; + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute remove field operation + private async executeRemoveField( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.fieldName) { + throw new Error('Remove field operation requires fieldName'); + } + + this.logger.info(`Removing field ${operation.fieldName} from ${operation.modelName}`); + + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (operation.fieldName in record) { + delete record[operation.fieldName]; + await this.updateRecord(operation.modelName, record); + modified++; + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute modify field operation + private async executeModifyField( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.fieldName || !operation.fieldConfig) { + throw new Error('Modify field operation requires fieldName and fieldConfig'); + } + + this.logger.info(`Modifying field ${operation.fieldName} in ${operation.modelName}`, { + newConfig: operation.fieldConfig, + }); + + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (operation.fieldName in record) { + // Apply type conversion if needed + const oldValue = record[operation.fieldName]; + const newValue = this.convertFieldValue(oldValue, operation.fieldConfig); + + if (newValue !== oldValue) { + record[operation.fieldName] = newValue; + await this.updateRecord(operation.modelName, record); + modified++; + } + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute rename field operation + private async executeRenameField( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.fieldName || !operation.newFieldName) { + throw new Error('Rename field operation requires fieldName and newFieldName'); + } + + this.logger.info( + `Renaming field ${operation.fieldName} to ${operation.newFieldName} in ${operation.modelName}`, + ); + + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (operation.fieldName in record) { + record[operation.newFieldName] = record[operation.fieldName]; + delete record[operation.fieldName]; + await this.updateRecord(operation.modelName, record); + modified++; + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute data transformation operation + private async executeDataTransformation( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.transformer) { + throw new Error('Transform data operation requires transformer function'); + } + + this.logger.info(`Transforming data for ${operation.modelName}`); + + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + try { + const originalRecord = JSON.stringify(record); + const transformedRecord = await operation.transformer(record); + + if (JSON.stringify(transformedRecord) !== originalRecord) { + Object.assign(record, transformedRecord); + await this.updateRecord(operation.modelName, record); + modified++; + } + } catch (error: any) { + context.progress.errors.push(`Transform error for record ${record.id}: ${error.message}`); + context.progress.errorCount++; + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute custom operation + private async executeCustomOperation( + context: MigrationContext, + _options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.customOperation) { + throw new Error('Custom operation requires customOperation function'); + } + + this.logger.info(`Executing custom operation for ${operation.modelName}`); + + try { + await operation.customOperation(context); + return { processed: 1, modified: 1 }; // Custom operations handle their own counting + } catch (error: any) { + context.progress.errors.push(`Custom operation error: ${error.message}`); + throw error; + } + } + + // Helper methods for data access + private async getAllRecordsForModel(modelName: string): Promise<any[]> { + // In a real implementation, this would query all shards for the model + // For now, return empty array as placeholder + this.logger.debug(`Getting all records for model: ${modelName}`); + return []; + } + + private async updateRecord(modelName: string, record: any): Promise<void> { + // In a real implementation, this would update the record in the appropriate database + this.logger.debug(`Updating record in ${modelName}:`, { id: record.id }); + } + + private convertFieldValue(value: any, fieldConfig: FieldConfig): any { + // Convert value based on field configuration + switch (fieldConfig.type) { + case 'string': + return value != null ? String(value) : null; + case 'number': + return value != null ? Number(value) : null; + case 'boolean': + return value != null ? Boolean(value) : null; + case 'array': + return Array.isArray(value) ? value : [value]; + default: + return value; + } + } + + // Validation methods + private validateMigrationStructure(migration: Migration): void { + if (!migration.id || !migration.version || !migration.name) { + throw new Error('Migration must have id, version, and name'); + } + + if (!migration.targetModels || migration.targetModels.length === 0) { + throw new Error('Migration must specify target models'); + } + + if (!migration.up || migration.up.length === 0) { + throw new Error('Migration must have at least one up operation'); + } + + // Validate operations + for (const operation of migration.up) { + this.validateOperation(operation); + } + + if (migration.down) { + for (const operation of migration.down) { + this.validateOperation(operation); + } + } + } + + private validateOperation(operation: MigrationOperation): void { + const validTypes = [ + 'add_field', + 'remove_field', + 'modify_field', + 'rename_field', + 'add_index', + 'remove_index', + 'transform_data', + 'custom', + ]; + + if (!validTypes.includes(operation.type)) { + throw new Error(`Invalid operation type: ${operation.type}`); + } + + if (!operation.modelName) { + throw new Error('Operation must specify modelName'); + } + } + + private async validateDependencies(migration: Migration): Promise<void> { + if (!migration.dependencies) return; + + const appliedMigrations = this.getAppliedMigrations(); + const appliedIds = new Set(appliedMigrations.map((m) => m.migrationId)); + + for (const dependencyId of migration.dependencies) { + if (!appliedIds.has(dependencyId)) { + throw new Error(`Migration dependency not satisfied: ${dependencyId}`); + } + } + } + + private async runPreMigrationValidation(migration: Migration): Promise<void> { + if (!migration.validators) return; + + for (const validator of migration.validators) { + this.logger.debug(`Running pre-migration validator: ${validator.name}`); + + const context: MigrationContext = { + migration, + modelName: '', // Will be set per model + databaseManager: this.databaseManager, + shardManager: this.shardManager, + operation: migration.up[0], // First operation for context + progress: this.activeMigrations.get(migration.id)!, + logger: this.logger, + }; + + const result = await validator.validate(context); + if (!result.valid) { + throw new Error(`Pre-migration validation failed: ${result.errors.join(', ')}`); + } + + if (result.warnings.length > 0) { + context.progress.warnings.push(...result.warnings); + } + } + } + + private async runPostMigrationValidation(_migration: Migration): Promise<void> { + // Similar to pre-migration validation but runs after + this.logger.debug('Running post-migration validation'); + } + + // Rollback operations + private async executeRollback( + migration: Migration, + progress: MigrationProgress, + ): Promise<MigrationResult> { + if (!migration.down || migration.down.length === 0) { + throw new Error('Migration has no rollback operations defined'); + } + + const startTime = Date.now(); + let totalProcessed = 0; + let totalModified = 0; + + // Execute rollback operations in reverse order + for (const modelName of migration.targetModels) { + for (const operation of migration.down.reverse()) { + if (operation.modelName !== modelName) continue; + + const context: MigrationContext = { + migration, + modelName, + databaseManager: this.databaseManager, + shardManager: this.shardManager, + operation, + progress, + logger: this.logger, + }; + + const operationResult = await this.executeOperation(context, {}); + totalProcessed += operationResult.processed; + totalModified += operationResult.modified; + } + } + + return { + migrationId: migration.id, + success: true, + duration: Date.now() - startTime, + recordsProcessed: totalProcessed, + recordsModified: totalModified, + warnings: progress.warnings, + errors: progress.errors, + rollbackAvailable: false, + }; + } + + private async attemptRollback( + migration: Migration, + progress: MigrationProgress, + ): Promise<{ success: boolean }> { + try { + if (migration.down && migration.down.length > 0) { + await this.executeRollback(migration, progress); + progress.status = 'rolled_back'; + return { success: true }; + } + } catch (error: any) { + this.logger.error(`Rollback failed for migration ${migration.id}`, { error }); + } + + return { success: false }; + } + + // Dry run functionality + private async performDryRun(migration: Migration, _options: any): Promise<MigrationResult> { + this.logger.info(`Performing dry run for migration: ${migration.name}`); + + const startTime = Date.now(); + let estimatedRecords = 0; + + // Estimate the number of records that would be affected + for (const modelName of migration.targetModels) { + const modelRecords = await this.countRecordsForModel(modelName); + estimatedRecords += modelRecords; + } + + // Simulate operations without actually modifying data + for (const operation of migration.up) { + this.logger.debug(`Dry run operation: ${operation.type} on ${operation.modelName}`); + } + + return { + migrationId: migration.id, + success: true, + duration: Date.now() - startTime, + recordsProcessed: estimatedRecords, + recordsModified: estimatedRecords, // Estimate + warnings: ['This was a dry run - no data was actually modified'], + errors: [], + rollbackAvailable: migration.down.length > 0, + }; + } + + private async countRecordsForModel(_modelName: string): Promise<number> { + // In a real implementation, this would count records across all shards + return 0; + } + + // Migration history and state management + private getAppliedMigrations(_modelName?: string): MigrationResult[] { + const allResults: MigrationResult[] = []; + + for (const results of this.migrationHistory.values()) { + allResults.push(...results.filter((r) => r.success)); + } + + return allResults; + } + + private async recordMigrationResult(result: MigrationResult): Promise<void> { + if (!this.migrationHistory.has(result.migrationId)) { + this.migrationHistory.set(result.migrationId, []); + } + + this.migrationHistory.get(result.migrationId)!.push(result); + + // In a real implementation, this would persist to database + this.logger.debug('Recorded migration result', { result }); + } + + // Version comparison + private compareVersions(version1: string, version2: string): number { + const v1Parts = version1.split('.').map(Number); + const v2Parts = version2.split('.').map(Number); + + for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; + + if (v1Part < v2Part) return -1; + if (v1Part > v2Part) return 1; + } + + return 0; + } + + private updateMigrationOrder(): void { + const migrations = Array.from(this.migrations.values()); + this.migrationOrder = migrations + .sort((a, b) => this.compareVersions(a.version, b.version)) + .map((m) => m.id); + } + + // Utility methods + private createDefaultLogger(): MigrationLogger { + return { + info: (message: string, meta?: any) => console.log(`[MIGRATION INFO] ${message}`, meta || ''), + warn: (message: string, meta?: any) => + console.warn(`[MIGRATION WARN] ${message}`, meta || ''), + error: (message: string, meta?: any) => + console.error(`[MIGRATION ERROR] ${message}`, meta || ''), + debug: (message: string, meta?: any) => + console.log(`[MIGRATION DEBUG] ${message}`, meta || ''), + }; + } + + // Status and monitoring + getMigrationProgress(migrationId: string): MigrationProgress | null { + return this.activeMigrations.get(migrationId) || null; + } + + getActiveMigrations(): MigrationProgress[] { + return Array.from(this.activeMigrations.values()); + } + + getMigrationHistory(migrationId?: string): MigrationResult[] { + if (migrationId) { + return this.migrationHistory.get(migrationId) || []; + } + + const allResults: MigrationResult[] = []; + for (const results of this.migrationHistory.values()) { + allResults.push(...results); + } + + return allResults.sort((a, b) => b.duration - a.duration); + } + + // Cleanup and maintenance + async cleanup(): Promise<void> { + this.logger.info('Cleaning up migration manager'); + this.activeMigrations.clear(); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
MigrationBuilder.ts | +
+
+ |
+ 0% | +0/103 | +0% | +0/34 | +0% | +0/38 | +0% | +0/102 | +
MigrationManager.ts | +
+
+ |
+ 0% | +0/332 | +0% | +0/165 | +0% | +0/51 | +0% | +0/315 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { StoreType, ValidationResult, ShardingConfig, PinningConfig } from '../types/framework'; +import { FieldConfig, RelationshipConfig, ValidationError } from '../types/models'; +import { QueryBuilder } from '../query/QueryBuilder'; + +export abstract class BaseModel { + // Instance properties + public id: string = ''; + public createdAt: number = 0; + public updatedAt: number = 0; + public _loadedRelations: Map<string, any> = new Map(); + protected _isDirty: boolean = false; + protected _isNew: boolean = true; + + // Static properties for model configuration + static modelName: string; + static dbType: StoreType = 'docstore'; + static scope: 'user' | 'global' = 'global'; + static sharding?: ShardingConfig; + static pinning?: PinningConfig; + static fields: Map<string, FieldConfig> = new Map(); + static relationships: Map<string, RelationshipConfig> = new Map(); + static hooks: Map<string, Function[]> = new Map(); + + constructor(data: any = {}) { + this.fromJSON(data); + } + + // Core CRUD operations + async save(): Promise<this> { + await this.validate(); + + if (this._isNew) { + await this.beforeCreate(); + + // Generate ID if not provided + if (!this.id) { + this.id = this.generateId(); + } + + this.createdAt = Date.now(); + this.updatedAt = this.createdAt; + + // Save to database (will be implemented when database manager is ready) + await this._saveToDatabase(); + + this._isNew = false; + this._isDirty = false; + + await this.afterCreate(); + } else if (this._isDirty) { + await this.beforeUpdate(); + + this.updatedAt = Date.now(); + + // Update in database + await this._updateInDatabase(); + + this._isDirty = false; + + await this.afterUpdate(); + } + + return this; + } + + static async create<T extends BaseModel>(this: new (data?: any) => T, data: any): Promise<T> { + const instance = new this(data); + return await instance.save(); + } + + static async get<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + _id: string, + ): Promise<T | null> { + // Will be implemented when query system is ready + throw new Error('get method not yet implemented - requires query system'); + } + + static async find<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + id: string, + ): Promise<T> { + const result = await this.get(id); + if (!result) { + throw new Error(`${this.name} with id ${id} not found`); + } + return result; + } + + async update(data: Partial<this>): Promise<this> { + Object.assign(this, data); + this._isDirty = true; + return await this.save(); + } + + async delete(): Promise<boolean> { + await this.beforeDelete(); + + // Delete from database (will be implemented when database manager is ready) + const success = await this._deleteFromDatabase(); + + if (success) { + await this.afterDelete(); + } + + return success; + } + + // Query operations (return QueryBuilder instances) + static where<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + operator: string, + value: any, + ): QueryBuilder<T> { + return new QueryBuilder<T>(this as any).where(field, operator, value); + } + + static whereIn<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + values: any[], + ): QueryBuilder<T> { + return new QueryBuilder<T>(this as any).whereIn(field, values); + } + + static orderBy<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + direction: 'asc' | 'desc' = 'asc', + ): QueryBuilder<T> { + return new QueryBuilder<T>(this as any).orderBy(field, direction); + } + + static limit<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + count: number, + ): QueryBuilder<T> { + return new QueryBuilder<T>(this as any).limit(count); + } + + static async all<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + ): Promise<T[]> { + return await new QueryBuilder<T>(this as any).exec(); + } + + // Relationship operations + async load(relationships: string[]): Promise<this> { + const framework = this.getFrameworkInstance(); + if (!framework?.relationshipManager) { + console.warn('RelationshipManager not available, skipping relationship loading'); + return this; + } + + await framework.relationshipManager.eagerLoadRelationships([this], relationships); + return this; + } + + async loadRelation(relationName: string): Promise<any> { + // Check if already loaded + if (this._loadedRelations.has(relationName)) { + return this._loadedRelations.get(relationName); + } + + const framework = this.getFrameworkInstance(); + if (!framework?.relationshipManager) { + console.warn('RelationshipManager not available, cannot load relationship'); + return null; + } + + return await framework.relationshipManager.loadRelationship(this, relationName); + } + + // Advanced relationship loading methods + async loadRelationWithConstraints( + relationName: string, + constraints: (query: any) => any, + ): Promise<any> { + const framework = this.getFrameworkInstance(); + if (!framework?.relationshipManager) { + console.warn('RelationshipManager not available, cannot load relationship'); + return null; + } + + return await framework.relationshipManager.loadRelationship(this, relationName, { + constraints, + }); + } + + async reloadRelation(relationName: string): Promise<any> { + // Clear cached relationship + this._loadedRelations.delete(relationName); + + const framework = this.getFrameworkInstance(); + if (framework?.relationshipManager) { + framework.relationshipManager.invalidateRelationshipCache(this, relationName); + } + + return await this.loadRelation(relationName); + } + + getLoadedRelations(): string[] { + return Array.from(this._loadedRelations.keys()); + } + + isRelationLoaded(relationName: string): boolean { + return this._loadedRelations.has(relationName); + } + + getRelation(relationName: string): any { + return this._loadedRelations.get(relationName); + } + + setRelation(relationName: string, value: any): void { + this._loadedRelations.set(relationName, value); + } + + clearRelation(relationName: string): void { + this._loadedRelations.delete(relationName); + } + + // Serialization + toJSON(): any { + const result: any = {}; + + // Include all enumerable properties + for (const key in this) { + if (this.hasOwnProperty(key) && !key.startsWith('_')) { + result[key] = (this as any)[key]; + } + } + + // Include loaded relations + this._loadedRelations.forEach((value, key) => { + result[key] = value; + }); + + return result; + } + + fromJSON(data: any): this { + if (!data) return this; + + // Set basic properties + Object.keys(data).forEach((key) => { + if (key !== '_loadedRelations' && key !== '_isDirty' && key !== '_isNew') { + (this as any)[key] = data[key]; + } + }); + + // Mark as existing if it has an ID + if (this.id) { + this._isNew = false; + } + + return this; + } + + // Validation + async validate(): Promise<ValidationResult> { + const errors: string[] = []; + const modelClass = this.constructor as typeof BaseModel; + + // Validate each field + for (const [fieldName, fieldConfig] of modelClass.fields) { + const value = (this as any)[fieldName]; + const fieldErrors = this.validateField(fieldName, value, fieldConfig); + errors.push(...fieldErrors); + } + + const result = { valid: errors.length === 0, errors }; + + if (!result.valid) { + throw new ValidationError(errors); + } + + return result; + } + + private validateField(fieldName: string, value: any, config: FieldConfig): string[] { + const errors: string[] = []; + + // Required validation + if (config.required && (value === undefined || value === null || value === '')) { + errors.push(`${fieldName} is required`); + return errors; // No point in further validation if required field is missing + } + + // Skip further validation if value is empty and not required + if (value === undefined || value === null) { + return errors; + } + + // Type validation + if (!this.isValidType(value, config.type)) { + errors.push(`${fieldName} must be of type ${config.type}`); + } + + // Custom validation + if (config.validate) { + const customResult = config.validate(value); + if (customResult === false) { + errors.push(`${fieldName} failed custom validation`); + } else if (typeof customResult === 'string') { + errors.push(customResult); + } + } + + return errors; + } + + private isValidType(value: any, expectedType: FieldConfig['type']): boolean { + switch (expectedType) { + case 'string': + return typeof value === 'string'; + case 'number': + return typeof value === 'number' && !isNaN(value); + case 'boolean': + return typeof value === 'boolean'; + case 'array': + return Array.isArray(value); + case 'object': + return typeof value === 'object' && !Array.isArray(value); + case 'date': + return value instanceof Date || (typeof value === 'number' && !isNaN(value)); + default: + return true; + } + } + + // Hook methods (can be overridden by subclasses) + async beforeCreate(): Promise<void> { + await this.runHooks('beforeCreate'); + } + + async afterCreate(): Promise<void> { + await this.runHooks('afterCreate'); + } + + async beforeUpdate(): Promise<void> { + await this.runHooks('beforeUpdate'); + } + + async afterUpdate(): Promise<void> { + await this.runHooks('afterUpdate'); + } + + async beforeDelete(): Promise<void> { + await this.runHooks('beforeDelete'); + } + + async afterDelete(): Promise<void> { + await this.runHooks('afterDelete'); + } + + private async runHooks(hookName: string): Promise<void> { + const modelClass = this.constructor as typeof BaseModel; + const hooks = modelClass.hooks.get(hookName) || []; + + for (const hook of hooks) { + await hook.call(this); + } + } + + // Utility methods + private generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + } + + // Database operations integrated with DatabaseManager + private async _saveToDatabase(): Promise<void> { + const framework = this.getFrameworkInstance(); + if (!framework) { + console.warn('Framework not initialized, skipping database save'); + return; + } + + const modelClass = this.constructor as typeof BaseModel; + + try { + if (modelClass.scope === 'user') { + // For user-scoped models, we need a userId + const userId = (this as any).userId; + if (!userId) { + throw new Error('User-scoped models must have a userId field'); + } + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.addDocument(database, modelClass.dbType, this.toJSON()); + } else { + // For global models + if (modelClass.sharding) { + // Use sharded database + const shard = framework.shardManager.getShardForKey(modelClass.modelName, this.id); + await framework.databaseManager.addDocument( + shard.database, + modelClass.dbType, + this.toJSON(), + ); + } else { + // Use single global database + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.addDocument(database, modelClass.dbType, this.toJSON()); + } + } + } catch (error) { + console.error('Failed to save to database:', error); + throw error; + } + } + + private async _updateInDatabase(): Promise<void> { + const framework = this.getFrameworkInstance(); + if (!framework) { + console.warn('Framework not initialized, skipping database update'); + return; + } + + const modelClass = this.constructor as typeof BaseModel; + + try { + if (modelClass.scope === 'user') { + const userId = (this as any).userId; + if (!userId) { + throw new Error('User-scoped models must have a userId field'); + } + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.updateDocument( + database, + modelClass.dbType, + this.id, + this.toJSON(), + ); + } else { + if (modelClass.sharding) { + const shard = framework.shardManager.getShardForKey(modelClass.modelName, this.id); + await framework.databaseManager.updateDocument( + shard.database, + modelClass.dbType, + this.id, + this.toJSON(), + ); + } else { + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.updateDocument( + database, + modelClass.dbType, + this.id, + this.toJSON(), + ); + } + } + } catch (error) { + console.error('Failed to update in database:', error); + throw error; + } + } + + private async _deleteFromDatabase(): Promise<boolean> { + const framework = this.getFrameworkInstance(); + if (!framework) { + console.warn('Framework not initialized, skipping database delete'); + return false; + } + + const modelClass = this.constructor as typeof BaseModel; + + try { + if (modelClass.scope === 'user') { + const userId = (this as any).userId; + if (!userId) { + throw new Error('User-scoped models must have a userId field'); + } + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.deleteDocument(database, modelClass.dbType, this.id); + } else { + if (modelClass.sharding) { + const shard = framework.shardManager.getShardForKey(modelClass.modelName, this.id); + await framework.databaseManager.deleteDocument( + shard.database, + modelClass.dbType, + this.id, + ); + } else { + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.deleteDocument(database, modelClass.dbType, this.id); + } + } + return true; + } catch (error) { + console.error('Failed to delete from database:', error); + throw error; + } + } + + private getFrameworkInstance(): any { + // This will be properly typed when DebrosFramework is created + return (globalThis as any).__debrosFramework; + } + + // Static methods for framework integration + static setStore(store: any): void { + (this as any)._store = store; + } + + static setShards(shards: any[]): void { + (this as any)._shards = shards; + } + + static getStore(): any { + return (this as any)._store; + } + + static getShards(): any[] { + return (this as any)._shards || []; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { FieldConfig, ValidationError } from '../../types/models'; + +export function Field(config: FieldConfig) { + return function (target: any, propertyKey: string) { + // Initialize fields map if it doesn't exist + if (!target.constructor.fields) { + target.constructor.fields = new Map(); + } + + // Store field configuration + target.constructor.fields.set(propertyKey, config); + + // Create getter/setter with validation and transformation + const privateKey = `_${propertyKey}`; + + // Store the current descriptor (if any) - for future use + const _currentDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey); + + Object.defineProperty(target, propertyKey, { + get() { + return this[privateKey]; + }, + set(value) { + // Apply transformation first + const transformedValue = config.transform ? config.transform(value) : value; + + // Validate the field value + const validationResult = validateFieldValue(transformedValue, config, propertyKey); + if (!validationResult.valid) { + throw new ValidationError(validationResult.errors); + } + + // Set the value and mark as dirty + this[privateKey] = transformedValue; + if (this._isDirty !== undefined) { + this._isDirty = true; + } + }, + enumerable: true, + configurable: true, + }); + + // Set default value if provided + if (config.default !== undefined) { + Object.defineProperty(target, privateKey, { + value: config.default, + writable: true, + enumerable: false, + configurable: true, + }); + } + }; +} + +function validateFieldValue( + value: any, + config: FieldConfig, + fieldName: string, +): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + // Required validation + if (config.required && (value === undefined || value === null || value === '')) { + errors.push(`${fieldName} is required`); + return { valid: false, errors }; + } + + // Skip further validation if value is empty and not required + if (value === undefined || value === null) { + return { valid: true, errors: [] }; + } + + // Type validation + if (!isValidType(value, config.type)) { + errors.push(`${fieldName} must be of type ${config.type}`); + } + + // Custom validation + if (config.validate) { + const customResult = config.validate(value); + if (customResult === false) { + errors.push(`${fieldName} failed custom validation`); + } else if (typeof customResult === 'string') { + errors.push(customResult); + } + } + + return { valid: errors.length === 0, errors }; +} + +function isValidType(value: any, expectedType: FieldConfig['type']): boolean { + switch (expectedType) { + case 'string': + return typeof value === 'string'; + case 'number': + return typeof value === 'number' && !isNaN(value); + case 'boolean': + return typeof value === 'boolean'; + case 'array': + return Array.isArray(value); + case 'object': + return typeof value === 'object' && !Array.isArray(value); + case 'date': + return value instanceof Date || (typeof value === 'number' && !isNaN(value)); + default: + return true; + } +} + +// Utility function to get field configuration +export function getFieldConfig(target: any, propertyKey: string): FieldConfig | undefined { + if (!target.constructor.fields) { + return undefined; + } + return target.constructor.fields.get(propertyKey); +} + +// Export the decorator type for TypeScript +export type FieldDecorator = (config: FieldConfig) => (target: any, propertyKey: string) => void; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../BaseModel'; +import { ModelConfig } from '../../types/models'; +import { StoreType } from '../../types/framework'; +import { ModelRegistry } from '../../core/ModelRegistry'; + +export function Model(config: ModelConfig = {}) { + return function <T extends typeof BaseModel>(target: T): T { + // Set model configuration on the class + target.modelName = config.tableName || target.name; + target.dbType = config.type || autoDetectType(target); + target.scope = config.scope || 'global'; + target.sharding = config.sharding; + target.pinning = config.pinning; + + // Register with framework + ModelRegistry.register(target.name, target, config); + + // TODO: Set up automatic database creation when DatabaseManager is ready + // DatabaseManager.scheduleCreation(target); + + return target; + }; +} + +function autoDetectType(modelClass: typeof BaseModel): StoreType { + // Analyze model fields to suggest optimal database type + const fields = modelClass.fields; + + if (!fields || fields.size === 0) { + return 'docstore'; // Default for complex objects + } + + let hasComplexFields = false; + let _hasSimpleFields = false; + + for (const [_fieldName, fieldConfig] of fields) { + if (fieldConfig.type === 'object' || fieldConfig.type === 'array') { + hasComplexFields = true; + } else { + _hasSimpleFields = true; + } + } + + // If we have complex fields, use docstore + if (hasComplexFields) { + return 'docstore'; + } + + // If we only have simple fields, we could use keyvalue + // But docstore is more flexible, so let's default to that + return 'docstore'; +} + +// Export the decorator type for TypeScript +export type ModelDecorator = (config?: ModelConfig) => <T extends typeof BaseModel>(target: T) => T; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export function BeforeCreate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'beforeCreate', descriptor.value); +} + +export function AfterCreate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'afterCreate', descriptor.value); +} + +export function BeforeUpdate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'beforeUpdate', descriptor.value); +} + +export function AfterUpdate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'afterUpdate', descriptor.value); +} + +export function BeforeDelete(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'beforeDelete', descriptor.value); +} + +export function AfterDelete(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'afterDelete', descriptor.value); +} + +export function BeforeSave(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'beforeSave', descriptor.value); +} + +export function AfterSave(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'afterSave', descriptor.value); +} + +function registerHook(target: any, hookName: string, hookFunction: Function): void { + // Initialize hooks map if it doesn't exist + if (!target.constructor.hooks) { + target.constructor.hooks = new Map(); + } + + // Get existing hooks for this hook name + const existingHooks = target.constructor.hooks.get(hookName) || []; + + // Add the new hook + existingHooks.push(hookFunction); + + // Store updated hooks array + target.constructor.hooks.set(hookName, existingHooks); + + console.log(`Registered ${hookName} hook for ${target.constructor.name}`); +} + +// Utility function to get hooks for a specific event +export function getHooks(target: any, hookName: string): Function[] { + if (!target.constructor.hooks) { + return []; + } + return target.constructor.hooks.get(hookName) || []; +} + +// Export decorator types for TypeScript +export type HookDecorator = ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, +) => void; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
Field.ts | +
+
+ |
+ 0% | +0/43 | +0% | +0/44 | +0% | +0/7 | +0% | +0/43 | +
Model.ts | +
+
+ |
+ 0% | +0/20 | +0% | +0/17 | +0% | +0/3 | +0% | +0/20 | +
hooks.ts | +
+
+ |
+ 0% | +0/17 | +0% | +0/8 | +0% | +0/10 | +0% | +0/17 | +
relationships.ts | +
+
+ |
+ 0% | +0/33 | +0% | +0/24 | +0% | +0/13 | +0% | +0/33 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../BaseModel'; +import { RelationshipConfig } from '../../types/models'; + +export function BelongsTo( + model: typeof BaseModel, + foreignKey: string, + options: { localKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'belongsTo', + model, + foreignKey, + localKey: options.localKey || 'id', + lazy: true, + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function HasMany( + model: typeof BaseModel, + foreignKey: string, + options: { localKey?: string; through?: typeof BaseModel } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'hasMany', + model, + foreignKey, + localKey: options.localKey || 'id', + through: options.through, + lazy: true, + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function HasOne( + model: typeof BaseModel, + foreignKey: string, + options: { localKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'hasOne', + model, + foreignKey, + localKey: options.localKey || 'id', + lazy: true, + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function ManyToMany( + model: typeof BaseModel, + through: typeof BaseModel, + foreignKey: string, + options: { localKey?: string; throughForeignKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'manyToMany', + model, + foreignKey, + localKey: options.localKey || 'id', + through, + lazy: true, + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +function registerRelationship(target: any, propertyKey: string, config: RelationshipConfig): void { + // Initialize relationships map if it doesn't exist + if (!target.constructor.relationships) { + target.constructor.relationships = new Map(); + } + + // Store relationship configuration + target.constructor.relationships.set(propertyKey, config); + + console.log( + `Registered ${config.type} relationship: ${target.constructor.name}.${propertyKey} -> ${config.model.name}`, + ); +} + +function createRelationshipProperty( + target: any, + propertyKey: string, + config: RelationshipConfig, +): void { + const _relationshipKey = `_relationship_${propertyKey}`; // For future use + + Object.defineProperty(target, propertyKey, { + get() { + // Check if relationship is already loaded + if (this._loadedRelations && this._loadedRelations.has(propertyKey)) { + return this._loadedRelations.get(propertyKey); + } + + if (config.lazy) { + // Return a promise for lazy loading + return this.loadRelation(propertyKey); + } else { + throw new Error( + `Relationship '${propertyKey}' not loaded. Use .load(['${propertyKey}']) first.`, + ); + } + }, + set(value) { + // Allow manual setting of relationship values + if (!this._loadedRelations) { + this._loadedRelations = new Map(); + } + this._loadedRelations.set(propertyKey, value); + }, + enumerable: true, + configurable: true, + }); +} + +// Utility function to get relationship configuration +export function getRelationshipConfig( + target: any, + propertyKey: string, +): RelationshipConfig | undefined { + if (!target.constructor.relationships) { + return undefined; + } + return target.constructor.relationships.get(propertyKey); +} + +// Type definitions for decorators +export type BelongsToDecorator = ( + model: typeof BaseModel, + foreignKey: string, + options?: { localKey?: string }, +) => (target: any, propertyKey: string) => void; + +export type HasManyDecorator = ( + model: typeof BaseModel, + foreignKey: string, + options?: { localKey?: string; through?: typeof BaseModel }, +) => (target: any, propertyKey: string) => void; + +export type HasOneDecorator = ( + model: typeof BaseModel, + foreignKey: string, + options?: { localKey?: string }, +) => (target: any, propertyKey: string) => void; + +export type ManyToManyDecorator = ( + model: typeof BaseModel, + through: typeof BaseModel, + foreignKey: string, + options?: { localKey?: string; throughForeignKey?: string }, +) => (target: any, propertyKey: string) => void; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
BaseModel.ts | +
+
+ |
+ 0% | +0/200 | +0% | +0/97 | +0% | +0/44 | +0% | +0/199 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * PinningManager - Automatic IPFS Pinning with Smart Strategies + * + * This class implements intelligent pinning strategies for IPFS content: + * - Fixed: Pin a fixed number of most important items + * - Popularity: Pin based on access frequency and recency + * - Size-based: Pin smaller items preferentially + * - Custom: User-defined pinning logic + * - Automatic cleanup of unpinned content + */ + +import { PinningStrategy, PinningStats } from '../types/framework'; + +// Node.js types for compatibility +declare global { + namespace NodeJS { + interface Timeout {} + } +} + +export interface PinningRule { + modelName: string; + strategy?: PinningStrategy; + factor?: number; + maxPins?: number; + minAccessCount?: number; + maxAge?: number; // in milliseconds + customLogic?: (item: any, stats: any) => number; // returns priority score +} + +export interface PinnedItem { + hash: string; + modelName: string; + itemId: string; + pinnedAt: number; + lastAccessed: number; + accessCount: number; + size: number; + priority: number; + metadata?: any; +} + +export interface PinningMetrics { + totalPinned: number; + totalSize: number; + averageSize: number; + oldestPin: number; + newestPin: number; + mostAccessed: PinnedItem | null; + leastAccessed: PinnedItem | null; + strategyBreakdown: Map<PinningStrategy, number>; +} + +export class PinningManager { + private ipfsService: any; + private pinnedItems: Map<string, PinnedItem> = new Map(); + private pinningRules: Map<string, PinningRule> = new Map(); + private accessLog: Map<string, { count: number; lastAccess: number }> = new Map(); + private cleanupInterval: NodeJS.Timeout | null = null; + private maxTotalPins: number = 10000; + private maxTotalSize: number = 10 * 1024 * 1024 * 1024; // 10GB + private cleanupIntervalMs: number = 60000; // 1 minute + + constructor( + ipfsService: any, + options: { + maxTotalPins?: number; + maxTotalSize?: number; + cleanupIntervalMs?: number; + } = {}, + ) { + this.ipfsService = ipfsService; + this.maxTotalPins = options.maxTotalPins || this.maxTotalPins; + this.maxTotalSize = options.maxTotalSize || this.maxTotalSize; + this.cleanupIntervalMs = options.cleanupIntervalMs || this.cleanupIntervalMs; + + // Start automatic cleanup + this.startAutoCleanup(); + } + + // Configure pinning rules for models + setPinningRule(modelName: string, rule: Partial<PinningRule>): void { + const existingRule = this.pinningRules.get(modelName); + const newRule: PinningRule = { + modelName, + strategy: 'popularity' as const, + factor: 1, + ...existingRule, + ...rule, + }; + + this.pinningRules.set(modelName, newRule); + console.log( + `📌 Set pinning rule for ${modelName}: ${newRule.strategy} (factor: ${newRule.factor})`, + ); + } + + // Pin content based on configured strategy + async pinContent( + hash: string, + modelName: string, + itemId: string, + metadata: any = {}, + ): Promise<boolean> { + try { + // Check if already pinned + if (this.pinnedItems.has(hash)) { + await this.recordAccess(hash); + return true; + } + + const rule = this.pinningRules.get(modelName); + if (!rule) { + console.warn(`No pinning rule found for model ${modelName}, skipping pin`); + return false; + } + + // Get content size + const size = await this.getContentSize(hash); + + // Calculate priority based on strategy + const priority = this.calculatePinningPriority(rule, metadata, size); + + // Check if we should pin based on priority and limits + const shouldPin = await this.shouldPinContent(rule, priority, size); + + if (!shouldPin) { + console.log( + `⏭️ Skipping pin for ${hash} (${modelName}): priority too low or limits exceeded`, + ); + return false; + } + + // Perform the actual pinning + await this.ipfsService.pin(hash); + + // Record the pinned item + const pinnedItem: PinnedItem = { + hash, + modelName, + itemId, + pinnedAt: Date.now(), + lastAccessed: Date.now(), + accessCount: 1, + size, + priority, + metadata, + }; + + this.pinnedItems.set(hash, pinnedItem); + this.recordAccess(hash); + + console.log( + `📌 Pinned ${hash} (${modelName}:${itemId}) with priority ${priority.toFixed(2)}`, + ); + + // Cleanup if we've exceeded limits + await this.enforceGlobalLimits(); + + return true; + } catch (error) { + console.error(`Failed to pin ${hash}:`, error); + return false; + } + } + + // Unpin content + async unpinContent(hash: string, force: boolean = false): Promise<boolean> { + try { + const pinnedItem = this.pinnedItems.get(hash); + if (!pinnedItem) { + console.warn(`Hash ${hash} is not tracked as pinned`); + return false; + } + + // Check if content should be protected from unpinning + if (!force && (await this.isProtectedFromUnpinning(pinnedItem))) { + console.log(`🔒 Content ${hash} is protected from unpinning`); + return false; + } + + await this.ipfsService.unpin(hash); + this.pinnedItems.delete(hash); + this.accessLog.delete(hash); + + console.log(`📌❌ Unpinned ${hash} (${pinnedItem.modelName}:${pinnedItem.itemId})`); + return true; + } catch (error) { + console.error(`Failed to unpin ${hash}:`, error); + return false; + } + } + + // Record access to pinned content + async recordAccess(hash: string): Promise<void> { + const pinnedItem = this.pinnedItems.get(hash); + if (pinnedItem) { + pinnedItem.lastAccessed = Date.now(); + pinnedItem.accessCount++; + } + + // Update access log + const accessInfo = this.accessLog.get(hash) || { count: 0, lastAccess: 0 }; + accessInfo.count++; + accessInfo.lastAccess = Date.now(); + this.accessLog.set(hash, accessInfo); + } + + // Calculate pinning priority based on strategy + private calculatePinningPriority(rule: PinningRule, metadata: any, size: number): number { + const now = Date.now(); + let priority = 0; + + switch (rule.strategy || 'popularity') { + case 'fixed': + // Fixed strategy: all items have equal priority + priority = rule.factor || 1; + break; + + case 'popularity': + // Popularity-based: recent access + total access count + const accessInfo = this.accessLog.get(metadata.hash) || { count: 0, lastAccess: 0 }; + const recencyScore = Math.max(0, 1 - (now - accessInfo.lastAccess) / (24 * 60 * 60 * 1000)); // 24h decay + const accessScore = Math.min(1, accessInfo.count / 100); // Cap at 100 accesses + priority = (recencyScore * 0.6 + accessScore * 0.4) * (rule.factor || 1); + break; + + case 'size': + // Size-based: prefer smaller content (inverse relationship) + const maxSize = 100 * 1024 * 1024; // 100MB + const sizeScore = Math.max(0.1, 1 - size / maxSize); + priority = sizeScore * (rule.factor || 1); + break; + + case 'age': + // Age-based: prefer newer content + const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days + const age = now - (metadata.createdAt || now); + const ageScore = Math.max(0.1, 1 - age / maxAge); + priority = ageScore * (rule.factor || 1); + break; + + case 'custom': + // Custom logic provided by user + if (rule.customLogic) { + priority = + rule.customLogic(metadata, { + size, + accessInfo: this.accessLog.get(metadata.hash), + now, + }) * (rule.factor || 1); + } else { + priority = rule.factor || 1; + } + break; + + default: + priority = rule.factor || 1; + } + + return Math.max(0, priority); + } + + // Determine if content should be pinned + private async shouldPinContent( + rule: PinningRule, + priority: number, + size: number, + ): Promise<boolean> { + // Check rule-specific limits + if (rule.maxPins) { + const currentPinsForModel = Array.from(this.pinnedItems.values()).filter( + (item) => item.modelName === rule.modelName, + ).length; + + if (currentPinsForModel >= rule.maxPins) { + // Find lowest priority item for this model to potentially replace + const lowestPriorityItem = Array.from(this.pinnedItems.values()) + .filter((item) => item.modelName === rule.modelName) + .sort((a, b) => a.priority - b.priority)[0]; + + if (!lowestPriorityItem || priority <= lowestPriorityItem.priority) { + return false; + } + + // Unpin the lowest priority item to make room + await this.unpinContent(lowestPriorityItem.hash, true); + } + } + + // Check global limits + const metrics = this.getMetrics(); + + if (metrics.totalPinned >= this.maxTotalPins) { + // Find globally lowest priority item to replace + const lowestPriorityItem = Array.from(this.pinnedItems.values()).sort( + (a, b) => a.priority - b.priority, + )[0]; + + if (!lowestPriorityItem || priority <= lowestPriorityItem.priority) { + return false; + } + + await this.unpinContent(lowestPriorityItem.hash, true); + } + + if (metrics.totalSize + size > this.maxTotalSize) { + // Need to free up space + const spaceNeeded = metrics.totalSize + size - this.maxTotalSize; + await this.freeUpSpace(spaceNeeded); + } + + return true; + } + + // Check if content is protected from unpinning + private async isProtectedFromUnpinning(pinnedItem: PinnedItem): Promise<boolean> { + const rule = this.pinningRules.get(pinnedItem.modelName); + if (!rule) return false; + + // Recently accessed content is protected + const timeSinceAccess = Date.now() - pinnedItem.lastAccessed; + if (timeSinceAccess < 60 * 60 * 1000) { + // 1 hour + return true; + } + + // High-priority content is protected + if (pinnedItem.priority > 0.8) { + return true; + } + + // Content with high access count is protected + if (pinnedItem.accessCount > 50) { + return true; + } + + return false; + } + + // Free up space by unpinning least important content + private async freeUpSpace(spaceNeeded: number): Promise<void> { + let freedSpace = 0; + + // Sort by priority (lowest first) + const sortedItems = Array.from(this.pinnedItems.values()) + .filter((item) => !this.isProtectedFromUnpinning(item)) + .sort((a, b) => a.priority - b.priority); + + for (const item of sortedItems) { + if (freedSpace >= spaceNeeded) break; + + await this.unpinContent(item.hash, true); + freedSpace += item.size; + } + + console.log(`🧹 Freed up ${(freedSpace / 1024 / 1024).toFixed(2)} MB of space`); + } + + // Enforce global pinning limits + private async enforceGlobalLimits(): Promise<void> { + const metrics = this.getMetrics(); + + // Check total pins limit + if (metrics.totalPinned > this.maxTotalPins) { + const excess = metrics.totalPinned - this.maxTotalPins; + const itemsToUnpin = Array.from(this.pinnedItems.values()) + .sort((a, b) => a.priority - b.priority) + .slice(0, excess); + + for (const item of itemsToUnpin) { + await this.unpinContent(item.hash, true); + } + } + + // Check total size limit + if (metrics.totalSize > this.maxTotalSize) { + const excessSize = metrics.totalSize - this.maxTotalSize; + await this.freeUpSpace(excessSize); + } + } + + // Automatic cleanup of old/unused pins + private async performCleanup(): Promise<void> { + const now = Date.now(); + const itemsToCleanup: PinnedItem[] = []; + + for (const item of this.pinnedItems.values()) { + const rule = this.pinningRules.get(item.modelName); + if (!rule) continue; + + let shouldCleanup = false; + + // Age-based cleanup + if (rule.maxAge) { + const age = now - item.pinnedAt; + if (age > rule.maxAge) { + shouldCleanup = true; + } + } + + // Access-based cleanup + if (rule.minAccessCount) { + if (item.accessCount < rule.minAccessCount) { + shouldCleanup = true; + } + } + + // Inactivity-based cleanup (not accessed for 7 days) + const inactivityThreshold = 7 * 24 * 60 * 60 * 1000; + if (now - item.lastAccessed > inactivityThreshold && item.priority < 0.3) { + shouldCleanup = true; + } + + if (shouldCleanup && !(await this.isProtectedFromUnpinning(item))) { + itemsToCleanup.push(item); + } + } + + // Unpin items marked for cleanup + for (const item of itemsToCleanup) { + await this.unpinContent(item.hash, true); + } + + if (itemsToCleanup.length > 0) { + console.log(`🧹 Cleaned up ${itemsToCleanup.length} old/unused pins`); + } + } + + // Start automatic cleanup + private startAutoCleanup(): void { + this.cleanupInterval = setInterval(() => { + this.performCleanup().catch((error) => { + console.error('Cleanup failed:', error); + }); + }, this.cleanupIntervalMs); + } + + // Stop automatic cleanup + stopAutoCleanup(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval as any); + this.cleanupInterval = null; + } + } + + // Get content size from IPFS + private async getContentSize(hash: string): Promise<number> { + try { + const stats = await this.ipfsService.object.stat(hash); + return stats.CumulativeSize || stats.BlockSize || 0; + } catch (error) { + console.warn(`Could not get size for ${hash}:`, error); + return 1024; // Default size + } + } + + // Get comprehensive metrics + getMetrics(): PinningMetrics { + const items = Array.from(this.pinnedItems.values()); + const totalSize = items.reduce((sum, item) => sum + item.size, 0); + const strategyBreakdown = new Map<PinningStrategy, number>(); + + // Count by strategy + for (const item of items) { + const rule = this.pinningRules.get(item.modelName); + if (rule) { + const strategy = rule.strategy || 'popularity'; + const count = strategyBreakdown.get(strategy) || 0; + strategyBreakdown.set(strategy, count + 1); + } + } + + // Find most/least accessed + const sortedByAccess = items.sort((a, b) => b.accessCount - a.accessCount); + + return { + totalPinned: items.length, + totalSize, + averageSize: items.length > 0 ? totalSize / items.length : 0, + oldestPin: items.length > 0 ? Math.min(...items.map((i) => i.pinnedAt)) : 0, + newestPin: items.length > 0 ? Math.max(...items.map((i) => i.pinnedAt)) : 0, + mostAccessed: sortedByAccess[0] || null, + leastAccessed: sortedByAccess[sortedByAccess.length - 1] || null, + strategyBreakdown, + }; + } + + // Get pinning statistics + getStats(): PinningStats { + const metrics = this.getMetrics(); + return { + totalPinned: metrics.totalPinned, + totalSize: metrics.totalSize, + averageSize: metrics.averageSize, + strategies: Object.fromEntries(metrics.strategyBreakdown), + oldestPin: metrics.oldestPin, + recentActivity: this.getRecentActivity(), + }; + } + + // Get recent pinning activity + private getRecentActivity(): Array<{ action: string; hash: string; timestamp: number }> { + // This would typically be implemented with a proper activity log + // For now, we'll return recent pins + const recentItems = Array.from(this.pinnedItems.values()) + .filter((item) => Date.now() - item.pinnedAt < 24 * 60 * 60 * 1000) // Last 24 hours + .sort((a, b) => b.pinnedAt - a.pinnedAt) + .slice(0, 10) + .map((item) => ({ + action: 'pinned', + hash: item.hash, + timestamp: item.pinnedAt, + })); + + return recentItems; + } + + // Analyze pinning performance + analyzePerformance(): any { + const metrics = this.getMetrics(); + const now = Date.now(); + + // Calculate hit rate (items accessed recently) + const recentlyAccessedCount = Array.from(this.pinnedItems.values()).filter( + (item) => now - item.lastAccessed < 60 * 60 * 1000, + ).length; // Last hour + + const hitRate = metrics.totalPinned > 0 ? recentlyAccessedCount / metrics.totalPinned : 0; + + // Calculate average priority + const averagePriority = + Array.from(this.pinnedItems.values()).reduce((sum, item) => sum + item.priority, 0) / + metrics.totalPinned || 0; + + // Storage efficiency + const storageEfficiency = + this.maxTotalSize > 0 ? (this.maxTotalSize - metrics.totalSize) / this.maxTotalSize : 0; + + return { + hitRate, + averagePriority, + storageEfficiency, + utilizationRate: metrics.totalPinned / this.maxTotalPins, + averageItemAge: now - (metrics.oldestPin + metrics.newestPin) / 2, + totalRules: this.pinningRules.size, + accessDistribution: this.getAccessDistribution(), + }; + } + + // Get access distribution statistics + private getAccessDistribution(): any { + const items = Array.from(this.pinnedItems.values()); + const accessCounts = items.map((item) => item.accessCount).sort((a, b) => a - b); + + if (accessCounts.length === 0) { + return { min: 0, max: 0, median: 0, q1: 0, q3: 0 }; + } + + const min = accessCounts[0]; + const max = accessCounts[accessCounts.length - 1]; + const median = accessCounts[Math.floor(accessCounts.length / 2)]; + const q1 = accessCounts[Math.floor(accessCounts.length / 4)]; + const q3 = accessCounts[Math.floor((accessCounts.length * 3) / 4)]; + + return { min, max, median, q1, q3 }; + } + + // Get pinned items for a specific model + getPinnedItemsForModel(modelName: string): PinnedItem[] { + return Array.from(this.pinnedItems.values()).filter((item) => item.modelName === modelName); + } + + // Check if specific content is pinned + isPinned(hash: string): boolean { + return this.pinnedItems.has(hash); + } + + // Clear all pins (for testing/reset) + async clearAllPins(): Promise<void> { + const hashes = Array.from(this.pinnedItems.keys()); + + for (const hash of hashes) { + await this.unpinContent(hash, true); + } + + this.pinnedItems.clear(); + this.accessLog.clear(); + + console.log(`🧹 Cleared all ${hashes.length} pins`); + } + + // Shutdown + async shutdown(): Promise<void> { + this.stopAutoCleanup(); + console.log('📌 PinningManager shut down'); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
PinningManager.ts | +
+
+ |
+ 0% | +0/227 | +0% | +0/132 | +0% | +0/44 | +0% | +0/218 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * PubSubManager - Automatic Event Publishing and Subscription + * + * This class handles automatic publishing of model changes and database events + * to IPFS PubSub topics, enabling real-time synchronization across nodes: + * - Model-level events (create, update, delete) + * - Database-level events (replication, sync) + * - Custom application events + * - Topic management and subscription handling + * - Event filtering and routing + */ + +import { BaseModel } from '../models/BaseModel'; + +// Node.js types for compatibility +declare global { + namespace NodeJS { + interface Timeout {} + } +} + +export interface PubSubConfig { + enabled: boolean; + autoPublishModelEvents: boolean; + autoPublishDatabaseEvents: boolean; + topicPrefix: string; + maxRetries: number; + retryDelay: number; + eventBuffer: { + enabled: boolean; + maxSize: number; + flushInterval: number; + }; + compression: { + enabled: boolean; + threshold: number; // bytes + }; + encryption: { + enabled: boolean; + publicKey?: string; + privateKey?: string; + }; +} + +export interface PubSubEvent { + id: string; + type: string; + topic: string; + data: any; + timestamp: number; + source: string; + metadata?: any; +} + +export interface TopicSubscription { + topic: string; + handler: (event: PubSubEvent) => void | Promise<void>; + filter?: (event: PubSubEvent) => boolean; + options: { + autoAck: boolean; + maxRetries: number; + deadLetterTopic?: string; + }; +} + +export interface PubSubStats { + totalPublished: number; + totalReceived: number; + totalSubscriptions: number; + publishErrors: number; + receiveErrors: number; + averageLatency: number; + topicStats: Map< + string, + { + published: number; + received: number; + subscribers: number; + lastActivity: number; + } + >; +} + +export class PubSubManager { + private ipfsService: any; + private config: PubSubConfig; + private subscriptions: Map<string, TopicSubscription[]> = new Map(); + private eventBuffer: PubSubEvent[] = []; + private bufferFlushInterval: any = null; + private stats: PubSubStats; + private latencyMeasurements: number[] = []; + private nodeId: string; + private isInitialized: boolean = false; + private eventListeners: Map<string, Function[]> = new Map(); + + constructor(ipfsService: any, config: Partial<PubSubConfig> = {}) { + this.ipfsService = ipfsService; + this.nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + this.config = { + enabled: true, + autoPublishModelEvents: true, + autoPublishDatabaseEvents: true, + topicPrefix: 'debros', + maxRetries: 3, + retryDelay: 1000, + eventBuffer: { + enabled: true, + maxSize: 100, + flushInterval: 5000, + }, + compression: { + enabled: true, + threshold: 1024, + }, + encryption: { + enabled: false, + }, + ...config, + }; + + this.stats = { + totalPublished: 0, + totalReceived: 0, + totalSubscriptions: 0, + publishErrors: 0, + receiveErrors: 0, + averageLatency: 0, + topicStats: new Map(), + }; + } + + // Simple event emitter functionality + emit(event: string, ...args: any[]): boolean { + const listeners = this.eventListeners.get(event) || []; + listeners.forEach((listener) => { + try { + listener(...args); + } catch (error) { + console.error(`Error in event listener for ${event}:`, error); + } + }); + return listeners.length > 0; + } + + on(event: string, listener: Function): this { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, []); + } + this.eventListeners.get(event)!.push(listener); + return this; + } + + off(event: string, listener?: Function): this { + if (!listener) { + this.eventListeners.delete(event); + } else { + const listeners = this.eventListeners.get(event) || []; + const index = listeners.indexOf(listener); + if (index >= 0) { + listeners.splice(index, 1); + } + } + return this; + } + + // Initialize PubSub system + async initialize(): Promise<void> { + if (!this.config.enabled) { + console.log('📡 PubSub disabled in configuration'); + return; + } + + try { + console.log('📡 Initializing PubSubManager...'); + + // Start event buffer flushing if enabled + if (this.config.eventBuffer.enabled) { + this.startEventBuffering(); + } + + // Subscribe to model events if auto-publishing is enabled + if (this.config.autoPublishModelEvents) { + this.setupModelEventPublishing(); + } + + // Subscribe to database events if auto-publishing is enabled + if (this.config.autoPublishDatabaseEvents) { + this.setupDatabaseEventPublishing(); + } + + this.isInitialized = true; + console.log('✅ PubSubManager initialized successfully'); + } catch (error) { + console.error('❌ Failed to initialize PubSubManager:', error); + throw error; + } + } + + // Publish event to a topic + async publish( + topic: string, + data: any, + options: { + priority?: 'low' | 'normal' | 'high'; + retries?: number; + compress?: boolean; + encrypt?: boolean; + metadata?: any; + } = {}, + ): Promise<boolean> { + if (!this.config.enabled || !this.isInitialized) { + return false; + } + + const event: PubSubEvent = { + id: this.generateEventId(), + type: this.extractEventType(topic), + topic: this.prefixTopic(topic), + data, + timestamp: Date.now(), + source: this.nodeId, + metadata: options.metadata, + }; + + try { + // Process event (compression, encryption, etc.) + const processedData = await this.processEventForPublishing(event, options); + + // Publish with buffering or directly + if (this.config.eventBuffer.enabled && options.priority !== 'high') { + return this.bufferEvent(event, processedData); + } else { + return await this.publishDirect(event.topic, processedData, options.retries); + } + } catch (error) { + this.stats.publishErrors++; + console.error(`❌ Failed to publish to ${topic}:`, error); + this.emit('publishError', { topic, error, event }); + return false; + } + } + + // Subscribe to a topic + async subscribe( + topic: string, + handler: (event: PubSubEvent) => void | Promise<void>, + options: { + filter?: (event: PubSubEvent) => boolean; + autoAck?: boolean; + maxRetries?: number; + deadLetterTopic?: string; + } = {}, + ): Promise<boolean> { + if (!this.config.enabled || !this.isInitialized) { + return false; + } + + const fullTopic = this.prefixTopic(topic); + + try { + const subscription: TopicSubscription = { + topic: fullTopic, + handler, + filter: options.filter, + options: { + autoAck: options.autoAck !== false, + maxRetries: options.maxRetries || this.config.maxRetries, + deadLetterTopic: options.deadLetterTopic, + }, + }; + + // Add to subscriptions map + if (!this.subscriptions.has(fullTopic)) { + this.subscriptions.set(fullTopic, []); + + // Subscribe to IPFS PubSub topic + await this.ipfsService.pubsub.subscribe(fullTopic, (message: any) => { + this.handleIncomingMessage(fullTopic, message); + }); + } + + this.subscriptions.get(fullTopic)!.push(subscription); + this.stats.totalSubscriptions++; + + // Update topic stats + this.updateTopicStats(fullTopic, 'subscribers', 1); + + console.log(`📡 Subscribed to topic: ${fullTopic}`); + this.emit('subscribed', { topic: fullTopic, subscription }); + + return true; + } catch (error) { + console.error(`❌ Failed to subscribe to ${topic}:`, error); + this.emit('subscribeError', { topic, error }); + return false; + } + } + + // Unsubscribe from a topic + async unsubscribe(topic: string, handler?: Function): Promise<boolean> { + const fullTopic = this.prefixTopic(topic); + const subscriptions = this.subscriptions.get(fullTopic); + + if (!subscriptions) { + return false; + } + + try { + if (handler) { + // Remove specific handler + const index = subscriptions.findIndex((sub) => sub.handler === handler); + if (index >= 0) { + subscriptions.splice(index, 1); + this.stats.totalSubscriptions--; + } + } else { + // Remove all handlers for this topic + this.stats.totalSubscriptions -= subscriptions.length; + subscriptions.length = 0; + } + + // If no more subscriptions, unsubscribe from IPFS + if (subscriptions.length === 0) { + await this.ipfsService.pubsub.unsubscribe(fullTopic); + this.subscriptions.delete(fullTopic); + this.stats.topicStats.delete(fullTopic); + } + + console.log(`📡 Unsubscribed from topic: ${fullTopic}`); + this.emit('unsubscribed', { topic: fullTopic }); + + return true; + } catch (error) { + console.error(`❌ Failed to unsubscribe from ${topic}:`, error); + return false; + } + } + + // Setup automatic model event publishing + private setupModelEventPublishing(): void { + const topics = { + create: 'model.created', + update: 'model.updated', + delete: 'model.deleted', + save: 'model.saved', + }; + + // Listen for model events on the global framework instance + this.on('modelEvent', async (eventType: string, model: BaseModel, changes?: any) => { + const topic = topics[eventType as keyof typeof topics]; + if (!topic) return; + + const eventData = { + modelName: model.constructor.name, + modelId: model.id, + userId: (model as any).userId, + changes, + timestamp: Date.now(), + }; + + await this.publish(topic, eventData, { + priority: eventType === 'delete' ? 'high' : 'normal', + metadata: { + modelType: model.constructor.name, + scope: (model.constructor as any).scope, + }, + }); + }); + } + + // Setup automatic database event publishing + private setupDatabaseEventPublishing(): void { + const databaseTopics = { + replication: 'database.replicated', + sync: 'database.synced', + conflict: 'database.conflict', + error: 'database.error', + }; + + // Listen for database events + this.on('databaseEvent', async (eventType: string, data: any) => { + const topic = databaseTopics[eventType as keyof typeof databaseTopics]; + if (!topic) return; + + await this.publish(topic, data, { + priority: eventType === 'error' ? 'high' : 'normal', + metadata: { + eventType, + source: 'database', + }, + }); + }); + } + + // Handle incoming PubSub messages + private async handleIncomingMessage(topic: string, message: any): Promise<void> { + try { + const startTime = Date.now(); + + // Parse and validate message + const event = await this.processIncomingMessage(message); + if (!event) return; + + // Update stats + this.stats.totalReceived++; + this.updateTopicStats(topic, 'received', 1); + + // Calculate latency + const latency = Date.now() - event.timestamp; + this.latencyMeasurements.push(latency); + if (this.latencyMeasurements.length > 100) { + this.latencyMeasurements.shift(); + } + this.stats.averageLatency = + this.latencyMeasurements.reduce((a, b) => a + b, 0) / this.latencyMeasurements.length; + + // Route to subscribers + const subscriptions = this.subscriptions.get(topic) || []; + + for (const subscription of subscriptions) { + try { + // Apply filter if present + if (subscription.filter && !subscription.filter(event)) { + continue; + } + + // Call handler + await this.callHandlerWithRetry(subscription, event); + } catch (error: any) { + this.stats.receiveErrors++; + console.error(`❌ Handler error for ${topic}:`, error); + + // Send to dead letter topic if configured + if (subscription.options.deadLetterTopic) { + await this.publish(subscription.options.deadLetterTopic, { + originalTopic: topic, + originalEvent: event, + error: error?.message || String(error), + timestamp: Date.now(), + }); + } + } + } + + this.emit('messageReceived', { topic, event, processingTime: Date.now() - startTime }); + } catch (error) { + this.stats.receiveErrors++; + console.error(`❌ Failed to handle message from ${topic}:`, error); + this.emit('messageError', { topic, error }); + } + } + + // Call handler with retry logic + private async callHandlerWithRetry( + subscription: TopicSubscription, + event: PubSubEvent, + attempt: number = 1, + ): Promise<void> { + try { + await subscription.handler(event); + } catch (error) { + if (attempt < subscription.options.maxRetries) { + console.warn( + `🔄 Retrying handler (attempt ${attempt + 1}/${subscription.options.maxRetries})`, + ); + await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay * attempt)); + return this.callHandlerWithRetry(subscription, event, attempt + 1); + } + throw error; + } + } + + // Process event for publishing (compression, encryption, etc.) + private async processEventForPublishing(event: PubSubEvent, options: any): Promise<string> { + let data = JSON.stringify(event); + + // Compression + if ( + options.compress !== false && + this.config.compression.enabled && + data.length > this.config.compression.threshold + ) { + // In a real implementation, you'd use a compression library like zlib + // data = await compress(data); + } + + // Encryption + if ( + options.encrypt !== false && + this.config.encryption.enabled && + this.config.encryption.publicKey + ) { + // In a real implementation, you'd encrypt with the public key + // data = await encrypt(data, this.config.encryption.publicKey); + } + + return data; + } + + // Process incoming message + private async processIncomingMessage(message: any): Promise<PubSubEvent | null> { + try { + let data = message.data.toString(); + + // Decryption + if (this.config.encryption.enabled && this.config.encryption.privateKey) { + // In a real implementation, you'd decrypt with the private key + // data = await decrypt(data, this.config.encryption.privateKey); + } + + // Decompression + if (this.config.compression.enabled) { + // In a real implementation, you'd detect and decompress + // data = await decompress(data); + } + + const event = JSON.parse(data) as PubSubEvent; + + // Validate event structure + if (!event.id || !event.topic || !event.timestamp) { + console.warn('❌ Invalid event structure received'); + return null; + } + + // Ignore our own messages + if (event.source === this.nodeId) { + return null; + } + + return event; + } catch (error) { + console.error('❌ Failed to process incoming message:', error); + return null; + } + } + + // Direct publish without buffering + private async publishDirect( + topic: string, + data: string, + retries: number = this.config.maxRetries, + ): Promise<boolean> { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + await this.ipfsService.pubsub.publish(topic, data); + + this.stats.totalPublished++; + this.updateTopicStats(topic, 'published', 1); + + return true; + } catch (error) { + if (attempt === retries) { + throw error; + } + + console.warn(`🔄 Retrying publish (attempt ${attempt + 1}/${retries})`); + await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay * attempt)); + } + } + + return false; + } + + // Buffer event for batch publishing + private bufferEvent(event: PubSubEvent, _data: string): boolean { + if (this.eventBuffer.length >= this.config.eventBuffer.maxSize) { + // Buffer is full, flush immediately + this.flushEventBuffer(); + } + + this.eventBuffer.push(event); + return true; + } + + // Start event buffering + private startEventBuffering(): void { + this.bufferFlushInterval = setInterval(() => { + this.flushEventBuffer(); + }, this.config.eventBuffer.flushInterval); + } + + // Flush event buffer + private async flushEventBuffer(): Promise<void> { + if (this.eventBuffer.length === 0) return; + + const events = [...this.eventBuffer]; + this.eventBuffer.length = 0; + + console.log(`📡 Flushing ${events.length} buffered events`); + + // Group events by topic for efficiency + const eventsByTopic = new Map<string, PubSubEvent[]>(); + for (const event of events) { + if (!eventsByTopic.has(event.topic)) { + eventsByTopic.set(event.topic, []); + } + eventsByTopic.get(event.topic)!.push(event); + } + + // Publish batches + for (const [topic, topicEvents] of eventsByTopic) { + try { + for (const event of topicEvents) { + const data = await this.processEventForPublishing(event, {}); + await this.publishDirect(topic, data); + } + } catch (error) { + console.error(`❌ Failed to flush events for ${topic}:`, error); + this.stats.publishErrors += topicEvents.length; + } + } + } + + // Update topic statistics + private updateTopicStats( + topic: string, + metric: 'published' | 'received' | 'subscribers', + delta: number, + ): void { + if (!this.stats.topicStats.has(topic)) { + this.stats.topicStats.set(topic, { + published: 0, + received: 0, + subscribers: 0, + lastActivity: Date.now(), + }); + } + + const stats = this.stats.topicStats.get(topic)!; + stats[metric] += delta; + stats.lastActivity = Date.now(); + } + + // Utility methods + private generateEventId(): string { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + + private extractEventType(topic: string): string { + const parts = topic.split('.'); + return parts[parts.length - 1]; + } + + private prefixTopic(topic: string): string { + return `${this.config.topicPrefix}.${topic}`; + } + + // Get PubSub statistics + getStats(): PubSubStats { + return { ...this.stats }; + } + + // Get list of active topics + getActiveTopics(): string[] { + return Array.from(this.subscriptions.keys()); + } + + // Get subscribers for a topic + getTopicSubscribers(topic: string): number { + const fullTopic = this.prefixTopic(topic); + return this.subscriptions.get(fullTopic)?.length || 0; + } + + // Check if topic exists + hasSubscriptions(topic: string): boolean { + const fullTopic = this.prefixTopic(topic); + return this.subscriptions.has(fullTopic) && this.subscriptions.get(fullTopic)!.length > 0; + } + + // Clear all subscriptions + async clearAllSubscriptions(): Promise<void> { + const topics = Array.from(this.subscriptions.keys()); + + for (const topic of topics) { + try { + await this.ipfsService.pubsub.unsubscribe(topic); + } catch (error) { + console.error(`Failed to unsubscribe from ${topic}:`, error); + } + } + + this.subscriptions.clear(); + this.stats.topicStats.clear(); + this.stats.totalSubscriptions = 0; + + console.log(`📡 Cleared all ${topics.length} subscriptions`); + } + + // Shutdown + async shutdown(): Promise<void> { + console.log('📡 Shutting down PubSubManager...'); + + // Stop event buffering + if (this.bufferFlushInterval) { + clearInterval(this.bufferFlushInterval as any); + this.bufferFlushInterval = null; + } + + // Flush remaining events + await this.flushEventBuffer(); + + // Clear all subscriptions + await this.clearAllSubscriptions(); + + // Clear event listeners + this.eventListeners.clear(); + + this.isInitialized = false; + console.log('✅ PubSubManager shut down successfully'); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
PubSubManager.ts | +
+
+ |
+ 0% | +0/228 | +0% | +0/110 | +0% | +0/37 | +0% | +0/220 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { QueryCondition, SortConfig } from '../types/queries'; +import { QueryExecutor } from './QueryExecutor'; + +export class QueryBuilder<T extends BaseModel> { + private model: typeof BaseModel; + private conditions: QueryCondition[] = []; + private relations: string[] = []; + private sorting: SortConfig[] = []; + private limitation?: number; + private offsetValue?: number; + private groupByFields: string[] = []; + private havingConditions: QueryCondition[] = []; + private distinctFields: string[] = []; + + constructor(model: typeof BaseModel) { + this.model = model; + } + + // Basic filtering + where(field: string, operator: string, value: any): this { + this.conditions.push({ field, operator, value }); + return this; + } + + whereIn(field: string, values: any[]): this { + return this.where(field, 'in', values); + } + + whereNotIn(field: string, values: any[]): this { + return this.where(field, 'not_in', values); + } + + whereNull(field: string): this { + return this.where(field, 'is_null', null); + } + + whereNotNull(field: string): this { + return this.where(field, 'is_not_null', null); + } + + whereBetween(field: string, min: any, max: any): this { + return this.where(field, 'between', [min, max]); + } + + whereNot(field: string, operator: string, value: any): this { + return this.where(field, `not_${operator}`, value); + } + + whereLike(field: string, pattern: string): this { + return this.where(field, 'like', pattern); + } + + whereILike(field: string, pattern: string): this { + return this.where(field, 'ilike', pattern); + } + + // Date filtering + whereDate(field: string, operator: string, date: Date | string | number): this { + return this.where(field, `date_${operator}`, date); + } + + whereDateBetween( + field: string, + startDate: Date | string | number, + endDate: Date | string | number, + ): this { + return this.where(field, 'date_between', [startDate, endDate]); + } + + whereYear(field: string, year: number): this { + return this.where(field, 'year', year); + } + + whereMonth(field: string, month: number): this { + return this.where(field, 'month', month); + } + + whereDay(field: string, day: number): this { + return this.where(field, 'day', day); + } + + // User-specific filtering (for user-scoped queries) + whereUser(userId: string): this { + return this.where('userId', '=', userId); + } + + whereUserIn(userIds: string[]): this { + this.conditions.push({ + field: 'userId', + operator: 'userIn', + value: userIds, + }); + return this; + } + + // Advanced filtering with OR conditions + orWhere(callback: (query: QueryBuilder<T>) => void): this { + const subQuery = new QueryBuilder<T>(this.model); + callback(subQuery); + + this.conditions.push({ + field: '__or__', + operator: 'or', + value: subQuery.getConditions(), + }); + + return this; + } + + // Array and object field queries + whereArrayContains(field: string, value: any): this { + return this.where(field, 'array_contains', value); + } + + whereArrayLength(field: string, operator: string, length: number): this { + return this.where(field, `array_length_${operator}`, length); + } + + whereObjectHasKey(field: string, key: string): this { + return this.where(field, 'object_has_key', key); + } + + whereObjectPath(field: string, path: string, operator: string, value: any): this { + return this.where(field, `object_path_${operator}`, { path, value }); + } + + // Sorting + orderBy(field: string, direction: 'asc' | 'desc' = 'asc'): this { + this.sorting.push({ field, direction }); + return this; + } + + orderByDesc(field: string): this { + return this.orderBy(field, 'desc'); + } + + orderByRaw(expression: string): this { + this.sorting.push({ field: expression, direction: 'asc' }); + return this; + } + + // Multiple field sorting + orderByMultiple(sorts: Array<{ field: string; direction: 'asc' | 'desc' }>): this { + sorts.forEach((sort) => this.orderBy(sort.field, sort.direction)); + return this; + } + + // Pagination + limit(count: number): this { + this.limitation = count; + return this; + } + + offset(count: number): this { + this.offsetValue = count; + return this; + } + + skip(count: number): this { + return this.offset(count); + } + + take(count: number): this { + return this.limit(count); + } + + // Pagination helpers + page(pageNumber: number, pageSize: number): this { + this.limitation = pageSize; + this.offsetValue = (pageNumber - 1) * pageSize; + return this; + } + + // Relationship loading + load(relationships: string[]): this { + this.relations = [...this.relations, ...relationships]; + return this; + } + + with(relationships: string[]): this { + return this.load(relationships); + } + + loadNested(relationship: string, _callback: (query: QueryBuilder<any>) => void): this { + // For nested relationship loading with constraints + this.relations.push(relationship); + // Store callback for nested query (implementation in QueryExecutor) + return this; + } + + // Aggregation + groupBy(...fields: string[]): this { + this.groupByFields.push(...fields); + return this; + } + + having(field: string, operator: string, value: any): this { + this.havingConditions.push({ field, operator, value }); + return this; + } + + // Distinct + distinct(...fields: string[]): this { + this.distinctFields.push(...fields); + return this; + } + + // Execution methods + async exec(): Promise<T[]> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.execute(); + } + + async get(): Promise<T[]> { + return await this.exec(); + } + + async first(): Promise<T | null> { + const results = await this.limit(1).exec(); + return results[0] || null; + } + + async firstOrFail(): Promise<T> { + const result = await this.first(); + if (!result) { + throw new Error(`No ${this.model.name} found matching the query`); + } + return result; + } + + async find(id: string): Promise<T | null> { + return await this.where('id', '=', id).first(); + } + + async findOrFail(id: string): Promise<T> { + const result = await this.find(id); + if (!result) { + throw new Error(`${this.model.name} with id ${id} not found`); + } + return result; + } + + async count(): Promise<number> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.count(); + } + + async exists(): Promise<boolean> { + const count = await this.count(); + return count > 0; + } + + async sum(field: string): Promise<number> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.sum(field); + } + + async avg(field: string): Promise<number> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.avg(field); + } + + async min(field: string): Promise<any> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.min(field); + } + + async max(field: string): Promise<any> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.max(field); + } + + // Pagination with metadata + async paginate( + page: number = 1, + perPage: number = 15, + ): Promise<{ + data: T[]; + total: number; + perPage: number; + currentPage: number; + lastPage: number; + hasNextPage: boolean; + hasPrevPage: boolean; + }> { + const total = await this.count(); + const lastPage = Math.ceil(total / perPage); + + const data = await this.page(page, perPage).exec(); + + return { + data, + total, + perPage, + currentPage: page, + lastPage, + hasNextPage: page < lastPage, + hasPrevPage: page > 1, + }; + } + + // Chunked processing + async chunk( + size: number, + callback: (items: T[], page: number) => Promise<void | boolean>, + ): Promise<void> { + let page = 1; + let hasMore = true; + + while (hasMore) { + const items = await this.page(page, size).exec(); + + if (items.length === 0) { + break; + } + + const result = await callback(items, page); + + // If callback returns false, stop processing + if (result === false) { + break; + } + + hasMore = items.length === size; + page++; + } + } + + // Query optimization hints + useIndex(indexName: string): this { + // Hint for query optimizer (implementation in QueryExecutor) + (this as any)._indexHint = indexName; + return this; + } + + preferShard(shardIndex: number): this { + // Force query to specific shard (for global sharded models) + (this as any)._preferredShard = shardIndex; + return this; + } + + // Raw queries (for advanced users) + whereRaw(expression: string, bindings: any[] = []): this { + this.conditions.push({ + field: '__raw__', + operator: 'raw', + value: { expression, bindings }, + }); + return this; + } + + // Getters for query configuration (used by QueryExecutor) + getConditions(): QueryCondition[] { + return [...this.conditions]; + } + + getRelations(): string[] { + return [...this.relations]; + } + + getSorting(): SortConfig[] { + return [...this.sorting]; + } + + getLimit(): number | undefined { + return this.limitation; + } + + getOffset(): number | undefined { + return this.offsetValue; + } + + getGroupBy(): string[] { + return [...this.groupByFields]; + } + + getHaving(): QueryCondition[] { + return [...this.havingConditions]; + } + + getDistinct(): string[] { + return [...this.distinctFields]; + } + + getModel(): typeof BaseModel { + return this.model; + } + + // Clone query for reuse + clone(): QueryBuilder<T> { + const cloned = new QueryBuilder<T>(this.model); + cloned.conditions = [...this.conditions]; + cloned.relations = [...this.relations]; + cloned.sorting = [...this.sorting]; + cloned.limitation = this.limitation; + cloned.offsetValue = this.offsetValue; + cloned.groupByFields = [...this.groupByFields]; + cloned.havingConditions = [...this.havingConditions]; + cloned.distinctFields = [...this.distinctFields]; + + return cloned; + } + + // Debug methods + toSQL(): string { + // Generate SQL-like representation for debugging + let sql = `SELECT * FROM ${this.model.name}`; + + if (this.conditions.length > 0) { + const whereClause = this.conditions + .map((c) => `${c.field} ${c.operator} ${JSON.stringify(c.value)}`) + .join(' AND '); + sql += ` WHERE ${whereClause}`; + } + + if (this.sorting.length > 0) { + const orderClause = this.sorting + .map((s) => `${s.field} ${s.direction.toUpperCase()}`) + .join(', '); + sql += ` ORDER BY ${orderClause}`; + } + + if (this.limitation) { + sql += ` LIMIT ${this.limitation}`; + } + + if (this.offsetValue) { + sql += ` OFFSET ${this.offsetValue}`; + } + + return sql; + } + + explain(): any { + return { + model: this.model.name, + scope: this.model.scope, + conditions: this.conditions, + relations: this.relations, + sorting: this.sorting, + limit: this.limitation, + offset: this.offsetValue, + sql: this.toSQL(), + }; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { QueryBuilder } from './QueryBuilder'; +import { BaseModel } from '../models/BaseModel'; + +export interface CacheEntry<T> { + key: string; + data: T[]; + timestamp: number; + ttl: number; + hitCount: number; +} + +export interface CacheStats { + totalRequests: number; + cacheHits: number; + cacheMisses: number; + hitRate: number; + size: number; + maxSize: number; +} + +export class QueryCache { + private cache: Map<string, CacheEntry<any>> = new Map(); + private maxSize: number; + private defaultTTL: number; + private stats: CacheStats; + + constructor(maxSize: number = 1000, defaultTTL: number = 300000) { + // 5 minutes default + this.maxSize = maxSize; + this.defaultTTL = defaultTTL; + this.stats = { + totalRequests: 0, + cacheHits: 0, + cacheMisses: 0, + hitRate: 0, + size: 0, + maxSize, + }; + } + + generateKey<T extends BaseModel>(query: QueryBuilder<T>): string { + const model = query.getModel(); + const conditions = query.getConditions(); + const relations = query.getRelations(); + const sorting = query.getSorting(); + const limit = query.getLimit(); + const offset = query.getOffset(); + + // Create a deterministic cache key + const keyParts = [ + model.name, + model.scope, + JSON.stringify(conditions.sort((a, b) => a.field.localeCompare(b.field))), + JSON.stringify(relations.sort()), + JSON.stringify(sorting), + limit?.toString() || 'no-limit', + offset?.toString() || 'no-offset', + ]; + + // Create hash of the key parts + return this.hashString(keyParts.join('|')); + } + + async get<T extends BaseModel>(query: QueryBuilder<T>): Promise<T[] | null> { + this.stats.totalRequests++; + + const key = this.generateKey(query); + const entry = this.cache.get(key); + + if (!entry) { + this.stats.cacheMisses++; + this.updateHitRate(); + return null; + } + + // Check if entry has expired + if (Date.now() - entry.timestamp > entry.ttl) { + this.cache.delete(key); + this.stats.cacheMisses++; + this.updateHitRate(); + return null; + } + + // Update hit count and stats + entry.hitCount++; + this.stats.cacheHits++; + this.updateHitRate(); + + // Convert cached data back to model instances + const modelClass = query.getModel() as any; // Type assertion for abstract class + return entry.data.map((item) => new modelClass(item)); + } + + set<T extends BaseModel>(query: QueryBuilder<T>, data: T[], customTTL?: number): void { + const key = this.generateKey(query); + const ttl = customTTL || this.defaultTTL; + + // Serialize model instances to plain objects for caching + const serializedData = data.map((item) => item.toJSON()); + + const entry: CacheEntry<any> = { + key, + data: serializedData, + timestamp: Date.now(), + ttl, + hitCount: 0, + }; + + // Check if we need to evict entries + if (this.cache.size >= this.maxSize) { + this.evictLeastUsed(); + } + + this.cache.set(key, entry); + this.stats.size = this.cache.size; + } + + invalidate<T extends BaseModel>(query: QueryBuilder<T>): boolean { + const key = this.generateKey(query); + const deleted = this.cache.delete(key); + this.stats.size = this.cache.size; + return deleted; + } + + invalidateByModel(modelName: string): number { + let deletedCount = 0; + + for (const [key, _entry] of this.cache.entries()) { + if (key.startsWith(this.hashString(modelName))) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.size = this.cache.size; + return deletedCount; + } + + invalidateByUser(userId: string): number { + let deletedCount = 0; + + for (const [key, entry] of this.cache.entries()) { + // Check if the cached entry contains user-specific data + if (this.entryContainsUser(entry, userId)) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.size = this.cache.size; + return deletedCount; + } + + clear(): void { + this.cache.clear(); + this.stats.size = 0; + this.stats.totalRequests = 0; + this.stats.cacheHits = 0; + this.stats.cacheMisses = 0; + this.stats.hitRate = 0; + } + + getStats(): CacheStats { + return { ...this.stats }; + } + + // Cache warming - preload frequently used queries + async warmup<T extends BaseModel>(queries: QueryBuilder<T>[]): Promise<void> { + console.log(`🔥 Warming up cache with ${queries.length} queries...`); + + const promises = queries.map(async (query) => { + try { + const results = await query.exec(); + this.set(query, results); + console.log(`✓ Cached query for ${query.getModel().name}`); + } catch (error) { + console.warn(`Failed to warm cache for ${query.getModel().name}:`, error); + } + }); + + await Promise.all(promises); + console.log(`✅ Cache warmup completed`); + } + + // Get cache entries sorted by various criteria + getPopularEntries(limit: number = 10): Array<{ key: string; hitCount: number; age: number }> { + return Array.from(this.cache.entries()) + .map(([key, entry]) => ({ + key, + hitCount: entry.hitCount, + age: Date.now() - entry.timestamp, + })) + .sort((a, b) => b.hitCount - a.hitCount) + .slice(0, limit); + } + + getExpiredEntries(): string[] { + const now = Date.now(); + const expired: string[] = []; + + for (const [key, entry] of this.cache.entries()) { + if (now - entry.timestamp > entry.ttl) { + expired.push(key); + } + } + + return expired; + } + + // Cleanup expired entries + cleanup(): number { + const expired = this.getExpiredEntries(); + + for (const key of expired) { + this.cache.delete(key); + } + + this.stats.size = this.cache.size; + return expired.length; + } + + // Configure cache behavior + setMaxSize(size: number): void { + this.maxSize = size; + this.stats.maxSize = size; + + // Evict entries if current size exceeds new max + while (this.cache.size > size) { + this.evictLeastUsed(); + } + } + + setDefaultTTL(ttl: number): void { + this.defaultTTL = ttl; + } + + // Cache analysis + analyzeUsage(): { + totalEntries: number; + averageHitCount: number; + averageAge: number; + memoryUsage: number; + } { + const entries = Array.from(this.cache.values()); + const now = Date.now(); + + const totalHits = entries.reduce((sum, entry) => sum + entry.hitCount, 0); + const totalAge = entries.reduce((sum, entry) => sum + (now - entry.timestamp), 0); + + // Rough memory usage estimation + const memoryUsage = entries.reduce((sum, entry) => { + return sum + JSON.stringify(entry.data).length; + }, 0); + + return { + totalEntries: entries.length, + averageHitCount: entries.length > 0 ? totalHits / entries.length : 0, + averageAge: entries.length > 0 ? totalAge / entries.length : 0, + memoryUsage, + }; + } + + private evictLeastUsed(): void { + if (this.cache.size === 0) return; + + // Find entry with lowest hit count and oldest timestamp + let leastUsedKey: string | null = null; + let leastUsedScore = Infinity; + + for (const [key, entry] of this.cache.entries()) { + // Score based on hit count and age (lower is worse) + const age = Date.now() - entry.timestamp; + const score = entry.hitCount - age / 1000000; // Age penalty + + if (score < leastUsedScore) { + leastUsedScore = score; + leastUsedKey = key; + } + } + + if (leastUsedKey) { + this.cache.delete(leastUsedKey); + this.stats.size = this.cache.size; + } + } + + private entryContainsUser(entry: CacheEntry<any>, userId: string): boolean { + // Check if the cached data contains user-specific information + try { + const dataStr = JSON.stringify(entry.data); + return dataStr.includes(userId); + } catch { + return false; + } + } + + private updateHitRate(): void { + if (this.stats.totalRequests > 0) { + this.stats.hitRate = this.stats.cacheHits / this.stats.totalRequests; + } + } + + private hashString(str: string): string { + let hash = 0; + if (str.length === 0) return hash.toString(); + + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + + return Math.abs(hash).toString(36); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { QueryBuilder } from './QueryBuilder'; +import { QueryCondition } from '../types/queries'; +import { StoreType } from '../types/framework'; +import { QueryOptimizer, QueryPlan } from './QueryOptimizer'; + +export class QueryExecutor<T extends BaseModel> { + private model: typeof BaseModel; + private query: QueryBuilder<T>; + private framework: any; // Will be properly typed later + private queryPlan?: QueryPlan; + private useCache: boolean = true; + + constructor(model: typeof BaseModel, query: QueryBuilder<T>) { + this.model = model; + this.query = query; + this.framework = this.getFrameworkInstance(); + } + + async execute(): Promise<T[]> { + const startTime = Date.now(); + console.log(`🔍 Executing query for ${this.model.name} (${this.model.scope})`); + + // Generate query plan for optimization + this.queryPlan = QueryOptimizer.analyzeQuery(this.query); + console.log( + `📊 Query plan: ${this.queryPlan.strategy} (cost: ${this.queryPlan.estimatedCost})`, + ); + + // Check cache first if enabled + if (this.useCache && this.framework.queryCache) { + const cached = await this.framework.queryCache.get(this.query); + if (cached) { + console.log(`⚡ Cache hit for ${this.model.name} query`); + return cached; + } + } + + // Execute query based on scope + let results: T[]; + if (this.model.scope === 'user') { + results = await this.executeUserScopedQuery(); + } else { + results = await this.executeGlobalQuery(); + } + + // Cache results if enabled + if (this.useCache && this.framework.queryCache && results.length > 0) { + this.framework.queryCache.set(this.query, results); + } + + const duration = Date.now() - startTime; + console.log(`✅ Query completed in ${duration}ms, returned ${results.length} results`); + + return results; + } + + async count(): Promise<number> { + const results = await this.execute(); + return results.length; + } + + async sum(field: string): Promise<number> { + const results = await this.execute(); + return results.reduce((sum, item) => { + const value = this.getNestedValue(item, field); + return sum + (typeof value === 'number' ? value : 0); + }, 0); + } + + async avg(field: string): Promise<number> { + const results = await this.execute(); + if (results.length === 0) return 0; + + const sum = await this.sum(field); + return sum / results.length; + } + + async min(field: string): Promise<any> { + const results = await this.execute(); + if (results.length === 0) return null; + + return results.reduce((min, item) => { + const value = this.getNestedValue(item, field); + return min === null || value < min ? value : min; + }, null); + } + + async max(field: string): Promise<any> { + const results = await this.execute(); + if (results.length === 0) return null; + + return results.reduce((max, item) => { + const value = this.getNestedValue(item, field); + return max === null || value > max ? value : max; + }, null); + } + + private async executeUserScopedQuery(): Promise<T[]> { + const conditions = this.query.getConditions(); + + // Check if we have user-specific filters + const userFilter = conditions.find((c) => c.field === 'userId' || c.operator === 'userIn'); + + if (userFilter) { + return await this.executeUserSpecificQuery(userFilter); + } else { + // Global query on user-scoped data - use global index + return await this.executeGlobalIndexQuery(); + } + } + + private async executeUserSpecificQuery(userFilter: QueryCondition): Promise<T[]> { + const userIds = userFilter.operator === 'userIn' ? userFilter.value : [userFilter.value]; + + console.log(`👤 Querying user databases for ${userIds.length} users`); + + const results: T[] = []; + + // Query each user's database in parallel + const promises = userIds.map(async (userId: string) => { + try { + const userDB = await this.framework.databaseManager.getUserDatabase( + userId, + this.model.modelName, + ); + + return await this.queryDatabase(userDB, this.model.dbType); + } catch (error) { + console.warn(`Failed to query user ${userId} database:`, error); + return []; + } + }); + + const userResults = await Promise.all(promises); + + // Flatten and combine results + for (const userResult of userResults) { + results.push(...userResult); + } + + return this.postProcessResults(results); + } + + private async executeGlobalIndexQuery(): Promise<T[]> { + console.log(`📇 Querying global index for ${this.model.name}`); + + // Query global index for user-scoped models + const globalIndexName = `${this.model.modelName}GlobalIndex`; + const indexShards = this.framework.shardManager.getAllShards(globalIndexName); + + if (!indexShards || indexShards.length === 0) { + console.warn(`No global index found for ${this.model.name}, falling back to all users query`); + return await this.executeAllUsersQuery(); + } + + const indexResults: any[] = []; + + // Query all index shards in parallel + const promises = indexShards.map((shard: any) => + this.queryDatabase(shard.database, 'keyvalue'), + ); + const shardResults = await Promise.all(promises); + + for (const shardResult of shardResults) { + indexResults.push(...shardResult); + } + + // Now fetch actual documents from user databases + return await this.fetchActualDocuments(indexResults); + } + + private async executeAllUsersQuery(): Promise<T[]> { + // This is a fallback for when global index is not available + // It's expensive but ensures completeness + console.warn(`⚠️ Executing expensive all-users query for ${this.model.name}`); + + // This would require getting all user IDs from the directory + // For now, return empty array and log warning + console.warn('All-users query not implemented - please ensure global indexes are set up'); + return []; + } + + private async executeGlobalQuery(): Promise<T[]> { + // For globally scoped models + if (this.model.sharding) { + return await this.executeShardedQuery(); + } else { + const db = await this.framework.databaseManager.getGlobalDatabase(this.model.modelName); + return await this.queryDatabase(db, this.model.dbType); + } + } + + private async executeShardedQuery(): Promise<T[]> { + console.log(`🔀 Executing sharded query for ${this.model.name}`); + + const conditions = this.query.getConditions(); + const shardingConfig = this.model.sharding!; + + // Check if we can route to specific shard(s) + const shardKeyCondition = conditions.find((c) => c.field === shardingConfig.key); + + if (shardKeyCondition && shardKeyCondition.operator === '=') { + // Single shard query + const shard = this.framework.shardManager.getShardForKey( + this.model.modelName, + shardKeyCondition.value, + ); + return await this.queryDatabase(shard.database, this.model.dbType); + } else if (shardKeyCondition && shardKeyCondition.operator === 'in') { + // Multiple specific shards + const results: T[] = []; + const shardKeys = shardKeyCondition.value; + + const shardQueries = shardKeys.map(async (key: string) => { + const shard = this.framework.shardManager.getShardForKey(this.model.modelName, key); + return await this.queryDatabase(shard.database, this.model.dbType); + }); + + const shardResults = await Promise.all(shardQueries); + for (const shardResult of shardResults) { + results.push(...shardResult); + } + + return this.postProcessResults(results); + } else { + // Query all shards + const results: T[] = []; + const allShards = this.framework.shardManager.getAllShards(this.model.modelName); + + const promises = allShards.map((shard: any) => + this.queryDatabase(shard.database, this.model.dbType), + ); + const shardResults = await Promise.all(promises); + + for (const shardResult of shardResults) { + results.push(...shardResult); + } + + return this.postProcessResults(results); + } + } + + private async queryDatabase(database: any, dbType: StoreType): Promise<T[]> { + // Get all documents from OrbitDB based on database type + let documents: any[]; + + try { + documents = await this.framework.databaseManager.getAllDocuments(database, dbType); + } catch (error) { + console.error(`Error querying ${dbType} database:`, error); + return []; + } + + // Apply filters in memory + documents = this.applyFilters(documents); + + // Apply sorting + documents = this.applySorting(documents); + + // Apply limit/offset + documents = this.applyLimitOffset(documents); + + // Convert to model instances + const ModelClass = this.model as any; // Type assertion for abstract class + return documents.map((doc) => new ModelClass(doc) as T); + } + + private async fetchActualDocuments(indexResults: any[]): Promise<T[]> { + console.log(`📄 Fetching ${indexResults.length} documents from user databases`); + + const results: T[] = []; + + // Group by userId for efficient database access + const userGroups = new Map<string, any[]>(); + + for (const indexEntry of indexResults) { + const userId = indexEntry.userId; + if (!userGroups.has(userId)) { + userGroups.set(userId, []); + } + userGroups.get(userId)!.push(indexEntry); + } + + // Fetch documents from each user's database + const promises = Array.from(userGroups.entries()).map(async ([userId, entries]) => { + try { + const userDB = await this.framework.databaseManager.getUserDatabase( + userId, + this.model.modelName, + ); + + const userResults: T[] = []; + + // Fetch specific documents by ID + for (const entry of entries) { + try { + const doc = await this.getDocumentById(userDB, this.model.dbType, entry.id); + if (doc) { + const ModelClass = this.model as any; // Type assertion for abstract class + userResults.push(new ModelClass(doc) as T); + } + } catch (error) { + console.warn(`Failed to fetch document ${entry.id} from user ${userId}:`, error); + } + } + + return userResults; + } catch (error) { + console.warn(`Failed to access user ${userId} database:`, error); + return []; + } + }); + + const userResults = await Promise.all(promises); + + // Flatten results + for (const userResult of userResults) { + results.push(...userResult); + } + + return this.postProcessResults(results); + } + + private async getDocumentById(database: any, dbType: StoreType, id: string): Promise<any | null> { + try { + switch (dbType) { + case 'keyvalue': + return await database.get(id); + + case 'docstore': + return await database.get(id); + + case 'eventlog': + case 'feed': + // For append-only stores, we need to search through entries + const iterator = database.iterator(); + const entries = iterator.collect(); + return ( + entries.find((entry: any) => entry.payload?.value?.id === id)?.payload?.value || null + ); + + default: + return null; + } + } catch (error) { + console.warn(`Error fetching document ${id} from ${dbType}:`, error); + return null; + } + } + + private applyFilters(documents: any[]): any[] { + const conditions = this.query.getConditions(); + + return documents.filter((doc) => { + return conditions.every((condition) => { + return this.evaluateCondition(doc, condition); + }); + }); + } + + private evaluateCondition(doc: any, condition: QueryCondition): boolean { + const { field, operator, value } = condition; + + // Handle special operators + if (operator === 'or') { + return value.some((subCondition: QueryCondition) => + this.evaluateCondition(doc, subCondition), + ); + } + + if (field === '__raw__') { + // Raw conditions would need custom evaluation + console.warn('Raw conditions not fully implemented'); + return true; + } + + const docValue = this.getNestedValue(doc, field); + + switch (operator) { + case '=': + case '==': + return docValue === value; + + case '!=': + case '<>': + return docValue !== value; + + case '>': + return docValue > value; + + case '>=': + case 'gte': + return docValue >= value; + + case '<': + return docValue < value; + + case '<=': + case 'lte': + return docValue <= value; + + case 'in': + return Array.isArray(value) && value.includes(docValue); + + case 'not_in': + return Array.isArray(value) && !value.includes(docValue); + + case 'contains': + return Array.isArray(docValue) && docValue.includes(value); + + case 'like': + return String(docValue).toLowerCase().includes(String(value).toLowerCase()); + + case 'ilike': + return String(docValue).toLowerCase().includes(String(value).toLowerCase()); + + case 'is_null': + return docValue === null || docValue === undefined; + + case 'is_not_null': + return docValue !== null && docValue !== undefined; + + case 'between': + return Array.isArray(value) && docValue >= value[0] && docValue <= value[1]; + + case 'array_contains': + return Array.isArray(docValue) && docValue.includes(value); + + case 'array_length_=': + return Array.isArray(docValue) && docValue.length === value; + + case 'array_length_>': + return Array.isArray(docValue) && docValue.length > value; + + case 'array_length_<': + return Array.isArray(docValue) && docValue.length < value; + + case 'object_has_key': + return typeof docValue === 'object' && docValue !== null && value in docValue; + + case 'date_=': + return this.compareDates(docValue, '=', value); + + case 'date_>': + return this.compareDates(docValue, '>', value); + + case 'date_<': + return this.compareDates(docValue, '<', value); + + case 'date_between': + return ( + this.compareDates(docValue, '>=', value[0]) && this.compareDates(docValue, '<=', value[1]) + ); + + case 'year': + return this.getDatePart(docValue, 'year') === value; + + case 'month': + return this.getDatePart(docValue, 'month') === value; + + case 'day': + return this.getDatePart(docValue, 'day') === value; + + default: + console.warn(`Unsupported operator: ${operator}`); + return true; + } + } + + private compareDates(docValue: any, operator: string, compareValue: any): boolean { + const docDate = this.normalizeDate(docValue); + const compDate = this.normalizeDate(compareValue); + + if (!docDate || !compDate) return false; + + switch (operator) { + case '=': + return docDate.getTime() === compDate.getTime(); + case '>': + return docDate.getTime() > compDate.getTime(); + case '<': + return docDate.getTime() < compDate.getTime(); + case '>=': + return docDate.getTime() >= compDate.getTime(); + case '<=': + return docDate.getTime() <= compDate.getTime(); + default: + return false; + } + } + + private normalizeDate(value: any): Date | null { + if (value instanceof Date) return value; + if (typeof value === 'number') return new Date(value); + if (typeof value === 'string') return new Date(value); + return null; + } + + private getDatePart(value: any, part: 'year' | 'month' | 'day'): number | null { + const date = this.normalizeDate(value); + if (!date) return null; + + switch (part) { + case 'year': + return date.getFullYear(); + case 'month': + return date.getMonth() + 1; // 1-based month + case 'day': + return date.getDate(); + default: + return null; + } + } + + private applySorting(documents: any[]): any[] { + const sorting = this.query.getSorting(); + + if (sorting.length === 0) { + return documents; + } + + return documents.sort((a, b) => { + for (const sort of sorting) { + const aValue = this.getNestedValue(a, sort.field); + const bValue = this.getNestedValue(b, sort.field); + + let comparison = 0; + + if (aValue < bValue) comparison = -1; + else if (aValue > bValue) comparison = 1; + + if (comparison !== 0) { + return sort.direction === 'desc' ? -comparison : comparison; + } + } + + return 0; + }); + } + + private applyLimitOffset(documents: any[]): any[] { + const limit = this.query.getLimit(); + const offset = this.query.getOffset(); + + let result = documents; + + if (offset && offset > 0) { + result = result.slice(offset); + } + + if (limit && limit > 0) { + result = result.slice(0, limit); + } + + return result; + } + + private postProcessResults(results: T[]): T[] { + // Apply global sorting across all results + results = this.applySorting(results); + + // Apply global limit/offset + results = this.applyLimitOffset(results); + + return results; + } + + private getNestedValue(obj: any, path: string): any { + if (!path) return obj; + + const keys = path.split('.'); + let current = obj; + + for (const key of keys) { + if (current === null || current === undefined) { + return undefined; + } + current = current[key]; + } + + return current; + } + + // Public methods for query control + disableCache(): this { + this.useCache = false; + return this; + } + + enableCache(): this { + this.useCache = true; + return this; + } + + getQueryPlan(): QueryPlan | undefined { + return this.queryPlan; + } + + explain(): any { + const plan = this.queryPlan || QueryOptimizer.analyzeQuery(this.query); + const suggestions = QueryOptimizer.suggestOptimizations(this.query); + + return { + query: this.query.explain(), + plan, + suggestions, + estimatedResultSize: QueryOptimizer.estimateResultSize(this.query), + }; + } + + private getFrameworkInstance(): any { + const framework = (globalThis as any).__debrosFramework; + if (!framework) { + throw new Error('Framework not initialized. Call framework.initialize() first.'); + } + return framework; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { QueryBuilder } from './QueryBuilder'; +import { QueryCondition } from '../types/queries'; +import { BaseModel } from '../models/BaseModel'; + +export interface QueryPlan { + strategy: 'single_user' | 'multi_user' | 'global_index' | 'all_shards' | 'specific_shards'; + targetDatabases: string[]; + estimatedCost: number; + indexHints: string[]; + optimizations: string[]; +} + +export class QueryOptimizer { + static analyzeQuery<T extends BaseModel>(query: QueryBuilder<T>): QueryPlan { + const model = query.getModel(); + const conditions = query.getConditions(); + const relations = query.getRelations(); + const limit = query.getLimit(); + + let strategy: QueryPlan['strategy'] = 'all_shards'; + let targetDatabases: string[] = []; + let estimatedCost = 100; // Base cost + let indexHints: string[] = []; + let optimizations: string[] = []; + + // Analyze based on model scope + if (model.scope === 'user') { + const userConditions = conditions.filter( + (c) => c.field === 'userId' || c.operator === 'userIn', + ); + + if (userConditions.length > 0) { + const userCondition = userConditions[0]; + + if (userCondition.operator === 'userIn') { + strategy = 'multi_user'; + targetDatabases = userCondition.value.map( + (userId: string) => `${userId}-${model.modelName.toLowerCase()}`, + ); + estimatedCost = 20 * userCondition.value.length; + optimizations.push('Direct user database access'); + } else { + strategy = 'single_user'; + targetDatabases = [`${userCondition.value}-${model.modelName.toLowerCase()}`]; + estimatedCost = 10; + optimizations.push('Single user database access'); + } + } else { + strategy = 'global_index'; + targetDatabases = [`${model.modelName}GlobalIndex`]; + estimatedCost = 50; + indexHints.push(`${model.modelName}GlobalIndex`); + optimizations.push('Global index lookup'); + } + } else { + // Global model + if (model.sharding) { + const shardKeyCondition = conditions.find((c) => c.field === model.sharding!.key); + + if (shardKeyCondition) { + if (shardKeyCondition.operator === '=') { + strategy = 'specific_shards'; + targetDatabases = [`${model.modelName}-shard-specific`]; + estimatedCost = 15; + optimizations.push('Single shard access'); + } else if (shardKeyCondition.operator === 'in') { + strategy = 'specific_shards'; + targetDatabases = shardKeyCondition.value.map( + (_: any, i: number) => `${model.modelName}-shard-${i}`, + ); + estimatedCost = 15 * shardKeyCondition.value.length; + optimizations.push('Multiple specific shards'); + } + } else { + strategy = 'all_shards'; + estimatedCost = 30 * (model.sharding.count || 4); + optimizations.push('All shards scan'); + } + } else { + strategy = 'single_user'; // Actually single global database + targetDatabases = [`global-${model.modelName.toLowerCase()}`]; + estimatedCost = 25; + optimizations.push('Single global database'); + } + } + + // Adjust cost based on other factors + if (limit && limit < 100) { + estimatedCost *= 0.8; + optimizations.push(`Limit optimization (${limit})`); + } + + if (relations.length > 0) { + estimatedCost *= 1 + relations.length * 0.3; + optimizations.push(`Relationship loading (${relations.length})`); + } + + // Suggest indexes based on conditions + const indexedFields = conditions + .filter((c) => c.field !== 'userId' && c.field !== '__or__' && c.field !== '__raw__') + .map((c) => c.field); + + if (indexedFields.length > 0) { + indexHints.push(...indexedFields.map((field) => `${model.modelName}_${field}_idx`)); + } + + return { + strategy, + targetDatabases, + estimatedCost, + indexHints, + optimizations, + }; + } + + static optimizeConditions(conditions: QueryCondition[]): QueryCondition[] { + const optimized = [...conditions]; + + // Remove redundant conditions + const seen = new Set(); + const filtered = optimized.filter((condition) => { + const key = `${condition.field}_${condition.operator}_${JSON.stringify(condition.value)}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + + // Sort conditions by selectivity (most selective first) + return filtered.sort((a, b) => { + const selectivityA = this.getConditionSelectivity(a); + const selectivityB = this.getConditionSelectivity(b); + return selectivityA - selectivityB; + }); + } + + private static getConditionSelectivity(condition: QueryCondition): number { + // Lower numbers = more selective (better to evaluate first) + switch (condition.operator) { + case '=': + return 1; + case 'in': + return Array.isArray(condition.value) ? condition.value.length : 10; + case '>': + case '<': + case '>=': + case '<=': + return 50; + case 'like': + case 'ilike': + return 75; + case 'is_not_null': + return 90; + default: + return 100; + } + } + + static shouldUseIndex(field: string, operator: string, model: typeof BaseModel): boolean { + // Check if field has index configuration + const fieldConfig = model.fields?.get(field); + if (fieldConfig?.index) { + return true; + } + + // Certain operators benefit from indexes + const indexBeneficialOps = ['=', 'in', '>', '<', '>=', '<=', 'between']; + return indexBeneficialOps.includes(operator); + } + + static estimateResultSize(query: QueryBuilder<any>): number { + const conditions = query.getConditions(); + const limit = query.getLimit(); + + // If there's a limit, that's our upper bound + if (limit) { + return limit; + } + + // Estimate based on conditions + let estimate = 1000; // Base estimate + + for (const condition of conditions) { + switch (condition.operator) { + case '=': + estimate *= 0.1; // Very selective + break; + case 'in': + estimate *= Array.isArray(condition.value) ? condition.value.length * 0.1 : 0.1; + break; + case '>': + case '<': + case '>=': + case '<=': + estimate *= 0.5; // Moderately selective + break; + case 'like': + estimate *= 0.3; // Somewhat selective + break; + default: + estimate *= 0.8; + } + } + + return Math.max(1, Math.round(estimate)); + } + + static suggestOptimizations<T extends BaseModel>(query: QueryBuilder<T>): string[] { + const suggestions: string[] = []; + const conditions = query.getConditions(); + const model = query.getModel(); + const limit = query.getLimit(); + + // Check for missing userId in user-scoped queries + if (model.scope === 'user') { + const hasUserFilter = conditions.some((c) => c.field === 'userId' || c.operator === 'userIn'); + if (!hasUserFilter) { + suggestions.push('Add userId filter to avoid expensive global index query'); + } + } + + // Check for missing limit on potentially large result sets + if (!limit) { + const estimatedSize = this.estimateResultSize(query); + if (estimatedSize > 100) { + suggestions.push('Add limit() to prevent large result sets'); + } + } + + // Check for unindexed field queries + for (const condition of conditions) { + if (!this.shouldUseIndex(condition.field, condition.operator, model)) { + suggestions.push(`Consider adding index for field: ${condition.field}`); + } + } + + // Check for expensive operations + const expensiveOps = conditions.filter((c) => + ['like', 'ilike', 'array_contains'].includes(c.operator), + ); + if (expensiveOps.length > 0) { + suggestions.push('Consider using more selective filters before expensive operations'); + } + + // Check for OR conditions + const orConditions = conditions.filter((c) => c.operator === 'or'); + if (orConditions.length > 0) { + suggestions.push('OR conditions can be expensive, consider restructuring query'); + } + + return suggestions; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
QueryBuilder.ts | +
+
+ |
+ 0% | +0/142 | +0% | +0/22 | +0% | +0/69 | +0% | +0/141 | +
QueryCache.ts | +
+
+ |
+ 0% | +0/130 | +0% | +0/35 | +0% | +0/29 | +0% | +0/123 | +
QueryExecutor.ts | +
+
+ |
+ 0% | +0/270 | +0% | +0/171 | +0% | +0/46 | +0% | +0/256 | +
QueryOptimizer.ts | +
+
+ |
+ 0% | +0/130 | +0% | +0/73 | +0% | +0/18 | +0% | +0/126 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { RelationshipConfig } from '../types/models'; +import { RelationshipManager, RelationshipLoadOptions } from './RelationshipManager'; + +export interface LazyLoadPromise<T> extends Promise<T> { + isLoaded(): boolean; + getLoadedValue(): T | undefined; + reload(options?: RelationshipLoadOptions): Promise<T>; +} + +export class LazyLoader { + private relationshipManager: RelationshipManager; + + constructor(relationshipManager: RelationshipManager) { + this.relationshipManager = relationshipManager; + } + + createLazyProperty<T>( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): LazyLoadPromise<T> { + let loadPromise: Promise<T> | null = null; + let loadedValue: T | undefined = undefined; + let isLoaded = false; + + const loadRelationship = async (): Promise<T> => { + if (loadPromise) { + return loadPromise; + } + + loadPromise = this.relationshipManager + .loadRelationship(instance, relationshipName, options) + .then((result: T) => { + loadedValue = result; + isLoaded = true; + return result; + }) + .catch((error) => { + loadPromise = null; // Reset so it can be retried + throw error; + }); + + return loadPromise; + }; + + const reload = async (newOptions?: RelationshipLoadOptions): Promise<T> => { + // Clear cache for this relationship + this.relationshipManager.invalidateRelationshipCache(instance, relationshipName); + + // Reset state + loadPromise = null; + loadedValue = undefined; + isLoaded = false; + + // Load with new options + const finalOptions = newOptions ? { ...options, ...newOptions } : options; + return this.relationshipManager.loadRelationship(instance, relationshipName, finalOptions); + }; + + // Create the main promise + const promise = loadRelationship() as LazyLoadPromise<T>; + + // Add custom methods + promise.isLoaded = () => isLoaded; + promise.getLoadedValue = () => loadedValue; + promise.reload = reload; + + return promise; + } + + createLazyPropertyWithProxy<T>( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): T { + const lazyPromise = this.createLazyProperty<T>(instance, relationshipName, config, options); + + // For single relationships, return a proxy that loads on property access + if (config.type === 'belongsTo' || config.type === 'hasOne') { + return new Proxy({} as any, { + get(target: any, prop: string | symbol) { + // Special methods + if (prop === 'then') { + return lazyPromise.then.bind(lazyPromise); + } + if (prop === 'catch') { + return lazyPromise.catch.bind(lazyPromise); + } + if (prop === 'finally') { + return lazyPromise.finally.bind(lazyPromise); + } + if (prop === 'isLoaded') { + return lazyPromise.isLoaded; + } + if (prop === 'reload') { + return lazyPromise.reload; + } + + // If already loaded, return the property from loaded value + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue(); + return loadedValue ? (loadedValue as any)[prop] : undefined; + } + + // Trigger loading and return undefined for now + lazyPromise.catch(() => {}); // Prevent unhandled promise rejection + return undefined; + }, + + has(target: any, prop: string | symbol) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue(); + return loadedValue ? prop in (loadedValue as any) : false; + } + return false; + }, + + ownKeys(_target: any) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue(); + return loadedValue ? Object.keys(loadedValue as any) : []; + } + return []; + }, + }); + } + + // For collection relationships, return a proxy array + if (config.type === 'hasMany' || config.type === 'manyToMany') { + return new Proxy([] as any, { + get(target: any[], prop: string | symbol) { + // Array methods and properties + if (prop === 'length') { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? loadedValue.length : 0; + } + return 0; + } + + // Promise methods + if (prop === 'then') { + return lazyPromise.then.bind(lazyPromise); + } + if (prop === 'catch') { + return lazyPromise.catch.bind(lazyPromise); + } + if (prop === 'finally') { + return lazyPromise.finally.bind(lazyPromise); + } + if (prop === 'isLoaded') { + return lazyPromise.isLoaded; + } + if (prop === 'reload') { + return lazyPromise.reload; + } + + // Array methods that should trigger loading + if ( + typeof prop === 'string' && + [ + 'forEach', + 'map', + 'filter', + 'find', + 'some', + 'every', + 'reduce', + 'slice', + 'indexOf', + 'includes', + ].includes(prop) + ) { + return async (...args: any[]) => { + const loadedValue = await lazyPromise; + return (loadedValue as any)[prop](...args); + }; + } + + // Numeric index access + if (typeof prop === 'string' && /^\d+$/.test(prop)) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? loadedValue[parseInt(prop, 10)] : undefined; + } + // Trigger loading + lazyPromise.catch(() => {}); + return undefined; + } + + // If already loaded, delegate to the actual array + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? (loadedValue as any)[prop] : undefined; + } + + return undefined; + }, + + has(target: any[], prop: string | symbol) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? prop in loadedValue : false; + } + return false; + }, + + ownKeys(_target: any[]) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? Object.keys(loadedValue) : []; + } + return []; + }, + }) as T; + } + + // Fallback to promise for other types + return lazyPromise as any; + } + + // Helper method to check if a value is a lazy-loaded relationship + static isLazyLoaded(value: any): value is LazyLoadPromise<any> { + return ( + value && + typeof value === 'object' && + typeof value.then === 'function' && + typeof value.isLoaded === 'function' && + typeof value.reload === 'function' + ); + } + + // Helper method to await all lazy relationships in an object + static async resolveAllLazy(obj: any): Promise<any> { + if (!obj || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return Promise.all(obj.map((item) => this.resolveAllLazy(item))); + } + + const resolved: any = {}; + const promises: Array<Promise<void>> = []; + + for (const [key, value] of Object.entries(obj)) { + if (this.isLazyLoaded(value)) { + promises.push( + value.then((resolvedValue) => { + resolved[key] = resolvedValue; + }), + ); + } else { + resolved[key] = value; + } + } + + await Promise.all(promises); + return resolved; + } + + // Helper method to get loaded relationships without triggering loading + static getLoadedRelationships(instance: BaseModel): Record<string, any> { + const loaded: Record<string, any> = {}; + + const loadedRelations = instance.getLoadedRelations(); + for (const relationName of loadedRelations) { + const value = instance.getRelation(relationName); + if (this.isLazyLoaded(value)) { + if (value.isLoaded()) { + loaded[relationName] = value.getLoadedValue(); + } + } else { + loaded[relationName] = value; + } + } + + return loaded; + } + + // Helper method to preload specific relationships + static async preloadRelationships( + instances: BaseModel[], + relationships: string[], + relationshipManager: RelationshipManager, + ): Promise<void> { + await relationshipManager.eagerLoadRelationships(instances, relationships); + } + + // Helper method to create lazy collection with advanced features + createLazyCollection<T extends BaseModel>( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): LazyCollection<T> { + return new LazyCollection<T>( + instance, + relationshipName, + config, + options, + this.relationshipManager, + ); + } +} + +// Advanced lazy collection with pagination and filtering +export class LazyCollection<T extends BaseModel> { + private instance: BaseModel; + private relationshipName: string; + private config: RelationshipConfig; + private options: RelationshipLoadOptions; + private relationshipManager: RelationshipManager; + private loadedItems: T[] = []; + private isFullyLoaded = false; + private currentPage = 1; + private pageSize = 20; + + constructor( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + relationshipManager: RelationshipManager, + ) { + this.instance = instance; + this.relationshipName = relationshipName; + this.config = config; + this.options = options; + this.relationshipManager = relationshipManager; + } + + async loadPage(page: number = 1, pageSize: number = this.pageSize): Promise<T[]> { + const offset = (page - 1) * pageSize; + + const pageOptions: RelationshipLoadOptions = { + ...this.options, + constraints: (query) => { + let q = query.offset(offset).limit(pageSize); + if (this.options.constraints) { + q = this.options.constraints(q); + } + return q; + }, + }; + + const pageItems = (await this.relationshipManager.loadRelationship( + this.instance, + this.relationshipName, + pageOptions, + )) as T[]; + + // Update loaded items if this is sequential loading + if (page === this.currentPage) { + this.loadedItems.push(...pageItems); + this.currentPage++; + + if (pageItems.length < pageSize) { + this.isFullyLoaded = true; + } + } + + return pageItems; + } + + async loadMore(count: number = this.pageSize): Promise<T[]> { + return this.loadPage(this.currentPage, count); + } + + async loadAll(): Promise<T[]> { + if (this.isFullyLoaded) { + return this.loadedItems; + } + + const allItems = (await this.relationshipManager.loadRelationship( + this.instance, + this.relationshipName, + this.options, + )) as T[]; + + this.loadedItems = allItems; + this.isFullyLoaded = true; + + return allItems; + } + + getLoadedItems(): T[] { + return [...this.loadedItems]; + } + + isLoaded(): boolean { + return this.loadedItems.length > 0; + } + + isCompletelyLoaded(): boolean { + return this.isFullyLoaded; + } + + async filter(predicate: (item: T) => boolean): Promise<T[]> { + if (!this.isFullyLoaded) { + await this.loadAll(); + } + return this.loadedItems.filter(predicate); + } + + async find(predicate: (item: T) => boolean): Promise<T | undefined> { + // Try loaded items first + const found = this.loadedItems.find(predicate); + if (found) { + return found; + } + + // If not fully loaded, load all and search + if (!this.isFullyLoaded) { + await this.loadAll(); + return this.loadedItems.find(predicate); + } + + return undefined; + } + + async count(): Promise<number> { + if (this.isFullyLoaded) { + return this.loadedItems.length; + } + + // For a complete count, we need to load all items + // In a more sophisticated implementation, we might have a separate count query + await this.loadAll(); + return this.loadedItems.length; + } + + clear(): void { + this.loadedItems = []; + this.isFullyLoaded = false; + this.currentPage = 1; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; + +export interface RelationshipCacheEntry { + key: string; + data: any; + timestamp: number; + ttl: number; + modelType: string; + relationshipType: string; +} + +export interface RelationshipCacheStats { + totalEntries: number; + hitCount: number; + missCount: number; + hitRate: number; + memoryUsage: number; +} + +export class RelationshipCache { + private cache: Map<string, RelationshipCacheEntry> = new Map(); + private maxSize: number; + private defaultTTL: number; + private stats: RelationshipCacheStats; + + constructor(maxSize: number = 1000, defaultTTL: number = 600000) { + // 10 minutes default + this.maxSize = maxSize; + this.defaultTTL = defaultTTL; + this.stats = { + totalEntries: 0, + hitCount: 0, + missCount: 0, + hitRate: 0, + memoryUsage: 0, + }; + } + + generateKey(instance: BaseModel, relationshipName: string, extraData?: any): string { + const baseKey = `${instance.constructor.name}:${instance.id}:${relationshipName}`; + + if (extraData) { + const extraStr = JSON.stringify(extraData); + return `${baseKey}:${this.hashString(extraStr)}`; + } + + return baseKey; + } + + get(key: string): any | null { + const entry = this.cache.get(key); + + if (!entry) { + this.stats.missCount++; + this.updateHitRate(); + return null; + } + + // Check if entry has expired + if (Date.now() - entry.timestamp > entry.ttl) { + this.cache.delete(key); + this.stats.missCount++; + this.updateHitRate(); + return null; + } + + this.stats.hitCount++; + this.updateHitRate(); + + return this.deserializeData(entry.data, entry.modelType); + } + + set( + key: string, + data: any, + modelType: string, + relationshipType: string, + customTTL?: number, + ): void { + const ttl = customTTL || this.defaultTTL; + + // Check if we need to evict entries + if (this.cache.size >= this.maxSize) { + this.evictOldest(); + } + + const entry: RelationshipCacheEntry = { + key, + data: this.serializeData(data), + timestamp: Date.now(), + ttl, + modelType, + relationshipType, + }; + + this.cache.set(key, entry); + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + } + + invalidate(key: string): boolean { + const deleted = this.cache.delete(key); + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return deleted; + } + + invalidateByInstance(instance: BaseModel): number { + const prefix = `${instance.constructor.name}:${instance.id}:`; + let deletedCount = 0; + + for (const [key] of this.cache.entries()) { + if (key.startsWith(prefix)) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return deletedCount; + } + + invalidateByModel(modelName: string): number { + let deletedCount = 0; + + for (const [key, entry] of this.cache.entries()) { + if (key.startsWith(`${modelName}:`) || entry.modelType === modelName) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return deletedCount; + } + + invalidateByRelationship(relationshipType: string): number { + let deletedCount = 0; + + for (const [key, entry] of this.cache.entries()) { + if (entry.relationshipType === relationshipType) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return deletedCount; + } + + clear(): void { + this.cache.clear(); + this.stats = { + totalEntries: 0, + hitCount: 0, + missCount: 0, + hitRate: 0, + memoryUsage: 0, + }; + } + + getStats(): RelationshipCacheStats { + return { ...this.stats }; + } + + // Preload relationships for multiple instances + async warmup( + instances: BaseModel[], + relationships: string[], + loadFunction: (instance: BaseModel, relationshipName: string) => Promise<any>, + ): Promise<void> { + console.log(`🔥 Warming relationship cache for ${instances.length} instances...`); + + const promises: Promise<void>[] = []; + + for (const instance of instances) { + for (const relationshipName of relationships) { + promises.push( + loadFunction(instance, relationshipName) + .then((data) => { + const key = this.generateKey(instance, relationshipName); + const modelType = data?.constructor?.name || 'unknown'; + this.set(key, data, modelType, relationshipName); + }) + .catch((error) => { + console.warn( + `Failed to warm cache for ${instance.constructor.name}:${instance.id}:${relationshipName}:`, + error, + ); + }), + ); + } + } + + await Promise.allSettled(promises); + console.log(`✅ Relationship cache warmed with ${promises.length} entries`); + } + + // Get cache entries by relationship type + getEntriesByRelationship(relationshipType: string): RelationshipCacheEntry[] { + return Array.from(this.cache.values()).filter( + (entry) => entry.relationshipType === relationshipType, + ); + } + + // Get expired entries + getExpiredEntries(): string[] { + const now = Date.now(); + const expired: string[] = []; + + for (const [key, entry] of this.cache.entries()) { + if (now - entry.timestamp > entry.ttl) { + expired.push(key); + } + } + + return expired; + } + + // Cleanup expired entries + cleanup(): number { + const expired = this.getExpiredEntries(); + + for (const key of expired) { + this.cache.delete(key); + } + + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return expired.length; + } + + // Performance analysis + analyzePerformance(): { + averageAge: number; + oldestEntry: number; + newestEntry: number; + relationshipTypes: Map<string, number>; + } { + const now = Date.now(); + let totalAge = 0; + let oldestAge = 0; + let newestAge = Infinity; + const relationshipTypes = new Map<string, number>(); + + for (const entry of this.cache.values()) { + const age = now - entry.timestamp; + totalAge += age; + + if (age > oldestAge) oldestAge = age; + if (age < newestAge) newestAge = age; + + const count = relationshipTypes.get(entry.relationshipType) || 0; + relationshipTypes.set(entry.relationshipType, count + 1); + } + + return { + averageAge: this.cache.size > 0 ? totalAge / this.cache.size : 0, + oldestEntry: oldestAge, + newestEntry: newestAge === Infinity ? 0 : newestAge, + relationshipTypes, + }; + } + + private serializeData(data: any): any { + if (Array.isArray(data)) { + return data.map((item) => this.serializeItem(item)); + } else { + return this.serializeItem(data); + } + } + + private serializeItem(item: any): any { + if (item && typeof item.toJSON === 'function') { + return { + __type: item.constructor.name, + __data: item.toJSON(), + }; + } + return item; + } + + private deserializeData(data: any, expectedType: string): any { + if (Array.isArray(data)) { + return data.map((item) => this.deserializeItem(item, expectedType)); + } else { + return this.deserializeItem(data, expectedType); + } + } + + private deserializeItem(item: any, _expectedType: string): any { + if (item && item.__type && item.__data) { + // For now, return the raw data + // In a full implementation, we would reconstruct the model instance + return item.__data; + } + return item; + } + + private evictOldest(): void { + if (this.cache.size === 0) return; + + let oldestKey: string | null = null; + let oldestTime = Infinity; + + for (const [key, entry] of this.cache.entries()) { + if (entry.timestamp < oldestTime) { + oldestTime = entry.timestamp; + oldestKey = key; + } + } + + if (oldestKey) { + this.cache.delete(oldestKey); + } + } + + private updateHitRate(): void { + const total = this.stats.hitCount + this.stats.missCount; + this.stats.hitRate = total > 0 ? this.stats.hitCount / total : 0; + } + + private updateMemoryUsage(): void { + // Rough estimation of memory usage + let size = 0; + for (const entry of this.cache.values()) { + size += JSON.stringify(entry.data).length; + } + this.stats.memoryUsage = size; + } + + private hashString(str: string): string { + let hash = 0; + if (str.length === 0) return hash.toString(); + + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + + return Math.abs(hash).toString(36); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { RelationshipConfig } from '../types/models'; +import { RelationshipCache } from './RelationshipCache'; +import { QueryBuilder } from '../query/QueryBuilder'; + +export interface RelationshipLoadOptions { + useCache?: boolean; + constraints?: (query: QueryBuilder<any>) => QueryBuilder<any>; + limit?: number; + orderBy?: { field: string; direction: 'asc' | 'desc' }; +} + +export interface EagerLoadPlan { + relationshipName: string; + config: RelationshipConfig; + instances: BaseModel[]; + options?: RelationshipLoadOptions; +} + +export class RelationshipManager { + private framework: any; + private cache: RelationshipCache; + + constructor(framework: any) { + this.framework = framework; + this.cache = new RelationshipCache(); + } + + async loadRelationship( + instance: BaseModel, + relationshipName: string, + options: RelationshipLoadOptions = {}, + ): Promise<any> { + const modelClass = instance.constructor as typeof BaseModel; + const relationConfig = modelClass.relationships?.get(relationshipName); + + if (!relationConfig) { + throw new Error(`Relationship '${relationshipName}' not found on ${modelClass.name}`); + } + + console.log( + `🔗 Loading ${relationConfig.type} relationship: ${modelClass.name}.${relationshipName}`, + ); + + // Check cache first if enabled + if (options.useCache !== false) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const cached = this.cache.get(cacheKey); + if (cached) { + console.log(`⚡ Cache hit for relationship ${relationshipName}`); + instance._loadedRelations.set(relationshipName, cached); + return cached; + } + } + + // Load relationship based on type + let result: any; + switch (relationConfig.type) { + case 'belongsTo': + result = await this.loadBelongsTo(instance, relationConfig, options); + break; + case 'hasMany': + result = await this.loadHasMany(instance, relationConfig, options); + break; + case 'hasOne': + result = await this.loadHasOne(instance, relationConfig, options); + break; + case 'manyToMany': + result = await this.loadManyToMany(instance, relationConfig, options); + break; + default: + throw new Error(`Unsupported relationship type: ${relationConfig.type}`); + } + + // Cache the result if enabled + if (options.useCache !== false && result) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const modelType = Array.isArray(result) + ? result[0]?.constructor?.name || 'unknown' + : result.constructor?.name || 'unknown'; + + this.cache.set(cacheKey, result, modelType, relationConfig.type); + } + + // Store in instance + instance.setRelation(relationshipName, result); + + console.log( + `✅ Loaded ${relationConfig.type} relationship: ${Array.isArray(result) ? result.length : 1} item(s)`, + ); + return result; + } + + private async loadBelongsTo( + instance: BaseModel, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<BaseModel | null> { + const foreignKeyValue = (instance as any)[config.foreignKey]; + + if (!foreignKeyValue) { + return null; + } + + // Build query for the related model + let query = (config.model as any).where('id', '=', foreignKeyValue); + + // Apply constraints if provided + if (options.constraints) { + query = options.constraints(query); + } + + const result = await query.first(); + return result; + } + + private async loadHasMany( + instance: BaseModel, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<BaseModel[]> { + if (config.through) { + return await this.loadManyToMany(instance, config, options); + } + + const localKeyValue = (instance as any)[config.localKey || 'id']; + + if (!localKeyValue) { + return []; + } + + // Build query for the related model + let query = (config.model as any).where(config.foreignKey, '=', localKeyValue); + + // Apply constraints if provided + if (options.constraints) { + query = options.constraints(query); + } + + // Apply default ordering and limiting + if (options.orderBy) { + query = query.orderBy(options.orderBy.field, options.orderBy.direction); + } + + if (options.limit) { + query = query.limit(options.limit); + } + + return await query.exec(); + } + + private async loadHasOne( + instance: BaseModel, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<BaseModel | null> { + const results = await this.loadHasMany( + instance, + { ...config, type: 'hasMany' }, + { + ...options, + limit: 1, + }, + ); + + return results[0] || null; + } + + private async loadManyToMany( + instance: BaseModel, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<BaseModel[]> { + if (!config.through) { + throw new Error('Many-to-many relationships require a through model'); + } + + const localKeyValue = (instance as any)[config.localKey || 'id']; + + if (!localKeyValue) { + return []; + } + + // Step 1: Get junction table records + let junctionQuery = (config.through as any).where(config.localKey || 'id', '=', localKeyValue); + + // Apply constraints to junction if needed + if (options.constraints) { + // Note: This is simplified - in a full implementation we'd need to handle + // constraints that apply to the final model vs the junction model + } + + const junctionRecords = await junctionQuery.exec(); + + if (junctionRecords.length === 0) { + return []; + } + + // Step 2: Extract foreign keys + const foreignKeys = junctionRecords.map((record: any) => record[config.foreignKey]); + + // Step 3: Get related models + let relatedQuery = (config.model as any).whereIn('id', foreignKeys); + + // Apply constraints if provided + if (options.constraints) { + relatedQuery = options.constraints(relatedQuery); + } + + // Apply ordering and limiting + if (options.orderBy) { + relatedQuery = relatedQuery.orderBy(options.orderBy.field, options.orderBy.direction); + } + + if (options.limit) { + relatedQuery = relatedQuery.limit(options.limit); + } + + return await relatedQuery.exec(); + } + + // Eager loading for multiple instances + async eagerLoadRelationships( + instances: BaseModel[], + relationships: string[], + options: Record<string, RelationshipLoadOptions> = {}, + ): Promise<void> { + if (instances.length === 0) return; + + console.log( + `🚀 Eager loading ${relationships.length} relationships for ${instances.length} instances`, + ); + + // Group instances by model type for efficient processing + const instanceGroups = this.groupInstancesByModel(instances); + + // Load each relationship for each model group + for (const relationshipName of relationships) { + await this.eagerLoadSingleRelationship( + instanceGroups, + relationshipName, + options[relationshipName] || {}, + ); + } + + console.log(`✅ Eager loading completed for ${relationships.length} relationships`); + } + + private async eagerLoadSingleRelationship( + instanceGroups: Map<string, BaseModel[]>, + relationshipName: string, + options: RelationshipLoadOptions, + ): Promise<void> { + for (const [modelName, instances] of instanceGroups) { + if (instances.length === 0) continue; + + const firstInstance = instances[0]; + const modelClass = firstInstance.constructor as typeof BaseModel; + const relationConfig = modelClass.relationships?.get(relationshipName); + + if (!relationConfig) { + console.warn(`Relationship '${relationshipName}' not found on ${modelName}`); + continue; + } + + console.log( + `🔗 Eager loading ${relationConfig.type} for ${instances.length} ${modelName} instances`, + ); + + switch (relationConfig.type) { + case 'belongsTo': + await this.eagerLoadBelongsTo(instances, relationshipName, relationConfig, options); + break; + case 'hasMany': + await this.eagerLoadHasMany(instances, relationshipName, relationConfig, options); + break; + case 'hasOne': + await this.eagerLoadHasOne(instances, relationshipName, relationConfig, options); + break; + case 'manyToMany': + await this.eagerLoadManyToMany(instances, relationshipName, relationConfig, options); + break; + } + } + } + + private async eagerLoadBelongsTo( + instances: BaseModel[], + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<void> { + // Get all foreign key values + const foreignKeys = instances + .map((instance) => (instance as any)[config.foreignKey]) + .filter((key) => key != null); + + if (foreignKeys.length === 0) { + // Set null for all instances + instances.forEach((instance) => { + instance._loadedRelations.set(relationshipName, null); + }); + return; + } + + // Remove duplicates + const uniqueForeignKeys = [...new Set(foreignKeys)]; + + // Load all related models at once + let query = (config.model as any).whereIn('id', uniqueForeignKeys); + + if (options.constraints) { + query = options.constraints(query); + } + + const relatedModels = await query.exec(); + + // Create lookup map + const relatedMap = new Map(); + relatedModels.forEach((model: any) => relatedMap.set(model.id, model)); + + // Assign to instances and cache + instances.forEach((instance) => { + const foreignKeyValue = (instance as any)[config.foreignKey]; + const related = relatedMap.get(foreignKeyValue) || null; + instance.setRelation(relationshipName, related); + + // Cache individual relationship + if (options.useCache !== false) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const modelType = related?.constructor?.name || 'null'; + this.cache.set(cacheKey, related, modelType, config.type); + } + }); + } + + private async eagerLoadHasMany( + instances: BaseModel[], + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<void> { + if (config.through) { + return await this.eagerLoadManyToMany(instances, relationshipName, config, options); + } + + // Get all local key values + const localKeys = instances + .map((instance) => (instance as any)[config.localKey || 'id']) + .filter((key) => key != null); + + if (localKeys.length === 0) { + instances.forEach((instance) => { + instance.setRelation(relationshipName, []); + }); + return; + } + + // Load all related models + let query = (config.model as any).whereIn(config.foreignKey, localKeys); + + if (options.constraints) { + query = options.constraints(query); + } + + if (options.orderBy) { + query = query.orderBy(options.orderBy.field, options.orderBy.direction); + } + + const relatedModels = await query.exec(); + + // Group by foreign key + const relatedGroups = new Map<string, BaseModel[]>(); + relatedModels.forEach((model: any) => { + const foreignKeyValue = model[config.foreignKey]; + if (!relatedGroups.has(foreignKeyValue)) { + relatedGroups.set(foreignKeyValue, []); + } + relatedGroups.get(foreignKeyValue)!.push(model); + }); + + // Apply limit per instance if specified + if (options.limit) { + relatedGroups.forEach((group) => { + if (group.length > options.limit!) { + group.splice(options.limit!); + } + }); + } + + // Assign to instances and cache + instances.forEach((instance) => { + const localKeyValue = (instance as any)[config.localKey || 'id']; + const related = relatedGroups.get(localKeyValue) || []; + instance.setRelation(relationshipName, related); + + // Cache individual relationship + if (options.useCache !== false) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const modelType = related[0]?.constructor?.name || 'array'; + this.cache.set(cacheKey, related, modelType, config.type); + } + }); + } + + private async eagerLoadHasOne( + instances: BaseModel[], + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<void> { + // Load as hasMany but take only the first result for each instance + await this.eagerLoadHasMany(instances, relationshipName, config, { + ...options, + limit: 1, + }); + + // Convert arrays to single items + instances.forEach((instance) => { + const relatedArray = instance._loadedRelations.get(relationshipName) || []; + const relatedItem = relatedArray[0] || null; + instance._loadedRelations.set(relationshipName, relatedItem); + }); + } + + private async eagerLoadManyToMany( + instances: BaseModel[], + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<void> { + if (!config.through) { + throw new Error('Many-to-many relationships require a through model'); + } + + // Get all local key values + const localKeys = instances + .map((instance) => (instance as any)[config.localKey || 'id']) + .filter((key) => key != null); + + if (localKeys.length === 0) { + instances.forEach((instance) => { + instance.setRelation(relationshipName, []); + }); + return; + } + + // Step 1: Get all junction records + const junctionRecords = await (config.through as any) + .whereIn(config.localKey || 'id', localKeys) + .exec(); + + if (junctionRecords.length === 0) { + instances.forEach((instance) => { + instance.setRelation(relationshipName, []); + }); + return; + } + + // Step 2: Group junction records by local key + const junctionGroups = new Map<string, any[]>(); + junctionRecords.forEach((record: any) => { + const localKeyValue = (record as any)[config.localKey || 'id']; + if (!junctionGroups.has(localKeyValue)) { + junctionGroups.set(localKeyValue, []); + } + junctionGroups.get(localKeyValue)!.push(record); + }); + + // Step 3: Get all foreign keys + const allForeignKeys = junctionRecords.map((record: any) => (record as any)[config.foreignKey]); + const uniqueForeignKeys = [...new Set(allForeignKeys)]; + + // Step 4: Load all related models + let relatedQuery = (config.model as any).whereIn('id', uniqueForeignKeys); + + if (options.constraints) { + relatedQuery = options.constraints(relatedQuery); + } + + if (options.orderBy) { + relatedQuery = relatedQuery.orderBy(options.orderBy.field, options.orderBy.direction); + } + + const relatedModels = await relatedQuery.exec(); + + // Create lookup map for related models + const relatedMap = new Map(); + relatedModels.forEach((model: any) => relatedMap.set(model.id, model)); + + // Step 5: Assign to instances + instances.forEach((instance) => { + const localKeyValue = (instance as any)[config.localKey || 'id']; + const junctionRecordsForInstance = junctionGroups.get(localKeyValue) || []; + + const relatedForInstance = junctionRecordsForInstance + .map((junction) => { + const foreignKeyValue = (junction as any)[config.foreignKey]; + return relatedMap.get(foreignKeyValue); + }) + .filter((related) => related != null); + + // Apply limit if specified + const finalRelated = options.limit + ? relatedForInstance.slice(0, options.limit) + : relatedForInstance; + + instance.setRelation(relationshipName, finalRelated); + + // Cache individual relationship + if (options.useCache !== false) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const modelType = finalRelated[0]?.constructor?.name || 'array'; + this.cache.set(cacheKey, finalRelated, modelType, config.type); + } + }); + } + + private groupInstancesByModel(instances: BaseModel[]): Map<string, BaseModel[]> { + const groups = new Map<string, BaseModel[]>(); + + instances.forEach((instance) => { + const modelName = instance.constructor.name; + if (!groups.has(modelName)) { + groups.set(modelName, []); + } + groups.get(modelName)!.push(instance); + }); + + return groups; + } + + // Cache management methods + invalidateRelationshipCache(instance: BaseModel, relationshipName?: string): number { + if (relationshipName) { + const key = this.cache.generateKey(instance, relationshipName); + return this.cache.invalidate(key) ? 1 : 0; + } else { + return this.cache.invalidateByInstance(instance); + } + } + + invalidateModelCache(modelName: string): number { + return this.cache.invalidateByModel(modelName); + } + + getRelationshipCacheStats(): any { + return { + cache: this.cache.getStats(), + performance: this.cache.analyzePerformance(), + }; + } + + // Preload relationships for better performance + async warmupRelationshipCache(instances: BaseModel[], relationships: string[]): Promise<void> { + await this.cache.warmup(instances, relationships, (instance, relationshipName) => + this.loadRelationship(instance, relationshipName, { useCache: false }), + ); + } + + // Cleanup and maintenance + cleanupExpiredCache(): number { + return this.cache.cleanup(); + } + + clearRelationshipCache(): void { + this.cache.clear(); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
LazyLoader.ts | +
+
+ |
+ 0% | +0/169 | +0% | +0/113 | +0% | +0/37 | +0% | +0/166 | +
RelationshipCache.ts | +
+
+ |
+ 0% | +0/140 | +0% | +0/57 | +0% | +0/28 | +0% | +0/133 | +
RelationshipManager.ts | +
+
+ |
+ 0% | +0/223 | +0% | +0/145 | +0% | +0/44 | +0% | +0/217 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { StoreType } from '../types/framework'; + +export interface OrbitDBInstance { + openDB(name: string, type: string): Promise<any>; + getOrbitDB(): any; + init(): Promise<any>; + stop?(): Promise<void>; +} + +export interface IPFSInstance { + init(): Promise<any>; + getHelia(): any; + getLibp2pInstance(): any; + stop?(): Promise<void>; + pubsub?: { + publish(topic: string, data: string): Promise<void>; + subscribe(topic: string, handler: (message: any) => void): Promise<void>; + unsubscribe(topic: string): Promise<void>; + }; +} + +export class FrameworkOrbitDBService { + private orbitDBService: OrbitDBInstance; + + constructor(orbitDBService: OrbitDBInstance) { + this.orbitDBService = orbitDBService; + } + + async openDatabase(name: string, type: StoreType): Promise<any> { + return await this.orbitDBService.openDB(name, type); + } + + async init(): Promise<void> { + await this.orbitDBService.init(); + } + + async stop(): Promise<void> { + if (this.orbitDBService.stop) { + await this.orbitDBService.stop(); + } + } + + getOrbitDB(): any { + return this.orbitDBService.getOrbitDB(); + } +} + +export class FrameworkIPFSService { + private ipfsService: IPFSInstance; + + constructor(ipfsService: IPFSInstance) { + this.ipfsService = ipfsService; + } + + async init(): Promise<void> { + await this.ipfsService.init(); + } + + async stop(): Promise<void> { + if (this.ipfsService.stop) { + await this.ipfsService.stop(); + } + } + + getHelia(): any { + return this.ipfsService.getHelia(); + } + + getLibp2p(): any { + return this.ipfsService.getLibp2pInstance(); + } + + async getConnectedPeers(): Promise<Map<string, any>> { + const libp2p = this.getLibp2p(); + if (!libp2p) { + return new Map(); + } + + const peers = libp2p.getPeers(); + const peerMap = new Map(); + + for (const peerId of peers) { + peerMap.set(peerId.toString(), peerId); + } + + return peerMap; + } + + async pinOnNode(nodeId: string, cid: string): Promise<void> { + // Implementation depends on your specific pinning setup + // This is a placeholder for the pinning functionality + console.log(`Pinning ${cid} on node ${nodeId}`); + } + + get pubsub() { + return this.ipfsService.pubsub; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
OrbitDBService.ts | +
+
+ |
+ 0% | +0/22 | +0% | +0/6 | +0% | +0/13 | +0% | +0/22 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ShardingConfig, StoreType } from '../types/framework'; +import { FrameworkOrbitDBService } from '../services/OrbitDBService'; + +export interface ShardInfo { + name: string; + index: number; + database: any; + address: string; +} + +export class ShardManager { + private orbitDBService?: FrameworkOrbitDBService; + private shards: Map<string, ShardInfo[]> = new Map(); + private shardConfigs: Map<string, ShardingConfig> = new Map(); + + setOrbitDBService(service: FrameworkOrbitDBService): void { + this.orbitDBService = service; + } + + async createShards( + modelName: string, + config: ShardingConfig, + dbType: StoreType = 'docstore', + ): Promise<void> { + if (!this.orbitDBService) { + throw new Error('OrbitDB service not initialized'); + } + + console.log(`🔀 Creating ${config.count} shards for model: ${modelName}`); + + const shards: ShardInfo[] = []; + this.shardConfigs.set(modelName, config); + + for (let i = 0; i < config.count; i++) { + const shardName = `${modelName.toLowerCase()}-shard-${i}`; + + try { + const shard = await this.createShard(shardName, i, dbType); + shards.push(shard); + + console.log(`✓ Created shard: ${shardName} (${shard.address})`); + } catch (error) { + console.error(`❌ Failed to create shard ${shardName}:`, error); + throw error; + } + } + + this.shards.set(modelName, shards); + console.log(`✅ Created ${shards.length} shards for ${modelName}`); + } + + getShardForKey(modelName: string, key: string): ShardInfo { + const shards = this.shards.get(modelName); + if (!shards || shards.length === 0) { + throw new Error(`No shards found for model ${modelName}`); + } + + const config = this.shardConfigs.get(modelName); + if (!config) { + throw new Error(`No shard configuration found for model ${modelName}`); + } + + const shardIndex = this.calculateShardIndex(key, shards.length, config.strategy); + return shards[shardIndex]; + } + + getAllShards(modelName: string): ShardInfo[] { + return this.shards.get(modelName) || []; + } + + getShardByIndex(modelName: string, index: number): ShardInfo | undefined { + const shards = this.shards.get(modelName); + if (!shards || index < 0 || index >= shards.length) { + return undefined; + } + return shards[index]; + } + + getShardCount(modelName: string): number { + const shards = this.shards.get(modelName); + return shards ? shards.length : 0; + } + + private calculateShardIndex( + key: string, + shardCount: number, + strategy: ShardingConfig['strategy'], + ): number { + switch (strategy) { + case 'hash': + return this.hashSharding(key, shardCount); + + case 'range': + return this.rangeSharding(key, shardCount); + + case 'user': + return this.userSharding(key, shardCount); + + default: + throw new Error(`Unsupported sharding strategy: ${strategy}`); + } + } + + private hashSharding(key: string, shardCount: number): number { + // Consistent hash-based sharding + let hash = 0; + for (let i = 0; i < key.length; i++) { + hash = ((hash << 5) - hash + key.charCodeAt(i)) & 0xffffffff; + } + return Math.abs(hash) % shardCount; + } + + private rangeSharding(key: string, shardCount: number): number { + // Range-based sharding (alphabetical) + const firstChar = key.charAt(0).toLowerCase(); + const charCode = firstChar.charCodeAt(0); + + // Map a-z (97-122) to shard indices + const normalizedCode = Math.max(97, Math.min(122, charCode)); + const range = (normalizedCode - 97) / 25; // 0-1 range + + return Math.floor(range * shardCount); + } + + private userSharding(key: string, shardCount: number): number { + // User-based sharding - similar to hash but optimized for user IDs + return this.hashSharding(key, shardCount); + } + + private async createShard( + shardName: string, + index: number, + dbType: StoreType, + ): Promise<ShardInfo> { + if (!this.orbitDBService) { + throw new Error('OrbitDB service not initialized'); + } + + const database = await this.orbitDBService.openDatabase(shardName, dbType); + + return { + name: shardName, + index, + database, + address: database.address.toString(), + }; + } + + // Global indexing support + async createGlobalIndex(modelName: string, indexName: string): Promise<void> { + if (!this.orbitDBService) { + throw new Error('OrbitDB service not initialized'); + } + + console.log(`📇 Creating global index: ${indexName} for model: ${modelName}`); + + // Create sharded global index + const INDEX_SHARD_COUNT = 4; // Configurable + const indexShards: ShardInfo[] = []; + + for (let i = 0; i < INDEX_SHARD_COUNT; i++) { + const indexShardName = `${indexName}-shard-${i}`; + + try { + const shard = await this.createShard(indexShardName, i, 'keyvalue'); + indexShards.push(shard); + + console.log(`✓ Created index shard: ${indexShardName}`); + } catch (error) { + console.error(`❌ Failed to create index shard ${indexShardName}:`, error); + throw error; + } + } + + // Store index shards + this.shards.set(indexName, indexShards); + + console.log(`✅ Created global index ${indexName} with ${indexShards.length} shards`); + } + + async addToGlobalIndex(indexName: string, key: string, value: any): Promise<void> { + const indexShards = this.shards.get(indexName); + if (!indexShards) { + throw new Error(`Global index ${indexName} not found`); + } + + // Determine which shard to use for this key + const shardIndex = this.hashSharding(key, indexShards.length); + const shard = indexShards[shardIndex]; + + try { + // For keyvalue stores, we use set + await shard.database.set(key, value); + } catch (error) { + console.error(`Failed to add to global index ${indexName}:`, error); + throw error; + } + } + + async getFromGlobalIndex(indexName: string, key: string): Promise<any> { + const indexShards = this.shards.get(indexName); + if (!indexShards) { + throw new Error(`Global index ${indexName} not found`); + } + + // Determine which shard contains this key + const shardIndex = this.hashSharding(key, indexShards.length); + const shard = indexShards[shardIndex]; + + try { + return await shard.database.get(key); + } catch (error) { + console.error(`Failed to get from global index ${indexName}:`, error); + return null; + } + } + + async removeFromGlobalIndex(indexName: string, key: string): Promise<void> { + const indexShards = this.shards.get(indexName); + if (!indexShards) { + throw new Error(`Global index ${indexName} not found`); + } + + // Determine which shard contains this key + const shardIndex = this.hashSharding(key, indexShards.length); + const shard = indexShards[shardIndex]; + + try { + await shard.database.del(key); + } catch (error) { + console.error(`Failed to remove from global index ${indexName}:`, error); + throw error; + } + } + + // Query all shards for a model + async queryAllShards( + modelName: string, + queryFn: (database: any) => Promise<any[]>, + ): Promise<any[]> { + const shards = this.shards.get(modelName); + if (!shards) { + throw new Error(`No shards found for model ${modelName}`); + } + + const results: any[] = []; + + // Query all shards in parallel + const promises = shards.map(async (shard) => { + try { + return await queryFn(shard.database); + } catch (error) { + console.warn(`Query failed on shard ${shard.name}:`, error); + return []; + } + }); + + const shardResults = await Promise.all(promises); + + // Flatten results + for (const shardResult of shardResults) { + results.push(...shardResult); + } + + return results; + } + + // Statistics and monitoring + getShardStatistics(modelName: string): any { + const shards = this.shards.get(modelName); + if (!shards) { + return null; + } + + return { + modelName, + shardCount: shards.length, + shards: shards.map((shard) => ({ + name: shard.name, + index: shard.index, + address: shard.address, + })), + }; + } + + getAllModelsWithShards(): string[] { + return Array.from(this.shards.keys()); + } + + // Cleanup + async stop(): Promise<void> { + console.log('🛑 Stopping ShardManager...'); + + this.shards.clear(); + this.shardConfigs.clear(); + + console.log('✅ ShardManager stopped'); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
ShardManager.ts | +
+
+ |
+ 0% | +0/120 | +0% | +0/36 | +0% | +0/21 | +0% | +0/117 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
models.ts | +
+
+ |
+ 0% | +0/3 | +100% | +0/0 | +0% | +0/1 | +0% | +0/3 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { StoreType, ShardingConfig, PinningConfig, PubSubConfig } from './framework'; + +export interface ModelConfig { + type?: StoreType; + scope?: 'user' | 'global'; + sharding?: ShardingConfig; + pinning?: PinningConfig; + pubsub?: PubSubConfig; + tableName?: string; +} + +export interface FieldConfig { + type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'date'; + required?: boolean; + unique?: boolean; + index?: boolean | 'global'; + default?: any; + validate?: (value: any) => boolean | string; + transform?: (value: any) => any; +} + +export interface RelationshipConfig { + type: 'belongsTo' | 'hasMany' | 'hasOne' | 'manyToMany'; + model: typeof BaseModel; + foreignKey: string; + localKey?: string; + through?: typeof BaseModel; + lazy?: boolean; +} + +export interface UserMappings { + userId: string; + databases: Record<string, string>; +} + +export class ValidationError extends Error { + public errors: string[]; + + constructor(errors: string[]) { + super(`Validation failed: ${errors.join(', ')}`); + this.errors = errors; + this.name = 'ValidationError'; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
framework | +
+
+ |
+ 0% | +0/249 | +0% | +0/129 | +0% | +0/49 | +0% | +0/247 | +
framework/core | +
+
+ |
+ 0% | +0/235 | +0% | +0/110 | +0% | +0/48 | +0% | +0/230 | +
framework/migrations | +
+
+ |
+ 0% | +0/435 | +0% | +0/199 | +0% | +0/89 | +0% | +0/417 | +
framework/models | +
+
+ |
+ 0% | +0/200 | +0% | +0/97 | +0% | +0/44 | +0% | +0/199 | +
framework/models/decorators | +
+
+ |
+ 0% | +0/113 | +0% | +0/93 | +0% | +0/33 | +0% | +0/113 | +
framework/pinning | +
+
+ |
+ 0% | +0/227 | +0% | +0/132 | +0% | +0/44 | +0% | +0/218 | +
framework/pubsub | +
+
+ |
+ 0% | +0/228 | +0% | +0/110 | +0% | +0/37 | +0% | +0/220 | +
framework/query | +
+
+ |
+ 0% | +0/672 | +0% | +0/301 | +0% | +0/162 | +0% | +0/646 | +
framework/relationships | +
+
+ |
+ 0% | +0/532 | +0% | +0/315 | +0% | +0/109 | +0% | +0/516 | +
framework/services | +
+
+ |
+ 0% | +0/22 | +0% | +0/6 | +0% | +0/13 | +0% | +0/22 | +
framework/sharding | +
+
+ |
+ 0% | +0/120 | +0% | +0/36 | +0% | +0/21 | +0% | +0/117 | +
framework/types | +
+
+ |
+ 0% | +0/3 | +100% | +0/0 | +0% | +0/1 | +0% | +0/3 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * DebrosFramework - Main Framework Class + * + * This is the primary entry point for the DebrosFramework, providing a unified + * API that integrates all framework components: + * - Model system with decorators and validation + * - Database management and sharding + * - Query system with optimization + * - Relationship management with lazy/eager loading + * - Automatic pinning and PubSub features + * - Migration system for schema evolution + * - Configuration and lifecycle management + */ + +import { BaseModel } from './models/BaseModel'; +import { ModelRegistry } from './core/ModelRegistry'; +import { DatabaseManager } from './core/DatabaseManager'; +import { ShardManager } from './sharding/ShardManager'; +import { ConfigManager } from './core/ConfigManager'; +import { FrameworkOrbitDBService, FrameworkIPFSService } from './services/OrbitDBService'; +import { QueryCache } from './query/QueryCache'; +import { RelationshipManager } from './relationships/RelationshipManager'; +import { PinningManager } from './pinning/PinningManager'; +import { PubSubManager } from './pubsub/PubSubManager'; +import { MigrationManager } from './migrations/MigrationManager'; +import { FrameworkConfig } from './types/framework'; + +export interface DebrosFrameworkConfig extends FrameworkConfig { + // Environment settings + environment?: 'development' | 'production' | 'test'; + + // Service configurations + orbitdb?: { + directory?: string; + options?: any; + }; + + ipfs?: { + config?: any; + options?: any; + }; + + // Feature toggles + features?: { + autoMigration?: boolean; + automaticPinning?: boolean; + pubsub?: boolean; + queryCache?: boolean; + relationshipCache?: boolean; + }; + + // Performance settings + performance?: { + queryTimeout?: number; + migrationTimeout?: number; + maxConcurrentOperations?: number; + batchSize?: number; + }; + + // Monitoring and logging + monitoring?: { + enableMetrics?: boolean; + logLevel?: 'error' | 'warn' | 'info' | 'debug'; + metricsInterval?: number; + }; +} + +export interface FrameworkMetrics { + uptime: number; + totalModels: number; + totalDatabases: number; + totalShards: number; + queriesExecuted: number; + migrationsRun: number; + cacheHitRate: number; + averageQueryTime: number; + memoryUsage: { + queryCache: number; + relationshipCache: number; + total: number; + }; + performance: { + slowQueries: number; + failedOperations: number; + averageResponseTime: number; + }; +} + +export interface FrameworkStatus { + initialized: boolean; + healthy: boolean; + version: string; + environment: string; + services: { + orbitdb: 'connected' | 'disconnected' | 'error'; + ipfs: 'connected' | 'disconnected' | 'error'; + pinning: 'active' | 'inactive' | 'error'; + pubsub: 'active' | 'inactive' | 'error'; + }; + lastHealthCheck: number; +} + +export class DebrosFramework { + private config: DebrosFrameworkConfig; + private configManager: ConfigManager; + + // Core services + private orbitDBService: FrameworkOrbitDBService | null = null; + private ipfsService: FrameworkIPFSService | null = null; + + // Framework components + private databaseManager: DatabaseManager | null = null; + private shardManager: ShardManager | null = null; + private queryCache: QueryCache | null = null; + private relationshipManager: RelationshipManager | null = null; + private pinningManager: PinningManager | null = null; + private pubsubManager: PubSubManager | null = null; + private migrationManager: MigrationManager | null = null; + + // Framework state + private initialized: boolean = false; + private startTime: number = 0; + private healthCheckInterval: any = null; + private metricsCollector: any = null; + private status: FrameworkStatus; + private metrics: FrameworkMetrics; + + constructor(config: DebrosFrameworkConfig = {}) { + this.config = this.mergeDefaultConfig(config); + this.configManager = new ConfigManager(this.config); + + this.status = { + initialized: false, + healthy: false, + version: '1.0.0', // This would come from package.json + environment: this.config.environment || 'development', + services: { + orbitdb: 'disconnected', + ipfs: 'disconnected', + pinning: 'inactive', + pubsub: 'inactive', + }, + lastHealthCheck: 0, + }; + + this.metrics = { + uptime: 0, + totalModels: 0, + totalDatabases: 0, + totalShards: 0, + queriesExecuted: 0, + migrationsRun: 0, + cacheHitRate: 0, + averageQueryTime: 0, + memoryUsage: { + queryCache: 0, + relationshipCache: 0, + total: 0, + }, + performance: { + slowQueries: 0, + failedOperations: 0, + averageResponseTime: 0, + }, + }; + } + + // Main initialization method + async initialize( + existingOrbitDBService?: any, + existingIPFSService?: any, + overrideConfig?: Partial<DebrosFrameworkConfig>, + ): Promise<void> { + if (this.initialized) { + throw new Error('Framework is already initialized'); + } + + try { + this.startTime = Date.now(); + console.log('🚀 Initializing DebrosFramework...'); + + // Apply config overrides + if (overrideConfig) { + this.config = { ...this.config, ...overrideConfig }; + this.configManager = new ConfigManager(this.config); + } + + // Initialize services + await this.initializeServices(existingOrbitDBService, existingIPFSService); + + // Initialize core components + await this.initializeCoreComponents(); + + // Initialize feature components + await this.initializeFeatureComponents(); + + // Setup global framework access + this.setupGlobalAccess(); + + // Start background processes + await this.startBackgroundProcesses(); + + // Run automatic migrations if enabled + if (this.config.features?.autoMigration && this.migrationManager) { + await this.runAutomaticMigrations(); + } + + this.initialized = true; + this.status.initialized = true; + this.status.healthy = true; + + console.log('✅ DebrosFramework initialized successfully'); + this.logFrameworkInfo(); + } catch (error) { + console.error('❌ Framework initialization failed:', error); + await this.cleanup(); + throw error; + } + } + + // Service initialization + private async initializeServices( + existingOrbitDBService?: any, + existingIPFSService?: any, + ): Promise<void> { + console.log('📡 Initializing core services...'); + + try { + // Initialize IPFS service + if (existingIPFSService) { + this.ipfsService = new FrameworkIPFSService(existingIPFSService); + } else { + // In a real implementation, create IPFS instance + throw new Error('IPFS service is required. Please provide an existing IPFS instance.'); + } + + await this.ipfsService.init(); + this.status.services.ipfs = 'connected'; + console.log('✅ IPFS service initialized'); + + // Initialize OrbitDB service + if (existingOrbitDBService) { + this.orbitDBService = new FrameworkOrbitDBService(existingOrbitDBService); + } else { + // In a real implementation, create OrbitDB instance + throw new Error( + 'OrbitDB service is required. Please provide an existing OrbitDB instance.', + ); + } + + await this.orbitDBService.init(); + this.status.services.orbitdb = 'connected'; + console.log('✅ OrbitDB service initialized'); + } catch (error) { + this.status.services.ipfs = 'error'; + this.status.services.orbitdb = 'error'; + throw new Error(`Service initialization failed: ${error}`); + } + } + + // Core component initialization + private async initializeCoreComponents(): Promise<void> { + console.log('🔧 Initializing core components...'); + + // Database Manager + this.databaseManager = new DatabaseManager(this.orbitDBService!); + await this.databaseManager.initializeAllDatabases(); + console.log('✅ DatabaseManager initialized'); + + // Shard Manager + this.shardManager = new ShardManager(); + this.shardManager.setOrbitDBService(this.orbitDBService!); + + // Initialize shards for registered models + const globalModels = ModelRegistry.getGlobalModels(); + for (const model of globalModels) { + if (model.sharding) { + await this.shardManager.createShards(model.modelName, model.sharding, model.dbType); + } + } + console.log('✅ ShardManager initialized'); + + // Query Cache + if (this.config.features?.queryCache !== false) { + const cacheConfig = this.configManager.cacheConfig; + this.queryCache = new QueryCache(cacheConfig?.maxSize || 1000, cacheConfig?.ttl || 300000); + console.log('✅ QueryCache initialized'); + } + + // Relationship Manager + this.relationshipManager = new RelationshipManager({ + databaseManager: this.databaseManager, + shardManager: this.shardManager, + queryCache: this.queryCache, + }); + console.log('✅ RelationshipManager initialized'); + } + + // Feature component initialization + private async initializeFeatureComponents(): Promise<void> { + console.log('🎛️ Initializing feature components...'); + + // Pinning Manager + if (this.config.features?.automaticPinning !== false) { + this.pinningManager = new PinningManager(this.ipfsService!.getHelia(), { + maxTotalPins: this.config.performance?.maxConcurrentOperations || 10000, + cleanupIntervalMs: 60000, + }); + + // Setup default pinning rules based on config + if (this.config.defaultPinning) { + const globalModels = ModelRegistry.getGlobalModels(); + for (const model of globalModels) { + this.pinningManager.setPinningRule(model.modelName, this.config.defaultPinning); + } + } + + this.status.services.pinning = 'active'; + console.log('✅ PinningManager initialized'); + } + + // PubSub Manager + if (this.config.features?.pubsub !== false) { + this.pubsubManager = new PubSubManager(this.ipfsService!.getHelia(), { + enabled: true, + autoPublishModelEvents: true, + autoPublishDatabaseEvents: true, + topicPrefix: `debros-${this.config.environment || 'dev'}`, + }); + + await this.pubsubManager.initialize(); + this.status.services.pubsub = 'active'; + console.log('✅ PubSubManager initialized'); + } + + // Migration Manager + this.migrationManager = new MigrationManager( + this.databaseManager, + this.shardManager, + this.createMigrationLogger(), + ); + console.log('✅ MigrationManager initialized'); + } + + // Setup global framework access for models + private setupGlobalAccess(): void { + (globalThis as any).__debrosFramework = { + databaseManager: this.databaseManager, + shardManager: this.shardManager, + configManager: this.configManager, + queryCache: this.queryCache, + relationshipManager: this.relationshipManager, + pinningManager: this.pinningManager, + pubsubManager: this.pubsubManager, + migrationManager: this.migrationManager, + framework: this, + }; + } + + // Start background processes + private async startBackgroundProcesses(): Promise<void> { + console.log('⚙️ Starting background processes...'); + + // Health check interval + this.healthCheckInterval = setInterval(() => { + this.performHealthCheck(); + }, 30000); // Every 30 seconds + + // Metrics collection + if (this.config.monitoring?.enableMetrics !== false) { + this.metricsCollector = setInterval(() => { + this.collectMetrics(); + }, this.config.monitoring?.metricsInterval || 60000); // Every minute + } + + console.log('✅ Background processes started'); + } + + // Automatic migration execution + private async runAutomaticMigrations(): Promise<void> { + if (!this.migrationManager) return; + + try { + console.log('🔄 Running automatic migrations...'); + + const pendingMigrations = this.migrationManager.getPendingMigrations(); + if (pendingMigrations.length > 0) { + console.log(`Found ${pendingMigrations.length} pending migrations`); + + const results = await this.migrationManager.runPendingMigrations({ + stopOnError: true, + batchSize: this.config.performance?.batchSize || 100, + }); + + const successful = results.filter((r) => r.success).length; + console.log(`✅ Completed ${successful}/${results.length} migrations`); + + this.metrics.migrationsRun += successful; + } else { + console.log('No pending migrations found'); + } + } catch (error) { + console.error('❌ Automatic migration failed:', error); + if (this.config.environment === 'production') { + // In production, don't fail initialization due to migration errors + console.warn('Continuing initialization despite migration failure'); + } else { + throw error; + } + } + } + + // Public API methods + + // Model registration + registerModel(modelClass: typeof BaseModel, config?: any): void { + ModelRegistry.register(modelClass.name, modelClass, config || {}); + console.log(`📝 Registered model: ${modelClass.name}`); + + this.metrics.totalModels = ModelRegistry.getModelNames().length; + } + + // Get model instance + getModel(modelName: string): typeof BaseModel | null { + return ModelRegistry.get(modelName) || null; + } + + // Database operations + async createUserDatabase(userId: string): Promise<void> { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + await this.databaseManager.createUserDatabases(userId); + this.metrics.totalDatabases++; + } + + async getUserDatabase(userId: string, modelName: string): Promise<any> { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + return await this.databaseManager.getUserDatabase(userId, modelName); + } + + async getGlobalDatabase(modelName: string): Promise<any> { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + return await this.databaseManager.getGlobalDatabase(modelName); + } + + // Migration operations + async runMigration(migrationId: string, options?: any): Promise<any> { + if (!this.migrationManager) { + throw new Error('MigrationManager not initialized'); + } + + const result = await this.migrationManager.runMigration(migrationId, options); + this.metrics.migrationsRun++; + return result; + } + + async registerMigration(migration: any): Promise<void> { + if (!this.migrationManager) { + throw new Error('MigrationManager not initialized'); + } + + this.migrationManager.registerMigration(migration); + } + + getPendingMigrations(modelName?: string): any[] { + if (!this.migrationManager) { + return []; + } + + return this.migrationManager.getPendingMigrations(modelName); + } + + // Cache management + clearQueryCache(): void { + if (this.queryCache) { + this.queryCache.clear(); + } + } + + clearRelationshipCache(): void { + if (this.relationshipManager) { + this.relationshipManager.clearRelationshipCache(); + } + } + + async warmupCaches(): Promise<void> { + console.log('🔥 Warming up caches...'); + + if (this.queryCache) { + // Warm up common queries + const commonQueries: any[] = []; // Would be populated with actual queries + await this.queryCache.warmup(commonQueries); + } + + if (this.relationshipManager && this.pinningManager) { + // Warm up relationship cache for popular content + // Implementation would depend on actual models + } + + console.log('✅ Cache warmup completed'); + } + + // Health and monitoring + performHealthCheck(): void { + try { + this.status.lastHealthCheck = Date.now(); + + // Check service health + this.status.services.orbitdb = this.orbitDBService ? 'connected' : 'disconnected'; + this.status.services.ipfs = this.ipfsService ? 'connected' : 'disconnected'; + this.status.services.pinning = this.pinningManager ? 'active' : 'inactive'; + this.status.services.pubsub = this.pubsubManager ? 'active' : 'inactive'; + + // Overall health check + const allServicesHealthy = Object.values(this.status.services).every( + (status) => status === 'connected' || status === 'active', + ); + + this.status.healthy = this.initialized && allServicesHealthy; + } catch (error) { + console.error('Health check failed:', error); + this.status.healthy = false; + } + } + + collectMetrics(): void { + try { + this.metrics.uptime = Date.now() - this.startTime; + this.metrics.totalModels = ModelRegistry.getModelNames().length; + + if (this.queryCache) { + const cacheStats = this.queryCache.getStats(); + this.metrics.cacheHitRate = cacheStats.hitRate; + this.metrics.averageQueryTime = 0; // Would need to be calculated from cache stats + this.metrics.memoryUsage.queryCache = cacheStats.size * 1024; // Estimate + } + + if (this.relationshipManager) { + const relStats = this.relationshipManager.getRelationshipCacheStats(); + this.metrics.memoryUsage.relationshipCache = relStats.cache.memoryUsage; + } + + this.metrics.memoryUsage.total = + this.metrics.memoryUsage.queryCache + this.metrics.memoryUsage.relationshipCache; + } catch (error) { + console.error('Metrics collection failed:', error); + } + } + + getStatus(): FrameworkStatus { + return { ...this.status }; + } + + getMetrics(): FrameworkMetrics { + this.collectMetrics(); // Ensure fresh metrics + return { ...this.metrics }; + } + + getConfig(): DebrosFrameworkConfig { + return { ...this.config }; + } + + // Component access + getDatabaseManager(): DatabaseManager | null { + return this.databaseManager; + } + + getShardManager(): ShardManager | null { + return this.shardManager; + } + + getRelationshipManager(): RelationshipManager | null { + return this.relationshipManager; + } + + getPinningManager(): PinningManager | null { + return this.pinningManager; + } + + getPubSubManager(): PubSubManager | null { + return this.pubsubManager; + } + + getMigrationManager(): MigrationManager | null { + return this.migrationManager; + } + + // Framework lifecycle + async stop(): Promise<void> { + if (!this.initialized) { + return; + } + + console.log('🛑 Stopping DebrosFramework...'); + + try { + await this.cleanup(); + this.initialized = false; + this.status.initialized = false; + this.status.healthy = false; + + console.log('✅ DebrosFramework stopped successfully'); + } catch (error) { + console.error('❌ Error during framework shutdown:', error); + throw error; + } + } + + async restart(newConfig?: Partial<DebrosFrameworkConfig>): Promise<void> { + console.log('🔄 Restarting DebrosFramework...'); + + const orbitDB = this.orbitDBService?.getOrbitDB(); + const ipfs = this.ipfsService?.getHelia(); + + await this.stop(); + + if (newConfig) { + this.config = { ...this.config, ...newConfig }; + } + + await this.initialize(orbitDB, ipfs); + } + + // Cleanup method + private async cleanup(): Promise<void> { + // Stop background processes + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + this.healthCheckInterval = null; + } + + if (this.metricsCollector) { + clearInterval(this.metricsCollector); + this.metricsCollector = null; + } + + // Cleanup components + if (this.pubsubManager) { + await this.pubsubManager.shutdown(); + } + + if (this.pinningManager) { + await this.pinningManager.shutdown(); + } + + if (this.migrationManager) { + await this.migrationManager.cleanup(); + } + + if (this.queryCache) { + this.queryCache.clear(); + } + + if (this.relationshipManager) { + this.relationshipManager.clearRelationshipCache(); + } + + if (this.databaseManager) { + await this.databaseManager.stop(); + } + + if (this.shardManager) { + await this.shardManager.stop(); + } + + // Clear global access + delete (globalThis as any).__debrosFramework; + } + + // Utility methods + private mergeDefaultConfig(config: DebrosFrameworkConfig): DebrosFrameworkConfig { + return { + environment: 'development', + features: { + autoMigration: true, + automaticPinning: true, + pubsub: true, + queryCache: true, + relationshipCache: true, + }, + performance: { + queryTimeout: 30000, + migrationTimeout: 300000, + maxConcurrentOperations: 100, + batchSize: 100, + }, + monitoring: { + enableMetrics: true, + logLevel: 'info', + metricsInterval: 60000, + }, + ...config, + }; + } + + private createMigrationLogger(): any { + const logLevel = this.config.monitoring?.logLevel || 'info'; + + return { + info: (message: string, meta?: any) => { + if (['info', 'debug'].includes(logLevel)) { + console.log(`[MIGRATION INFO] ${message}`, meta || ''); + } + }, + warn: (message: string, meta?: any) => { + if (['warn', 'info', 'debug'].includes(logLevel)) { + console.warn(`[MIGRATION WARN] ${message}`, meta || ''); + } + }, + error: (message: string, meta?: any) => { + console.error(`[MIGRATION ERROR] ${message}`, meta || ''); + }, + debug: (message: string, meta?: any) => { + if (logLevel === 'debug') { + console.log(`[MIGRATION DEBUG] ${message}`, meta || ''); + } + }, + }; + } + + private logFrameworkInfo(): void { + console.log('\n📋 DebrosFramework Information:'); + console.log('=============================='); + console.log(`Version: ${this.status.version}`); + console.log(`Environment: ${this.status.environment}`); + console.log(`Models registered: ${this.metrics.totalModels}`); + console.log( + `Services: ${Object.entries(this.status.services) + .map(([name, status]) => `${name}:${status}`) + .join(', ')}`, + ); + console.log( + `Features enabled: ${Object.entries(this.config.features || {}) + .filter(([, enabled]) => enabled) + .map(([feature]) => feature) + .join(', ')}`, + ); + console.log(''); + } + + // Static factory methods + static async create(config: DebrosFrameworkConfig = {}): Promise<DebrosFramework> { + const framework = new DebrosFramework(config); + return framework; + } + + static async createWithServices( + orbitDBService: any, + ipfsService: any, + config: DebrosFrameworkConfig = {}, + ): Promise<DebrosFramework> { + const framework = new DebrosFramework(config); + await framework.initialize(orbitDBService, ipfsService); + return framework; + } +} + +// Export the main framework class as default +export default DebrosFramework; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { FrameworkConfig, CacheConfig, PinningConfig } from '../types/framework'; + +export interface DatabaseConfig { + userDirectoryShards?: number; + defaultGlobalShards?: number; + cacheSize?: number; +} + +export interface ExtendedFrameworkConfig extends FrameworkConfig { + database?: DatabaseConfig; + debug?: boolean; + logLevel?: 'error' | 'warn' | 'info' | 'debug'; +} + +export class ConfigManager { + private config: ExtendedFrameworkConfig; + private defaults: ExtendedFrameworkConfig = { + cache: { + enabled: true, + maxSize: 1000, + ttl: 300000, // 5 minutes + }, + defaultPinning: { + strategy: 'fixed' as const, + factor: 2, + }, + database: { + userDirectoryShards: 4, + defaultGlobalShards: 8, + cacheSize: 100, + }, + autoMigration: true, + debug: false, + logLevel: 'info', + }; + + constructor(config: ExtendedFrameworkConfig = {}) { + this.config = this.mergeWithDefaults(config); + this.validateConfig(); + } + + private mergeWithDefaults(config: ExtendedFrameworkConfig): ExtendedFrameworkConfig { + return { + ...this.defaults, + ...config, + cache: { + ...this.defaults.cache, + ...config.cache, + }, + defaultPinning: { + ...this.defaults.defaultPinning, + ...(config.defaultPinning || {}), + }, + database: { + ...this.defaults.database, + ...config.database, + }, + }; + } + + private validateConfig(): void { + // Validate cache configuration + if (this.config.cache) { + if (this.config.cache.maxSize && this.config.cache.maxSize < 1) { + throw new Error('Cache maxSize must be at least 1'); + } + if (this.config.cache.ttl && this.config.cache.ttl < 1000) { + throw new Error('Cache TTL must be at least 1000ms'); + } + } + + // Validate pinning configuration + if (this.config.defaultPinning) { + if (this.config.defaultPinning.factor && this.config.defaultPinning.factor < 1) { + throw new Error('Pinning factor must be at least 1'); + } + } + + // Validate database configuration + if (this.config.database) { + if ( + this.config.database.userDirectoryShards && + this.config.database.userDirectoryShards < 1 + ) { + throw new Error('User directory shards must be at least 1'); + } + if ( + this.config.database.defaultGlobalShards && + this.config.database.defaultGlobalShards < 1 + ) { + throw new Error('Default global shards must be at least 1'); + } + } + } + + // Getters for configuration values + get cacheConfig(): CacheConfig | undefined { + return this.config.cache; + } + + get defaultPinningConfig(): PinningConfig | undefined { + return this.config.defaultPinning; + } + + get databaseConfig(): DatabaseConfig | undefined { + return this.config.database; + } + + get autoMigration(): boolean { + return this.config.autoMigration || false; + } + + get debug(): boolean { + return this.config.debug || false; + } + + get logLevel(): string { + return this.config.logLevel || 'info'; + } + + // Update configuration at runtime + updateConfig(newConfig: Partial<ExtendedFrameworkConfig>): void { + this.config = this.mergeWithDefaults({ + ...this.config, + ...newConfig, + }); + this.validateConfig(); + } + + // Get full configuration + getConfig(): ExtendedFrameworkConfig { + return { ...this.config }; + } + + // Configuration presets + static developmentConfig(): ExtendedFrameworkConfig { + return { + debug: true, + logLevel: 'debug', + cache: { + enabled: true, + maxSize: 100, + ttl: 60000, // 1 minute for development + }, + database: { + userDirectoryShards: 2, + defaultGlobalShards: 2, + cacheSize: 50, + }, + defaultPinning: { + strategy: 'fixed' as const, + factor: 1, // Minimal pinning for development + }, + }; + } + + static productionConfig(): ExtendedFrameworkConfig { + return { + debug: false, + logLevel: 'warn', + cache: { + enabled: true, + maxSize: 10000, + ttl: 600000, // 10 minutes + }, + database: { + userDirectoryShards: 16, + defaultGlobalShards: 32, + cacheSize: 1000, + }, + defaultPinning: { + strategy: 'popularity' as const, + factor: 5, // Higher redundancy for production + }, + }; + } + + static testConfig(): ExtendedFrameworkConfig { + return { + debug: true, + logLevel: 'error', // Minimal logging during tests + cache: { + enabled: false, // Disable caching for predictable tests + }, + database: { + userDirectoryShards: 1, + defaultGlobalShards: 1, + cacheSize: 10, + }, + defaultPinning: { + strategy: 'fixed', + factor: 1, + }, + autoMigration: false, // Manual migration control in tests + }; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ModelRegistry } from './ModelRegistry'; +import { FrameworkOrbitDBService } from '../services/OrbitDBService'; +import { StoreType } from '../types/framework'; +import { UserMappings } from '../types/models'; + +export class UserMappingsData implements UserMappings { + constructor( + public userId: string, + public databases: Record<string, string>, + ) {} +} + +export class DatabaseManager { + private orbitDBService: FrameworkOrbitDBService; + private databases: Map<string, any> = new Map(); + private userMappings: Map<string, any> = new Map(); + private globalDatabases: Map<string, any> = new Map(); + private globalDirectoryShards: any[] = []; + private initialized: boolean = false; + + constructor(orbitDBService: FrameworkOrbitDBService) { + this.orbitDBService = orbitDBService; + } + + async initializeAllDatabases(): Promise<void> { + if (this.initialized) { + return; + } + + console.log('🚀 Initializing DebrosFramework databases...'); + + // Initialize global databases first + await this.initializeGlobalDatabases(); + + // Initialize system databases (user directory, etc.) + await this.initializeSystemDatabases(); + + this.initialized = true; + console.log('✅ Database initialization complete'); + } + + private async initializeGlobalDatabases(): Promise<void> { + const globalModels = ModelRegistry.getGlobalModels(); + + console.log(`📊 Creating ${globalModels.length} global databases...`); + + for (const model of globalModels) { + const dbName = `global-${model.modelName.toLowerCase()}`; + + try { + const db = await this.createDatabase(dbName, model.dbType, 'global'); + this.globalDatabases.set(model.modelName, db); + + console.log(`✓ Created global database: ${dbName} (${model.dbType})`); + } catch (error) { + console.error(`❌ Failed to create global database ${dbName}:`, error); + throw error; + } + } + } + + private async initializeSystemDatabases(): Promise<void> { + console.log('🔧 Creating system databases...'); + + // Create global user directory shards + const DIRECTORY_SHARD_COUNT = 4; // Configurable + + for (let i = 0; i < DIRECTORY_SHARD_COUNT; i++) { + const shardName = `global-user-directory-shard-${i}`; + try { + const shard = await this.createDatabase(shardName, 'keyvalue', 'system'); + this.globalDirectoryShards.push(shard); + + console.log(`✓ Created directory shard: ${shardName}`); + } catch (error) { + console.error(`❌ Failed to create directory shard ${shardName}:`, error); + throw error; + } + } + + console.log(`✅ Created ${this.globalDirectoryShards.length} directory shards`); + } + + async createUserDatabases(userId: string): Promise<UserMappingsData> { + console.log(`👤 Creating databases for user: ${userId}`); + + const userScopedModels = ModelRegistry.getUserScopedModels(); + const databases: Record<string, string> = {}; + + // Create mappings database first + const mappingsDBName = `${userId}-mappings`; + const mappingsDB = await this.createDatabase(mappingsDBName, 'keyvalue', 'user'); + + // Create database for each user-scoped model + for (const model of userScopedModels) { + const dbName = `${userId}-${model.modelName.toLowerCase()}`; + + try { + const db = await this.createDatabase(dbName, model.dbType, 'user'); + databases[`${model.modelName.toLowerCase()}DB`] = db.address.toString(); + + console.log(`✓ Created user database: ${dbName} (${model.dbType})`); + } catch (error) { + console.error(`❌ Failed to create user database ${dbName}:`, error); + throw error; + } + } + + // Store mappings in the mappings database + await mappingsDB.set('mappings', databases); + console.log(`✓ Stored database mappings for user ${userId}`); + + // Register in global directory + await this.registerUserInDirectory(userId, mappingsDB.address.toString()); + + const userMappings = new UserMappingsData(userId, databases); + + // Cache for future use + this.userMappings.set(userId, userMappings); + + console.log(`✅ User databases created successfully for ${userId}`); + return userMappings; + } + + async getUserDatabase(userId: string, modelName: string): Promise<any> { + const mappings = await this.getUserMappings(userId); + const dbKey = `${modelName.toLowerCase()}DB`; + const dbAddress = mappings.databases[dbKey]; + + if (!dbAddress) { + throw new Error(`Database not found for user ${userId} and model ${modelName}`); + } + + // Check if we have this database cached + const cacheKey = `${userId}-${modelName}`; + if (this.databases.has(cacheKey)) { + return this.databases.get(cacheKey); + } + + // Open the database + const db = await this.openDatabase(dbAddress); + this.databases.set(cacheKey, db); + + return db; + } + + async getUserMappings(userId: string): Promise<UserMappingsData> { + // Check cache first + if (this.userMappings.has(userId)) { + return this.userMappings.get(userId); + } + + // Get from global directory + const shardIndex = this.getShardIndex(userId, this.globalDirectoryShards.length); + const shard = this.globalDirectoryShards[shardIndex]; + + if (!shard) { + throw new Error('Global directory not initialized'); + } + + const mappingsAddress = await shard.get(userId); + if (!mappingsAddress) { + throw new Error(`User ${userId} not found in directory`); + } + + const mappingsDB = await this.openDatabase(mappingsAddress); + const mappings = await mappingsDB.get('mappings'); + + if (!mappings) { + throw new Error(`No database mappings found for user ${userId}`); + } + + const userMappings = new UserMappingsData(userId, mappings); + + // Cache for future use + this.userMappings.set(userId, userMappings); + + return userMappings; + } + + async getGlobalDatabase(modelName: string): Promise<any> { + const db = this.globalDatabases.get(modelName); + if (!db) { + throw new Error(`Global database not found for model: ${modelName}`); + } + return db; + } + + async getGlobalDirectoryShards(): Promise<any[]> { + return this.globalDirectoryShards; + } + + private async createDatabase(name: string, type: StoreType, _scope: string): Promise<any> { + try { + const db = await this.orbitDBService.openDatabase(name, type); + + // Store database reference + this.databases.set(name, db); + + return db; + } catch (error) { + console.error(`Failed to create database ${name}:`, error); + throw new Error(`Database creation failed for ${name}: ${error}`); + } + } + + private async openDatabase(address: string): Promise<any> { + try { + // Check if we already have this database cached by address + if (this.databases.has(address)) { + return this.databases.get(address); + } + + // Open database by address (implementation may vary based on OrbitDB version) + const orbitdb = this.orbitDBService.getOrbitDB(); + const db = await orbitdb.open(address); + + // Cache the database + this.databases.set(address, db); + + return db; + } catch (error) { + console.error(`Failed to open database at address ${address}:`, error); + throw new Error(`Database opening failed: ${error}`); + } + } + + private async registerUserInDirectory(userId: string, mappingsAddress: string): Promise<void> { + const shardIndex = this.getShardIndex(userId, this.globalDirectoryShards.length); + const shard = this.globalDirectoryShards[shardIndex]; + + if (!shard) { + throw new Error('Global directory shards not initialized'); + } + + try { + await shard.set(userId, mappingsAddress); + console.log(`✓ Registered user ${userId} in directory shard ${shardIndex}`); + } catch (error) { + console.error(`Failed to register user ${userId} in directory:`, error); + throw error; + } + } + + private getShardIndex(key: string, shardCount: number): number { + // Simple hash-based sharding + let hash = 0; + for (let i = 0; i < key.length; i++) { + hash = ((hash << 5) - hash + key.charCodeAt(i)) & 0xffffffff; + } + return Math.abs(hash) % shardCount; + } + + // Database operation helpers + async getAllDocuments(database: any, dbType: StoreType): Promise<any[]> { + try { + switch (dbType) { + case 'eventlog': + const iterator = database.iterator(); + return iterator.collect(); + + case 'keyvalue': + return Object.values(database.all()); + + case 'docstore': + return database.query(() => true); + + case 'feed': + const feedIterator = database.iterator(); + return feedIterator.collect(); + + case 'counter': + return [{ value: database.value, id: database.id }]; + + default: + throw new Error(`Unsupported database type: ${dbType}`); + } + } catch (error) { + console.error(`Error fetching documents from ${dbType} database:`, error); + throw error; + } + } + + async addDocument(database: any, dbType: StoreType, data: any): Promise<string> { + try { + switch (dbType) { + case 'eventlog': + return await database.add(data); + + case 'keyvalue': + await database.set(data.id, data); + return data.id; + + case 'docstore': + return await database.put(data); + + case 'feed': + return await database.add(data); + + case 'counter': + await database.inc(data.amount || 1); + return database.id; + + default: + throw new Error(`Unsupported database type: ${dbType}`); + } + } catch (error) { + console.error(`Error adding document to ${dbType} database:`, error); + throw error; + } + } + + async updateDocument(database: any, dbType: StoreType, id: string, data: any): Promise<void> { + try { + switch (dbType) { + case 'keyvalue': + await database.set(id, data); + break; + + case 'docstore': + await database.put(data); + break; + + default: + // For append-only stores, we add a new entry + await this.addDocument(database, dbType, data); + } + } catch (error) { + console.error(`Error updating document in ${dbType} database:`, error); + throw error; + } + } + + async deleteDocument(database: any, dbType: StoreType, id: string): Promise<void> { + try { + switch (dbType) { + case 'keyvalue': + await database.del(id); + break; + + case 'docstore': + await database.del(id); + break; + + default: + // For append-only stores, we might add a deletion marker + await this.addDocument(database, dbType, { _deleted: true, id, deletedAt: Date.now() }); + } + } catch (error) { + console.error(`Error deleting document from ${dbType} database:`, error); + throw error; + } + } + + // Cleanup methods + async stop(): Promise<void> { + console.log('🛑 Stopping DatabaseManager...'); + + // Clear caches + this.databases.clear(); + this.userMappings.clear(); + this.globalDatabases.clear(); + this.globalDirectoryShards = []; + + this.initialized = false; + console.log('✅ DatabaseManager stopped'); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { ModelConfig } from '../types/models'; +import { StoreType } from '../types/framework'; + +export class ModelRegistry { + private static models: Map<string, typeof BaseModel> = new Map(); + private static configs: Map<string, ModelConfig> = new Map(); + + static register(name: string, modelClass: typeof BaseModel, config: ModelConfig): void { + this.models.set(name, modelClass); + this.configs.set(name, config); + + // Validate model configuration + this.validateModel(modelClass, config); + + console.log(`Registered model: ${name} with scope: ${config.scope || 'global'}`); + } + + static get(name: string): typeof BaseModel | undefined { + return this.models.get(name); + } + + static getConfig(name: string): ModelConfig | undefined { + return this.configs.get(name); + } + + static getAllModels(): Map<string, typeof BaseModel> { + return new Map(this.models); + } + + static getUserScopedModels(): Array<typeof BaseModel> { + return Array.from(this.models.values()).filter((model) => model.scope === 'user'); + } + + static getGlobalModels(): Array<typeof BaseModel> { + return Array.from(this.models.values()).filter((model) => model.scope === 'global'); + } + + static getModelNames(): string[] { + return Array.from(this.models.keys()); + } + + static clear(): void { + this.models.clear(); + this.configs.clear(); + } + + private static validateModel(modelClass: typeof BaseModel, config: ModelConfig): void { + // Validate model name + if (!modelClass.name) { + throw new Error('Model class must have a name'); + } + + // Validate database type + if (config.type && !this.isValidStoreType(config.type)) { + throw new Error(`Invalid store type: ${config.type}`); + } + + // Validate scope + if (config.scope && !['user', 'global'].includes(config.scope)) { + throw new Error(`Invalid scope: ${config.scope}. Must be 'user' or 'global'`); + } + + // Validate sharding configuration + if (config.sharding) { + this.validateShardingConfig(config.sharding); + } + + // Validate pinning configuration + if (config.pinning) { + this.validatePinningConfig(config.pinning); + } + + console.log(`✓ Model ${modelClass.name} configuration validated`); + } + + private static isValidStoreType(type: StoreType): boolean { + return ['eventlog', 'keyvalue', 'docstore', 'counter', 'feed'].includes(type); + } + + private static validateShardingConfig(config: any): void { + if (!config.strategy || !['hash', 'range', 'user'].includes(config.strategy)) { + throw new Error('Sharding strategy must be one of: hash, range, user'); + } + + if (!config.count || config.count < 1) { + throw new Error('Sharding count must be a positive number'); + } + + if (!config.key) { + throw new Error('Sharding key is required'); + } + } + + private static validatePinningConfig(config: any): void { + if (config.strategy && !['fixed', 'popularity', 'tiered'].includes(config.strategy)) { + throw new Error('Pinning strategy must be one of: fixed, popularity, tiered'); + } + + if (config.factor && (typeof config.factor !== 'number' || config.factor < 1)) { + throw new Error('Pinning factor must be a positive number'); + } + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
ConfigManager.ts | +
+
+ |
+ 0% | +0/29 | +0% | +0/35 | +0% | +0/14 | +0% | +0/29 | +
DatabaseManager.ts | +
+
+ |
+ 0% | +0/168 | +0% | +0/40 | +0% | +0/20 | +0% | +0/165 | +
ModelRegistry.ts | +
+
+ |
+ 0% | +0/38 | +0% | +0/35 | +0% | +0/14 | +0% | +0/36 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
DebrosFramework.ts | +
+
+ |
+ 0% | +0/249 | +0% | +0/129 | +0% | +0/49 | +0% | +0/247 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * MigrationBuilder - Fluent API for Creating Migrations + * + * This class provides a convenient fluent interface for creating migration objects + * with built-in validation and common operation patterns. + */ + +import { Migration, MigrationOperation, MigrationValidator } from './MigrationManager'; +import { FieldConfig } from '../types/models'; + +export class MigrationBuilder { + private migration: Partial<Migration>; + private upOperations: MigrationOperation[] = []; + private downOperations: MigrationOperation[] = []; + private validators: MigrationValidator[] = []; + + constructor(id: string, version: string, name: string) { + this.migration = { + id, + version, + name, + description: '', + targetModels: [], + createdAt: Date.now(), + tags: [], + }; + } + + // Basic migration metadata + description(desc: string): this { + this.migration.description = desc; + return this; + } + + author(author: string): this { + this.migration.author = author; + return this; + } + + tags(...tags: string[]): this { + this.migration.tags = tags; + return this; + } + + targetModels(...models: string[]): this { + this.migration.targetModels = models; + return this; + } + + dependencies(...migrationIds: string[]): this { + this.migration.dependencies = migrationIds; + return this; + } + + // Field operations + addField(modelName: string, fieldName: string, fieldConfig: FieldConfig): this { + this.upOperations.push({ + type: 'add_field', + modelName, + fieldName, + fieldConfig, + }); + + // Auto-generate reverse operation + this.downOperations.unshift({ + type: 'remove_field', + modelName, + fieldName, + }); + + this.ensureTargetModel(modelName); + return this; + } + + removeField(modelName: string, fieldName: string, preserveData: boolean = false): this { + this.upOperations.push({ + type: 'remove_field', + modelName, + fieldName, + }); + + if (!preserveData) { + // Cannot auto-reverse field removal without knowing the original config + this.downOperations.unshift({ + type: 'custom', + modelName, + customOperation: async (context) => { + context.logger.warn(`Cannot reverse removal of field ${fieldName} - data may be lost`); + }, + }); + } + + this.ensureTargetModel(modelName); + return this; + } + + modifyField( + modelName: string, + fieldName: string, + newFieldConfig: FieldConfig, + oldFieldConfig?: FieldConfig, + ): this { + this.upOperations.push({ + type: 'modify_field', + modelName, + fieldName, + fieldConfig: newFieldConfig, + }); + + if (oldFieldConfig) { + this.downOperations.unshift({ + type: 'modify_field', + modelName, + fieldName, + fieldConfig: oldFieldConfig, + }); + } + + this.ensureTargetModel(modelName); + return this; + } + + renameField(modelName: string, oldFieldName: string, newFieldName: string): this { + this.upOperations.push({ + type: 'rename_field', + modelName, + fieldName: oldFieldName, + newFieldName, + }); + + // Auto-generate reverse operation + this.downOperations.unshift({ + type: 'rename_field', + modelName, + fieldName: newFieldName, + newFieldName: oldFieldName, + }); + + this.ensureTargetModel(modelName); + return this; + } + + // Data transformation operations + transformData( + modelName: string, + transformer: (data: any) => any, + reverseTransformer?: (data: any) => any, + ): this { + this.upOperations.push({ + type: 'transform_data', + modelName, + transformer, + }); + + if (reverseTransformer) { + this.downOperations.unshift({ + type: 'transform_data', + modelName, + transformer: reverseTransformer, + }); + } + + this.ensureTargetModel(modelName); + return this; + } + + // Custom operations + customOperation( + modelName: string, + operation: (context: any) => Promise<void>, + rollbackOperation?: (context: any) => Promise<void>, + ): this { + this.upOperations.push({ + type: 'custom', + modelName, + customOperation: operation, + }); + + if (rollbackOperation) { + this.downOperations.unshift({ + type: 'custom', + modelName, + customOperation: rollbackOperation, + }); + } + + this.ensureTargetModel(modelName); + return this; + } + + // Common patterns + addTimestamps(modelName: string): this { + this.addField(modelName, 'createdAt', { + type: 'number', + required: false, + default: Date.now(), + }); + + this.addField(modelName, 'updatedAt', { + type: 'number', + required: false, + default: Date.now(), + }); + + return this; + } + + addSoftDeletes(modelName: string): this { + this.addField(modelName, 'deletedAt', { + type: 'number', + required: false, + default: null, + }); + + return this; + } + + addUuid(modelName: string, fieldName: string = 'uuid'): this { + this.addField(modelName, fieldName, { + type: 'string', + required: true, + unique: true, + default: () => this.generateUuid(), + }); + + return this; + } + + renameModel(oldModelName: string, newModelName: string): this { + // This would require more complex operations across the entire system + this.customOperation( + oldModelName, + async (context) => { + context.logger.info(`Renaming model ${oldModelName} to ${newModelName}`); + // Implementation would involve updating model registry, database names, etc. + }, + async (context) => { + context.logger.info(`Reverting model rename ${newModelName} to ${oldModelName}`); + }, + ); + + return this; + } + + // Migration patterns for common scenarios + createIndex(modelName: string, fieldNames: string[], options: any = {}): this { + this.upOperations.push({ + type: 'add_index', + modelName, + indexConfig: { + fields: fieldNames, + ...options, + }, + }); + + this.downOperations.unshift({ + type: 'remove_index', + modelName, + indexConfig: { + fields: fieldNames, + ...options, + }, + }); + + this.ensureTargetModel(modelName); + return this; + } + + // Data migration helpers + migrateData( + fromModel: string, + toModel: string, + fieldMapping: Record<string, string>, + options: { + batchSize?: number; + condition?: (data: any) => boolean; + transform?: (data: any) => any; + } = {}, + ): this { + this.customOperation(fromModel, async (context) => { + context.logger.info(`Migrating data from ${fromModel} to ${toModel}`); + + const records = await context.databaseManager.getAllRecords(fromModel); + const batchSize = options.batchSize || 100; + + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (options.condition && !options.condition(record)) { + continue; + } + + const newRecord: any = {}; + + // Map fields + for (const [oldField, newField] of Object.entries(fieldMapping)) { + if (oldField in record) { + newRecord[newField] = record[oldField]; + } + } + + // Apply transformation if provided + if (options.transform) { + Object.assign(newRecord, options.transform(newRecord)); + } + + await context.databaseManager.createRecord(toModel, newRecord); + } + } + }); + + this.ensureTargetModel(fromModel); + this.ensureTargetModel(toModel); + return this; + } + + // Validation + addValidator( + name: string, + description: string, + validateFn: (context: any) => Promise<any>, + ): this { + this.validators.push({ + name, + description, + validate: validateFn, + }); + return this; + } + + validateFieldExists(modelName: string, fieldName: string): this { + return this.addValidator( + `validate_${fieldName}_exists`, + `Ensure field ${fieldName} exists in ${modelName}`, + async (_context) => { + // Implementation would check if field exists + return { valid: true, errors: [], warnings: [] }; + }, + ); + } + + validateDataIntegrity(modelName: string, checkFn: (records: any[]) => any): this { + return this.addValidator( + `validate_${modelName}_integrity`, + `Validate data integrity for ${modelName}`, + async (context) => { + const records = await context.databaseManager.getAllRecords(modelName); + return checkFn(records); + }, + ); + } + + // Build the final migration + build(): Migration { + if (!this.migration.targetModels || this.migration.targetModels.length === 0) { + throw new Error('Migration must have at least one target model'); + } + + if (this.upOperations.length === 0) { + throw new Error('Migration must have at least one operation'); + } + + return { + id: this.migration.id!, + version: this.migration.version!, + name: this.migration.name!, + description: this.migration.description!, + targetModels: this.migration.targetModels!, + up: this.upOperations, + down: this.downOperations, + dependencies: this.migration.dependencies, + validators: this.validators.length > 0 ? this.validators : undefined, + createdAt: this.migration.createdAt!, + author: this.migration.author, + tags: this.migration.tags, + }; + } + + // Helper methods + private ensureTargetModel(modelName: string): void { + if (!this.migration.targetModels!.includes(modelName)) { + this.migration.targetModels!.push(modelName); + } + } + + private generateUuid(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } + + // Static factory methods for common migration types + static create(id: string, version: string, name: string): MigrationBuilder { + return new MigrationBuilder(id, version, name); + } + + static addFieldMigration( + id: string, + version: string, + modelName: string, + fieldName: string, + fieldConfig: FieldConfig, + ): Migration { + return new MigrationBuilder(id, version, `Add ${fieldName} to ${modelName}`) + .description(`Add new field ${fieldName} to ${modelName} model`) + .addField(modelName, fieldName, fieldConfig) + .build(); + } + + static removeFieldMigration( + id: string, + version: string, + modelName: string, + fieldName: string, + ): Migration { + return new MigrationBuilder(id, version, `Remove ${fieldName} from ${modelName}`) + .description(`Remove field ${fieldName} from ${modelName} model`) + .removeField(modelName, fieldName) + .build(); + } + + static renameFieldMigration( + id: string, + version: string, + modelName: string, + oldFieldName: string, + newFieldName: string, + ): Migration { + return new MigrationBuilder( + id, + version, + `Rename ${oldFieldName} to ${newFieldName} in ${modelName}`, + ) + .description(`Rename field ${oldFieldName} to ${newFieldName} in ${modelName} model`) + .renameField(modelName, oldFieldName, newFieldName) + .build(); + } + + static dataTransformMigration( + id: string, + version: string, + modelName: string, + description: string, + transformer: (data: any) => any, + reverseTransformer?: (data: any) => any, + ): Migration { + return new MigrationBuilder(id, version, `Transform data in ${modelName}`) + .description(description) + .transformData(modelName, transformer, reverseTransformer) + .build(); + } +} + +// Export convenience function for creating migrations +export function createMigration(id: string, version: string, name: string): MigrationBuilder { + return MigrationBuilder.create(id, version, name); +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * MigrationManager - Schema Migration and Data Transformation System + * + * This class handles: + * - Schema version management across distributed databases + * - Automatic data migration and transformation + * - Rollback capabilities for failed migrations + * - Conflict resolution during migration + * - Migration validation and integrity checks + * - Cross-shard migration coordination + */ + +import { FieldConfig } from '../types/models'; + +export interface Migration { + id: string; + version: string; + name: string; + description: string; + targetModels: string[]; + up: MigrationOperation[]; + down: MigrationOperation[]; + dependencies?: string[]; // Migration IDs that must run before this one + validators?: MigrationValidator[]; + createdAt: number; + author?: string; + tags?: string[]; +} + +export interface MigrationOperation { + type: + | 'add_field' + | 'remove_field' + | 'modify_field' + | 'rename_field' + | 'add_index' + | 'remove_index' + | 'transform_data' + | 'custom'; + modelName: string; + fieldName?: string; + newFieldName?: string; + fieldConfig?: FieldConfig; + indexConfig?: any; + transformer?: (data: any) => any; + customOperation?: (context: MigrationContext) => Promise<void>; + rollbackOperation?: (context: MigrationContext) => Promise<void>; + options?: { + batchSize?: number; + parallel?: boolean; + skipValidation?: boolean; + }; +} + +export interface MigrationValidator { + name: string; + description: string; + validate: (context: MigrationContext) => Promise<ValidationResult>; +} + +export interface MigrationContext { + migration: Migration; + modelName: string; + databaseManager: any; + shardManager: any; + currentData?: any[]; + operation: MigrationOperation; + progress: MigrationProgress; + logger: MigrationLogger; +} + +export interface MigrationProgress { + migrationId: string; + status: 'pending' | 'running' | 'completed' | 'failed' | 'rolled_back'; + startedAt?: number; + completedAt?: number; + totalRecords: number; + processedRecords: number; + errorCount: number; + warnings: string[]; + errors: string[]; + currentOperation?: string; + estimatedTimeRemaining?: number; +} + +export interface MigrationResult { + migrationId: string; + success: boolean; + duration: number; + recordsProcessed: number; + recordsModified: number; + warnings: string[]; + errors: string[]; + rollbackAvailable: boolean; +} + +export interface MigrationLogger { + info: (message: string, meta?: any) => void; + warn: (message: string, meta?: any) => void; + error: (message: string, meta?: any) => void; + debug: (message: string, meta?: any) => void; +} + +export interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} + +export class MigrationManager { + private databaseManager: any; + private shardManager: any; + private migrations: Map<string, Migration> = new Map(); + private migrationHistory: Map<string, MigrationResult[]> = new Map(); + private activeMigrations: Map<string, MigrationProgress> = new Map(); + private migrationOrder: string[] = []; + private logger: MigrationLogger; + + constructor(databaseManager: any, shardManager: any, logger?: MigrationLogger) { + this.databaseManager = databaseManager; + this.shardManager = shardManager; + this.logger = logger || this.createDefaultLogger(); + } + + // Register a new migration + registerMigration(migration: Migration): void { + // Validate migration structure + this.validateMigrationStructure(migration); + + // Check for version conflicts + const existingMigration = Array.from(this.migrations.values()).find( + (m) => m.version === migration.version, + ); + + if (existingMigration && existingMigration.id !== migration.id) { + throw new Error(`Migration version ${migration.version} already exists with different ID`); + } + + this.migrations.set(migration.id, migration); + this.updateMigrationOrder(); + + this.logger.info(`Registered migration: ${migration.name} (${migration.version})`, { + migrationId: migration.id, + targetModels: migration.targetModels, + }); + } + + // Get all registered migrations + getMigrations(): Migration[] { + return Array.from(this.migrations.values()).sort((a, b) => + this.compareVersions(a.version, b.version), + ); + } + + // Get migration by ID + getMigration(migrationId: string): Migration | null { + return this.migrations.get(migrationId) || null; + } + + // Get pending migrations for a model or all models + getPendingMigrations(modelName?: string): Migration[] { + const allMigrations = this.getMigrations(); + const appliedMigrations = this.getAppliedMigrations(modelName); + const appliedIds = new Set(appliedMigrations.map((m) => m.migrationId)); + + return allMigrations.filter((migration) => { + if (!appliedIds.has(migration.id)) { + return modelName ? migration.targetModels.includes(modelName) : true; + } + return false; + }); + } + + // Run a specific migration + async runMigration( + migrationId: string, + options: { + dryRun?: boolean; + batchSize?: number; + parallelShards?: boolean; + skipValidation?: boolean; + } = {}, + ): Promise<MigrationResult> { + const migration = this.migrations.get(migrationId); + if (!migration) { + throw new Error(`Migration ${migrationId} not found`); + } + + // Check if migration is already running + if (this.activeMigrations.has(migrationId)) { + throw new Error(`Migration ${migrationId} is already running`); + } + + // Check dependencies + await this.validateDependencies(migration); + + const startTime = Date.now(); + const progress: MigrationProgress = { + migrationId, + status: 'running', + startedAt: startTime, + totalRecords: 0, + processedRecords: 0, + errorCount: 0, + warnings: [], + errors: [], + }; + + this.activeMigrations.set(migrationId, progress); + + try { + this.logger.info(`Starting migration: ${migration.name}`, { + migrationId, + dryRun: options.dryRun, + options, + }); + + if (options.dryRun) { + return await this.performDryRun(migration, options); + } + + // Pre-migration validation + if (!options.skipValidation) { + await this.runPreMigrationValidation(migration); + } + + // Execute migration operations + const result = await this.executeMigration(migration, options, progress); + + // Post-migration validation + if (!options.skipValidation) { + await this.runPostMigrationValidation(migration); + } + + // Record successful migration + progress.status = 'completed'; + progress.completedAt = Date.now(); + + await this.recordMigrationResult(result); + + this.logger.info(`Migration completed: ${migration.name}`, { + migrationId, + duration: result.duration, + recordsProcessed: result.recordsProcessed, + }); + + return result; + } catch (error: any) { + progress.status = 'failed'; + progress.errors.push(error.message); + + this.logger.error(`Migration failed: ${migration.name}`, { + migrationId, + error: error.message, + stack: error.stack, + }); + + // Attempt rollback if possible + const rollbackResult = await this.attemptRollback(migration, progress); + + const result: MigrationResult = { + migrationId, + success: false, + duration: Date.now() - startTime, + recordsProcessed: progress.processedRecords, + recordsModified: 0, + warnings: progress.warnings, + errors: progress.errors, + rollbackAvailable: rollbackResult.success, + }; + + await this.recordMigrationResult(result); + throw error; + } finally { + this.activeMigrations.delete(migrationId); + } + } + + // Run all pending migrations + async runPendingMigrations( + options: { + modelName?: string; + dryRun?: boolean; + stopOnError?: boolean; + batchSize?: number; + } = {}, + ): Promise<MigrationResult[]> { + const pendingMigrations = this.getPendingMigrations(options.modelName); + const results: MigrationResult[] = []; + + this.logger.info(`Running ${pendingMigrations.length} pending migrations`, { + modelName: options.modelName, + dryRun: options.dryRun, + }); + + for (const migration of pendingMigrations) { + try { + const result = await this.runMigration(migration.id, { + dryRun: options.dryRun, + batchSize: options.batchSize, + }); + results.push(result); + + if (!result.success && options.stopOnError) { + this.logger.warn('Stopping migration run due to error', { + failedMigration: migration.id, + stopOnError: options.stopOnError, + }); + break; + } + } catch (error) { + if (options.stopOnError) { + throw error; + } + this.logger.error(`Skipping failed migration: ${migration.id}`, { error }); + } + } + + return results; + } + + // Rollback a migration + async rollbackMigration(migrationId: string): Promise<MigrationResult> { + const migration = this.migrations.get(migrationId); + if (!migration) { + throw new Error(`Migration ${migrationId} not found`); + } + + const appliedMigrations = this.getAppliedMigrations(); + const isApplied = appliedMigrations.some((m) => m.migrationId === migrationId && m.success); + + if (!isApplied) { + throw new Error(`Migration ${migrationId} has not been applied`); + } + + const startTime = Date.now(); + const progress: MigrationProgress = { + migrationId, + status: 'running', + startedAt: startTime, + totalRecords: 0, + processedRecords: 0, + errorCount: 0, + warnings: [], + errors: [], + }; + + try { + this.logger.info(`Starting rollback: ${migration.name}`, { migrationId }); + + const result = await this.executeRollback(migration, progress); + + result.rollbackAvailable = false; + await this.recordMigrationResult(result); + + this.logger.info(`Rollback completed: ${migration.name}`, { + migrationId, + duration: result.duration, + }); + + return result; + } catch (error: any) { + this.logger.error(`Rollback failed: ${migration.name}`, { + migrationId, + error: error.message, + }); + throw error; + } + } + + // Execute migration operations + private async executeMigration( + migration: Migration, + options: any, + progress: MigrationProgress, + ): Promise<MigrationResult> { + const startTime = Date.now(); + let totalProcessed = 0; + let totalModified = 0; + + for (const modelName of migration.targetModels) { + for (const operation of migration.up) { + if (operation.modelName !== modelName) continue; + + progress.currentOperation = `${operation.type} on ${operation.modelName}.${operation.fieldName || 'N/A'}`; + + this.logger.debug(`Executing operation: ${progress.currentOperation}`, { + migrationId: migration.id, + operation: operation.type, + }); + + const context: MigrationContext = { + migration, + modelName, + databaseManager: this.databaseManager, + shardManager: this.shardManager, + operation, + progress, + logger: this.logger, + }; + + const operationResult = await this.executeOperation(context, options); + totalProcessed += operationResult.processed; + totalModified += operationResult.modified; + progress.processedRecords = totalProcessed; + } + } + + return { + migrationId: migration.id, + success: true, + duration: Date.now() - startTime, + recordsProcessed: totalProcessed, + recordsModified: totalModified, + warnings: progress.warnings, + errors: progress.errors, + rollbackAvailable: migration.down.length > 0, + }; + } + + // Execute a single migration operation + private async executeOperation( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + switch (operation.type) { + case 'add_field': + return await this.executeAddField(context, options); + + case 'remove_field': + return await this.executeRemoveField(context, options); + + case 'modify_field': + return await this.executeModifyField(context, options); + + case 'rename_field': + return await this.executeRenameField(context, options); + + case 'transform_data': + return await this.executeDataTransformation(context, options); + + case 'custom': + return await this.executeCustomOperation(context, options); + + default: + throw new Error(`Unsupported operation type: ${operation.type}`); + } + } + + // Execute add field operation + private async executeAddField( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.fieldName || !operation.fieldConfig) { + throw new Error('Add field operation requires fieldName and fieldConfig'); + } + + // Update model metadata (in a real implementation, this would update the model registry) + this.logger.info(`Adding field ${operation.fieldName} to ${operation.modelName}`, { + fieldConfig: operation.fieldConfig, + }); + + // Get all records for this model + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + // Add default value to existing records + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (!(operation.fieldName in record)) { + record[operation.fieldName] = operation.fieldConfig.default || null; + await this.updateRecord(operation.modelName, record); + modified++; + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute remove field operation + private async executeRemoveField( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.fieldName) { + throw new Error('Remove field operation requires fieldName'); + } + + this.logger.info(`Removing field ${operation.fieldName} from ${operation.modelName}`); + + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (operation.fieldName in record) { + delete record[operation.fieldName]; + await this.updateRecord(operation.modelName, record); + modified++; + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute modify field operation + private async executeModifyField( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.fieldName || !operation.fieldConfig) { + throw new Error('Modify field operation requires fieldName and fieldConfig'); + } + + this.logger.info(`Modifying field ${operation.fieldName} in ${operation.modelName}`, { + newConfig: operation.fieldConfig, + }); + + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (operation.fieldName in record) { + // Apply type conversion if needed + const oldValue = record[operation.fieldName]; + const newValue = this.convertFieldValue(oldValue, operation.fieldConfig); + + if (newValue !== oldValue) { + record[operation.fieldName] = newValue; + await this.updateRecord(operation.modelName, record); + modified++; + } + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute rename field operation + private async executeRenameField( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.fieldName || !operation.newFieldName) { + throw new Error('Rename field operation requires fieldName and newFieldName'); + } + + this.logger.info( + `Renaming field ${operation.fieldName} to ${operation.newFieldName} in ${operation.modelName}`, + ); + + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + if (operation.fieldName in record) { + record[operation.newFieldName] = record[operation.fieldName]; + delete record[operation.fieldName]; + await this.updateRecord(operation.modelName, record); + modified++; + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute data transformation operation + private async executeDataTransformation( + context: MigrationContext, + options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.transformer) { + throw new Error('Transform data operation requires transformer function'); + } + + this.logger.info(`Transforming data for ${operation.modelName}`); + + const records = await this.getAllRecordsForModel(operation.modelName); + let modified = 0; + + const batchSize = options.batchSize || 100; + for (let i = 0; i < records.length; i += batchSize) { + const batch = records.slice(i, i + batchSize); + + for (const record of batch) { + try { + const originalRecord = JSON.stringify(record); + const transformedRecord = await operation.transformer(record); + + if (JSON.stringify(transformedRecord) !== originalRecord) { + Object.assign(record, transformedRecord); + await this.updateRecord(operation.modelName, record); + modified++; + } + } catch (error: any) { + context.progress.errors.push(`Transform error for record ${record.id}: ${error.message}`); + context.progress.errorCount++; + } + } + + context.progress.processedRecords += batch.length; + } + + return { processed: records.length, modified }; + } + + // Execute custom operation + private async executeCustomOperation( + context: MigrationContext, + _options: any, + ): Promise<{ processed: number; modified: number }> { + const { operation } = context; + + if (!operation.customOperation) { + throw new Error('Custom operation requires customOperation function'); + } + + this.logger.info(`Executing custom operation for ${operation.modelName}`); + + try { + await operation.customOperation(context); + return { processed: 1, modified: 1 }; // Custom operations handle their own counting + } catch (error: any) { + context.progress.errors.push(`Custom operation error: ${error.message}`); + throw error; + } + } + + // Helper methods for data access + private async getAllRecordsForModel(modelName: string): Promise<any[]> { + // In a real implementation, this would query all shards for the model + // For now, return empty array as placeholder + this.logger.debug(`Getting all records for model: ${modelName}`); + return []; + } + + private async updateRecord(modelName: string, record: any): Promise<void> { + // In a real implementation, this would update the record in the appropriate database + this.logger.debug(`Updating record in ${modelName}:`, { id: record.id }); + } + + private convertFieldValue(value: any, fieldConfig: FieldConfig): any { + // Convert value based on field configuration + switch (fieldConfig.type) { + case 'string': + return value != null ? String(value) : null; + case 'number': + return value != null ? Number(value) : null; + case 'boolean': + return value != null ? Boolean(value) : null; + case 'array': + return Array.isArray(value) ? value : [value]; + default: + return value; + } + } + + // Validation methods + private validateMigrationStructure(migration: Migration): void { + if (!migration.id || !migration.version || !migration.name) { + throw new Error('Migration must have id, version, and name'); + } + + if (!migration.targetModels || migration.targetModels.length === 0) { + throw new Error('Migration must specify target models'); + } + + if (!migration.up || migration.up.length === 0) { + throw new Error('Migration must have at least one up operation'); + } + + // Validate operations + for (const operation of migration.up) { + this.validateOperation(operation); + } + + if (migration.down) { + for (const operation of migration.down) { + this.validateOperation(operation); + } + } + } + + private validateOperation(operation: MigrationOperation): void { + const validTypes = [ + 'add_field', + 'remove_field', + 'modify_field', + 'rename_field', + 'add_index', + 'remove_index', + 'transform_data', + 'custom', + ]; + + if (!validTypes.includes(operation.type)) { + throw new Error(`Invalid operation type: ${operation.type}`); + } + + if (!operation.modelName) { + throw new Error('Operation must specify modelName'); + } + } + + private async validateDependencies(migration: Migration): Promise<void> { + if (!migration.dependencies) return; + + const appliedMigrations = this.getAppliedMigrations(); + const appliedIds = new Set(appliedMigrations.map((m) => m.migrationId)); + + for (const dependencyId of migration.dependencies) { + if (!appliedIds.has(dependencyId)) { + throw new Error(`Migration dependency not satisfied: ${dependencyId}`); + } + } + } + + private async runPreMigrationValidation(migration: Migration): Promise<void> { + if (!migration.validators) return; + + for (const validator of migration.validators) { + this.logger.debug(`Running pre-migration validator: ${validator.name}`); + + const context: MigrationContext = { + migration, + modelName: '', // Will be set per model + databaseManager: this.databaseManager, + shardManager: this.shardManager, + operation: migration.up[0], // First operation for context + progress: this.activeMigrations.get(migration.id)!, + logger: this.logger, + }; + + const result = await validator.validate(context); + if (!result.valid) { + throw new Error(`Pre-migration validation failed: ${result.errors.join(', ')}`); + } + + if (result.warnings.length > 0) { + context.progress.warnings.push(...result.warnings); + } + } + } + + private async runPostMigrationValidation(_migration: Migration): Promise<void> { + // Similar to pre-migration validation but runs after + this.logger.debug('Running post-migration validation'); + } + + // Rollback operations + private async executeRollback( + migration: Migration, + progress: MigrationProgress, + ): Promise<MigrationResult> { + if (!migration.down || migration.down.length === 0) { + throw new Error('Migration has no rollback operations defined'); + } + + const startTime = Date.now(); + let totalProcessed = 0; + let totalModified = 0; + + // Execute rollback operations in reverse order + for (const modelName of migration.targetModels) { + for (const operation of migration.down.reverse()) { + if (operation.modelName !== modelName) continue; + + const context: MigrationContext = { + migration, + modelName, + databaseManager: this.databaseManager, + shardManager: this.shardManager, + operation, + progress, + logger: this.logger, + }; + + const operationResult = await this.executeOperation(context, {}); + totalProcessed += operationResult.processed; + totalModified += operationResult.modified; + } + } + + return { + migrationId: migration.id, + success: true, + duration: Date.now() - startTime, + recordsProcessed: totalProcessed, + recordsModified: totalModified, + warnings: progress.warnings, + errors: progress.errors, + rollbackAvailable: false, + }; + } + + private async attemptRollback( + migration: Migration, + progress: MigrationProgress, + ): Promise<{ success: boolean }> { + try { + if (migration.down && migration.down.length > 0) { + await this.executeRollback(migration, progress); + progress.status = 'rolled_back'; + return { success: true }; + } + } catch (error: any) { + this.logger.error(`Rollback failed for migration ${migration.id}`, { error }); + } + + return { success: false }; + } + + // Dry run functionality + private async performDryRun(migration: Migration, _options: any): Promise<MigrationResult> { + this.logger.info(`Performing dry run for migration: ${migration.name}`); + + const startTime = Date.now(); + let estimatedRecords = 0; + + // Estimate the number of records that would be affected + for (const modelName of migration.targetModels) { + const modelRecords = await this.countRecordsForModel(modelName); + estimatedRecords += modelRecords; + } + + // Simulate operations without actually modifying data + for (const operation of migration.up) { + this.logger.debug(`Dry run operation: ${operation.type} on ${operation.modelName}`); + } + + return { + migrationId: migration.id, + success: true, + duration: Date.now() - startTime, + recordsProcessed: estimatedRecords, + recordsModified: estimatedRecords, // Estimate + warnings: ['This was a dry run - no data was actually modified'], + errors: [], + rollbackAvailable: migration.down.length > 0, + }; + } + + private async countRecordsForModel(_modelName: string): Promise<number> { + // In a real implementation, this would count records across all shards + return 0; + } + + // Migration history and state management + private getAppliedMigrations(_modelName?: string): MigrationResult[] { + const allResults: MigrationResult[] = []; + + for (const results of this.migrationHistory.values()) { + allResults.push(...results.filter((r) => r.success)); + } + + return allResults; + } + + private async recordMigrationResult(result: MigrationResult): Promise<void> { + if (!this.migrationHistory.has(result.migrationId)) { + this.migrationHistory.set(result.migrationId, []); + } + + this.migrationHistory.get(result.migrationId)!.push(result); + + // In a real implementation, this would persist to database + this.logger.debug('Recorded migration result', { result }); + } + + // Version comparison + private compareVersions(version1: string, version2: string): number { + const v1Parts = version1.split('.').map(Number); + const v2Parts = version2.split('.').map(Number); + + for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; + + if (v1Part < v2Part) return -1; + if (v1Part > v2Part) return 1; + } + + return 0; + } + + private updateMigrationOrder(): void { + const migrations = Array.from(this.migrations.values()); + this.migrationOrder = migrations + .sort((a, b) => this.compareVersions(a.version, b.version)) + .map((m) => m.id); + } + + // Utility methods + private createDefaultLogger(): MigrationLogger { + return { + info: (message: string, meta?: any) => console.log(`[MIGRATION INFO] ${message}`, meta || ''), + warn: (message: string, meta?: any) => + console.warn(`[MIGRATION WARN] ${message}`, meta || ''), + error: (message: string, meta?: any) => + console.error(`[MIGRATION ERROR] ${message}`, meta || ''), + debug: (message: string, meta?: any) => + console.log(`[MIGRATION DEBUG] ${message}`, meta || ''), + }; + } + + // Status and monitoring + getMigrationProgress(migrationId: string): MigrationProgress | null { + return this.activeMigrations.get(migrationId) || null; + } + + getActiveMigrations(): MigrationProgress[] { + return Array.from(this.activeMigrations.values()); + } + + getMigrationHistory(migrationId?: string): MigrationResult[] { + if (migrationId) { + return this.migrationHistory.get(migrationId) || []; + } + + const allResults: MigrationResult[] = []; + for (const results of this.migrationHistory.values()) { + allResults.push(...results); + } + + return allResults.sort((a, b) => b.duration - a.duration); + } + + // Cleanup and maintenance + async cleanup(): Promise<void> { + this.logger.info('Cleaning up migration manager'); + this.activeMigrations.clear(); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
MigrationBuilder.ts | +
+
+ |
+ 0% | +0/103 | +0% | +0/34 | +0% | +0/38 | +0% | +0/102 | +
MigrationManager.ts | +
+
+ |
+ 0% | +0/332 | +0% | +0/165 | +0% | +0/51 | +0% | +0/315 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { StoreType, ValidationResult, ShardingConfig, PinningConfig } from '../types/framework'; +import { FieldConfig, RelationshipConfig, ValidationError } from '../types/models'; +import { QueryBuilder } from '../query/QueryBuilder'; + +export abstract class BaseModel { + // Instance properties + public id: string = ''; + public createdAt: number = 0; + public updatedAt: number = 0; + public _loadedRelations: Map<string, any> = new Map(); + protected _isDirty: boolean = false; + protected _isNew: boolean = true; + + // Static properties for model configuration + static modelName: string; + static dbType: StoreType = 'docstore'; + static scope: 'user' | 'global' = 'global'; + static sharding?: ShardingConfig; + static pinning?: PinningConfig; + static fields: Map<string, FieldConfig> = new Map(); + static relationships: Map<string, RelationshipConfig> = new Map(); + static hooks: Map<string, Function[]> = new Map(); + + constructor(data: any = {}) { + this.fromJSON(data); + } + + // Core CRUD operations + async save(): Promise<this> { + await this.validate(); + + if (this._isNew) { + await this.beforeCreate(); + + // Generate ID if not provided + if (!this.id) { + this.id = this.generateId(); + } + + this.createdAt = Date.now(); + this.updatedAt = this.createdAt; + + // Save to database (will be implemented when database manager is ready) + await this._saveToDatabase(); + + this._isNew = false; + this._isDirty = false; + + await this.afterCreate(); + } else if (this._isDirty) { + await this.beforeUpdate(); + + this.updatedAt = Date.now(); + + // Update in database + await this._updateInDatabase(); + + this._isDirty = false; + + await this.afterUpdate(); + } + + return this; + } + + static async create<T extends BaseModel>(this: new (data?: any) => T, data: any): Promise<T> { + const instance = new this(data); + return await instance.save(); + } + + static async get<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + _id: string, + ): Promise<T | null> { + // Will be implemented when query system is ready + throw new Error('get method not yet implemented - requires query system'); + } + + static async find<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + id: string, + ): Promise<T> { + const result = await this.get(id); + if (!result) { + throw new Error(`${this.name} with id ${id} not found`); + } + return result; + } + + async update(data: Partial<this>): Promise<this> { + Object.assign(this, data); + this._isDirty = true; + return await this.save(); + } + + async delete(): Promise<boolean> { + await this.beforeDelete(); + + // Delete from database (will be implemented when database manager is ready) + const success = await this._deleteFromDatabase(); + + if (success) { + await this.afterDelete(); + } + + return success; + } + + // Query operations (return QueryBuilder instances) + static where<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + operator: string, + value: any, + ): QueryBuilder<T> { + return new QueryBuilder<T>(this as any).where(field, operator, value); + } + + static whereIn<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + values: any[], + ): QueryBuilder<T> { + return new QueryBuilder<T>(this as any).whereIn(field, values); + } + + static orderBy<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + direction: 'asc' | 'desc' = 'asc', + ): QueryBuilder<T> { + return new QueryBuilder<T>(this as any).orderBy(field, direction); + } + + static limit<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + count: number, + ): QueryBuilder<T> { + return new QueryBuilder<T>(this as any).limit(count); + } + + static async all<T extends BaseModel>( + this: typeof BaseModel & (new (data?: any) => T), + ): Promise<T[]> { + return await new QueryBuilder<T>(this as any).exec(); + } + + // Relationship operations + async load(relationships: string[]): Promise<this> { + const framework = this.getFrameworkInstance(); + if (!framework?.relationshipManager) { + console.warn('RelationshipManager not available, skipping relationship loading'); + return this; + } + + await framework.relationshipManager.eagerLoadRelationships([this], relationships); + return this; + } + + async loadRelation(relationName: string): Promise<any> { + // Check if already loaded + if (this._loadedRelations.has(relationName)) { + return this._loadedRelations.get(relationName); + } + + const framework = this.getFrameworkInstance(); + if (!framework?.relationshipManager) { + console.warn('RelationshipManager not available, cannot load relationship'); + return null; + } + + return await framework.relationshipManager.loadRelationship(this, relationName); + } + + // Advanced relationship loading methods + async loadRelationWithConstraints( + relationName: string, + constraints: (query: any) => any, + ): Promise<any> { + const framework = this.getFrameworkInstance(); + if (!framework?.relationshipManager) { + console.warn('RelationshipManager not available, cannot load relationship'); + return null; + } + + return await framework.relationshipManager.loadRelationship(this, relationName, { + constraints, + }); + } + + async reloadRelation(relationName: string): Promise<any> { + // Clear cached relationship + this._loadedRelations.delete(relationName); + + const framework = this.getFrameworkInstance(); + if (framework?.relationshipManager) { + framework.relationshipManager.invalidateRelationshipCache(this, relationName); + } + + return await this.loadRelation(relationName); + } + + getLoadedRelations(): string[] { + return Array.from(this._loadedRelations.keys()); + } + + isRelationLoaded(relationName: string): boolean { + return this._loadedRelations.has(relationName); + } + + getRelation(relationName: string): any { + return this._loadedRelations.get(relationName); + } + + setRelation(relationName: string, value: any): void { + this._loadedRelations.set(relationName, value); + } + + clearRelation(relationName: string): void { + this._loadedRelations.delete(relationName); + } + + // Serialization + toJSON(): any { + const result: any = {}; + + // Include all enumerable properties + for (const key in this) { + if (this.hasOwnProperty(key) && !key.startsWith('_')) { + result[key] = (this as any)[key]; + } + } + + // Include loaded relations + this._loadedRelations.forEach((value, key) => { + result[key] = value; + }); + + return result; + } + + fromJSON(data: any): this { + if (!data) return this; + + // Set basic properties + Object.keys(data).forEach((key) => { + if (key !== '_loadedRelations' && key !== '_isDirty' && key !== '_isNew') { + (this as any)[key] = data[key]; + } + }); + + // Mark as existing if it has an ID + if (this.id) { + this._isNew = false; + } + + return this; + } + + // Validation + async validate(): Promise<ValidationResult> { + const errors: string[] = []; + const modelClass = this.constructor as typeof BaseModel; + + // Validate each field + for (const [fieldName, fieldConfig] of modelClass.fields) { + const value = (this as any)[fieldName]; + const fieldErrors = this.validateField(fieldName, value, fieldConfig); + errors.push(...fieldErrors); + } + + const result = { valid: errors.length === 0, errors }; + + if (!result.valid) { + throw new ValidationError(errors); + } + + return result; + } + + private validateField(fieldName: string, value: any, config: FieldConfig): string[] { + const errors: string[] = []; + + // Required validation + if (config.required && (value === undefined || value === null || value === '')) { + errors.push(`${fieldName} is required`); + return errors; // No point in further validation if required field is missing + } + + // Skip further validation if value is empty and not required + if (value === undefined || value === null) { + return errors; + } + + // Type validation + if (!this.isValidType(value, config.type)) { + errors.push(`${fieldName} must be of type ${config.type}`); + } + + // Custom validation + if (config.validate) { + const customResult = config.validate(value); + if (customResult === false) { + errors.push(`${fieldName} failed custom validation`); + } else if (typeof customResult === 'string') { + errors.push(customResult); + } + } + + return errors; + } + + private isValidType(value: any, expectedType: FieldConfig['type']): boolean { + switch (expectedType) { + case 'string': + return typeof value === 'string'; + case 'number': + return typeof value === 'number' && !isNaN(value); + case 'boolean': + return typeof value === 'boolean'; + case 'array': + return Array.isArray(value); + case 'object': + return typeof value === 'object' && !Array.isArray(value); + case 'date': + return value instanceof Date || (typeof value === 'number' && !isNaN(value)); + default: + return true; + } + } + + // Hook methods (can be overridden by subclasses) + async beforeCreate(): Promise<void> { + await this.runHooks('beforeCreate'); + } + + async afterCreate(): Promise<void> { + await this.runHooks('afterCreate'); + } + + async beforeUpdate(): Promise<void> { + await this.runHooks('beforeUpdate'); + } + + async afterUpdate(): Promise<void> { + await this.runHooks('afterUpdate'); + } + + async beforeDelete(): Promise<void> { + await this.runHooks('beforeDelete'); + } + + async afterDelete(): Promise<void> { + await this.runHooks('afterDelete'); + } + + private async runHooks(hookName: string): Promise<void> { + const modelClass = this.constructor as typeof BaseModel; + const hooks = modelClass.hooks.get(hookName) || []; + + for (const hook of hooks) { + await hook.call(this); + } + } + + // Utility methods + private generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + } + + // Database operations integrated with DatabaseManager + private async _saveToDatabase(): Promise<void> { + const framework = this.getFrameworkInstance(); + if (!framework) { + console.warn('Framework not initialized, skipping database save'); + return; + } + + const modelClass = this.constructor as typeof BaseModel; + + try { + if (modelClass.scope === 'user') { + // For user-scoped models, we need a userId + const userId = (this as any).userId; + if (!userId) { + throw new Error('User-scoped models must have a userId field'); + } + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.addDocument(database, modelClass.dbType, this.toJSON()); + } else { + // For global models + if (modelClass.sharding) { + // Use sharded database + const shard = framework.shardManager.getShardForKey(modelClass.modelName, this.id); + await framework.databaseManager.addDocument( + shard.database, + modelClass.dbType, + this.toJSON(), + ); + } else { + // Use single global database + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.addDocument(database, modelClass.dbType, this.toJSON()); + } + } + } catch (error) { + console.error('Failed to save to database:', error); + throw error; + } + } + + private async _updateInDatabase(): Promise<void> { + const framework = this.getFrameworkInstance(); + if (!framework) { + console.warn('Framework not initialized, skipping database update'); + return; + } + + const modelClass = this.constructor as typeof BaseModel; + + try { + if (modelClass.scope === 'user') { + const userId = (this as any).userId; + if (!userId) { + throw new Error('User-scoped models must have a userId field'); + } + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.updateDocument( + database, + modelClass.dbType, + this.id, + this.toJSON(), + ); + } else { + if (modelClass.sharding) { + const shard = framework.shardManager.getShardForKey(modelClass.modelName, this.id); + await framework.databaseManager.updateDocument( + shard.database, + modelClass.dbType, + this.id, + this.toJSON(), + ); + } else { + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.updateDocument( + database, + modelClass.dbType, + this.id, + this.toJSON(), + ); + } + } + } catch (error) { + console.error('Failed to update in database:', error); + throw error; + } + } + + private async _deleteFromDatabase(): Promise<boolean> { + const framework = this.getFrameworkInstance(); + if (!framework) { + console.warn('Framework not initialized, skipping database delete'); + return false; + } + + const modelClass = this.constructor as typeof BaseModel; + + try { + if (modelClass.scope === 'user') { + const userId = (this as any).userId; + if (!userId) { + throw new Error('User-scoped models must have a userId field'); + } + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.deleteDocument(database, modelClass.dbType, this.id); + } else { + if (modelClass.sharding) { + const shard = framework.shardManager.getShardForKey(modelClass.modelName, this.id); + await framework.databaseManager.deleteDocument( + shard.database, + modelClass.dbType, + this.id, + ); + } else { + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.deleteDocument(database, modelClass.dbType, this.id); + } + } + return true; + } catch (error) { + console.error('Failed to delete from database:', error); + throw error; + } + } + + private getFrameworkInstance(): any { + // This will be properly typed when DebrosFramework is created + return (globalThis as any).__debrosFramework; + } + + // Static methods for framework integration + static setStore(store: any): void { + (this as any)._store = store; + } + + static setShards(shards: any[]): void { + (this as any)._shards = shards; + } + + static getStore(): any { + return (this as any)._store; + } + + static getShards(): any[] { + return (this as any)._shards || []; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { FieldConfig, ValidationError } from '../../types/models'; + +export function Field(config: FieldConfig) { + return function (target: any, propertyKey: string) { + // Initialize fields map if it doesn't exist + if (!target.constructor.fields) { + target.constructor.fields = new Map(); + } + + // Store field configuration + target.constructor.fields.set(propertyKey, config); + + // Create getter/setter with validation and transformation + const privateKey = `_${propertyKey}`; + + // Store the current descriptor (if any) - for future use + const _currentDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey); + + Object.defineProperty(target, propertyKey, { + get() { + return this[privateKey]; + }, + set(value) { + // Apply transformation first + const transformedValue = config.transform ? config.transform(value) : value; + + // Validate the field value + const validationResult = validateFieldValue(transformedValue, config, propertyKey); + if (!validationResult.valid) { + throw new ValidationError(validationResult.errors); + } + + // Set the value and mark as dirty + this[privateKey] = transformedValue; + if (this._isDirty !== undefined) { + this._isDirty = true; + } + }, + enumerable: true, + configurable: true, + }); + + // Set default value if provided + if (config.default !== undefined) { + Object.defineProperty(target, privateKey, { + value: config.default, + writable: true, + enumerable: false, + configurable: true, + }); + } + }; +} + +function validateFieldValue( + value: any, + config: FieldConfig, + fieldName: string, +): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + // Required validation + if (config.required && (value === undefined || value === null || value === '')) { + errors.push(`${fieldName} is required`); + return { valid: false, errors }; + } + + // Skip further validation if value is empty and not required + if (value === undefined || value === null) { + return { valid: true, errors: [] }; + } + + // Type validation + if (!isValidType(value, config.type)) { + errors.push(`${fieldName} must be of type ${config.type}`); + } + + // Custom validation + if (config.validate) { + const customResult = config.validate(value); + if (customResult === false) { + errors.push(`${fieldName} failed custom validation`); + } else if (typeof customResult === 'string') { + errors.push(customResult); + } + } + + return { valid: errors.length === 0, errors }; +} + +function isValidType(value: any, expectedType: FieldConfig['type']): boolean { + switch (expectedType) { + case 'string': + return typeof value === 'string'; + case 'number': + return typeof value === 'number' && !isNaN(value); + case 'boolean': + return typeof value === 'boolean'; + case 'array': + return Array.isArray(value); + case 'object': + return typeof value === 'object' && !Array.isArray(value); + case 'date': + return value instanceof Date || (typeof value === 'number' && !isNaN(value)); + default: + return true; + } +} + +// Utility function to get field configuration +export function getFieldConfig(target: any, propertyKey: string): FieldConfig | undefined { + if (!target.constructor.fields) { + return undefined; + } + return target.constructor.fields.get(propertyKey); +} + +// Export the decorator type for TypeScript +export type FieldDecorator = (config: FieldConfig) => (target: any, propertyKey: string) => void; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../BaseModel'; +import { ModelConfig } from '../../types/models'; +import { StoreType } from '../../types/framework'; +import { ModelRegistry } from '../../core/ModelRegistry'; + +export function Model(config: ModelConfig = {}) { + return function <T extends typeof BaseModel>(target: T): T { + // Set model configuration on the class + target.modelName = config.tableName || target.name; + target.dbType = config.type || autoDetectType(target); + target.scope = config.scope || 'global'; + target.sharding = config.sharding; + target.pinning = config.pinning; + + // Register with framework + ModelRegistry.register(target.name, target, config); + + // TODO: Set up automatic database creation when DatabaseManager is ready + // DatabaseManager.scheduleCreation(target); + + return target; + }; +} + +function autoDetectType(modelClass: typeof BaseModel): StoreType { + // Analyze model fields to suggest optimal database type + const fields = modelClass.fields; + + if (!fields || fields.size === 0) { + return 'docstore'; // Default for complex objects + } + + let hasComplexFields = false; + let _hasSimpleFields = false; + + for (const [_fieldName, fieldConfig] of fields) { + if (fieldConfig.type === 'object' || fieldConfig.type === 'array') { + hasComplexFields = true; + } else { + _hasSimpleFields = true; + } + } + + // If we have complex fields, use docstore + if (hasComplexFields) { + return 'docstore'; + } + + // If we only have simple fields, we could use keyvalue + // But docstore is more flexible, so let's default to that + return 'docstore'; +} + +// Export the decorator type for TypeScript +export type ModelDecorator = (config?: ModelConfig) => <T extends typeof BaseModel>(target: T) => T; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | export function BeforeCreate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'beforeCreate', descriptor.value); +} + +export function AfterCreate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'afterCreate', descriptor.value); +} + +export function BeforeUpdate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'beforeUpdate', descriptor.value); +} + +export function AfterUpdate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'afterUpdate', descriptor.value); +} + +export function BeforeDelete(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'beforeDelete', descriptor.value); +} + +export function AfterDelete(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'afterDelete', descriptor.value); +} + +export function BeforeSave(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'beforeSave', descriptor.value); +} + +export function AfterSave(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + registerHook(target, 'afterSave', descriptor.value); +} + +function registerHook(target: any, hookName: string, hookFunction: Function): void { + // Initialize hooks map if it doesn't exist + if (!target.constructor.hooks) { + target.constructor.hooks = new Map(); + } + + // Get existing hooks for this hook name + const existingHooks = target.constructor.hooks.get(hookName) || []; + + // Add the new hook + existingHooks.push(hookFunction); + + // Store updated hooks array + target.constructor.hooks.set(hookName, existingHooks); + + console.log(`Registered ${hookName} hook for ${target.constructor.name}`); +} + +// Utility function to get hooks for a specific event +export function getHooks(target: any, hookName: string): Function[] { + if (!target.constructor.hooks) { + return []; + } + return target.constructor.hooks.get(hookName) || []; +} + +// Export decorator types for TypeScript +export type HookDecorator = ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, +) => void; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
Field.ts | +
+
+ |
+ 0% | +0/43 | +0% | +0/44 | +0% | +0/7 | +0% | +0/43 | +
Model.ts | +
+
+ |
+ 0% | +0/20 | +0% | +0/17 | +0% | +0/3 | +0% | +0/20 | +
hooks.ts | +
+
+ |
+ 0% | +0/17 | +0% | +0/8 | +0% | +0/10 | +0% | +0/17 | +
relationships.ts | +
+
+ |
+ 0% | +0/33 | +0% | +0/24 | +0% | +0/13 | +0% | +0/33 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../BaseModel'; +import { RelationshipConfig } from '../../types/models'; + +export function BelongsTo( + model: typeof BaseModel, + foreignKey: string, + options: { localKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'belongsTo', + model, + foreignKey, + localKey: options.localKey || 'id', + lazy: true, + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function HasMany( + model: typeof BaseModel, + foreignKey: string, + options: { localKey?: string; through?: typeof BaseModel } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'hasMany', + model, + foreignKey, + localKey: options.localKey || 'id', + through: options.through, + lazy: true, + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function HasOne( + model: typeof BaseModel, + foreignKey: string, + options: { localKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'hasOne', + model, + foreignKey, + localKey: options.localKey || 'id', + lazy: true, + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function ManyToMany( + model: typeof BaseModel, + through: typeof BaseModel, + foreignKey: string, + options: { localKey?: string; throughForeignKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'manyToMany', + model, + foreignKey, + localKey: options.localKey || 'id', + through, + lazy: true, + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +function registerRelationship(target: any, propertyKey: string, config: RelationshipConfig): void { + // Initialize relationships map if it doesn't exist + if (!target.constructor.relationships) { + target.constructor.relationships = new Map(); + } + + // Store relationship configuration + target.constructor.relationships.set(propertyKey, config); + + console.log( + `Registered ${config.type} relationship: ${target.constructor.name}.${propertyKey} -> ${config.model.name}`, + ); +} + +function createRelationshipProperty( + target: any, + propertyKey: string, + config: RelationshipConfig, +): void { + const _relationshipKey = `_relationship_${propertyKey}`; // For future use + + Object.defineProperty(target, propertyKey, { + get() { + // Check if relationship is already loaded + if (this._loadedRelations && this._loadedRelations.has(propertyKey)) { + return this._loadedRelations.get(propertyKey); + } + + if (config.lazy) { + // Return a promise for lazy loading + return this.loadRelation(propertyKey); + } else { + throw new Error( + `Relationship '${propertyKey}' not loaded. Use .load(['${propertyKey}']) first.`, + ); + } + }, + set(value) { + // Allow manual setting of relationship values + if (!this._loadedRelations) { + this._loadedRelations = new Map(); + } + this._loadedRelations.set(propertyKey, value); + }, + enumerable: true, + configurable: true, + }); +} + +// Utility function to get relationship configuration +export function getRelationshipConfig( + target: any, + propertyKey: string, +): RelationshipConfig | undefined { + if (!target.constructor.relationships) { + return undefined; + } + return target.constructor.relationships.get(propertyKey); +} + +// Type definitions for decorators +export type BelongsToDecorator = ( + model: typeof BaseModel, + foreignKey: string, + options?: { localKey?: string }, +) => (target: any, propertyKey: string) => void; + +export type HasManyDecorator = ( + model: typeof BaseModel, + foreignKey: string, + options?: { localKey?: string; through?: typeof BaseModel }, +) => (target: any, propertyKey: string) => void; + +export type HasOneDecorator = ( + model: typeof BaseModel, + foreignKey: string, + options?: { localKey?: string }, +) => (target: any, propertyKey: string) => void; + +export type ManyToManyDecorator = ( + model: typeof BaseModel, + through: typeof BaseModel, + foreignKey: string, + options?: { localKey?: string; throughForeignKey?: string }, +) => (target: any, propertyKey: string) => void; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
BaseModel.ts | +
+
+ |
+ 0% | +0/200 | +0% | +0/97 | +0% | +0/44 | +0% | +0/199 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * PinningManager - Automatic IPFS Pinning with Smart Strategies + * + * This class implements intelligent pinning strategies for IPFS content: + * - Fixed: Pin a fixed number of most important items + * - Popularity: Pin based on access frequency and recency + * - Size-based: Pin smaller items preferentially + * - Custom: User-defined pinning logic + * - Automatic cleanup of unpinned content + */ + +import { PinningStrategy, PinningStats } from '../types/framework'; + +// Node.js types for compatibility +declare global { + namespace NodeJS { + interface Timeout {} + } +} + +export interface PinningRule { + modelName: string; + strategy?: PinningStrategy; + factor?: number; + maxPins?: number; + minAccessCount?: number; + maxAge?: number; // in milliseconds + customLogic?: (item: any, stats: any) => number; // returns priority score +} + +export interface PinnedItem { + hash: string; + modelName: string; + itemId: string; + pinnedAt: number; + lastAccessed: number; + accessCount: number; + size: number; + priority: number; + metadata?: any; +} + +export interface PinningMetrics { + totalPinned: number; + totalSize: number; + averageSize: number; + oldestPin: number; + newestPin: number; + mostAccessed: PinnedItem | null; + leastAccessed: PinnedItem | null; + strategyBreakdown: Map<PinningStrategy, number>; +} + +export class PinningManager { + private ipfsService: any; + private pinnedItems: Map<string, PinnedItem> = new Map(); + private pinningRules: Map<string, PinningRule> = new Map(); + private accessLog: Map<string, { count: number; lastAccess: number }> = new Map(); + private cleanupInterval: NodeJS.Timeout | null = null; + private maxTotalPins: number = 10000; + private maxTotalSize: number = 10 * 1024 * 1024 * 1024; // 10GB + private cleanupIntervalMs: number = 60000; // 1 minute + + constructor( + ipfsService: any, + options: { + maxTotalPins?: number; + maxTotalSize?: number; + cleanupIntervalMs?: number; + } = {}, + ) { + this.ipfsService = ipfsService; + this.maxTotalPins = options.maxTotalPins || this.maxTotalPins; + this.maxTotalSize = options.maxTotalSize || this.maxTotalSize; + this.cleanupIntervalMs = options.cleanupIntervalMs || this.cleanupIntervalMs; + + // Start automatic cleanup + this.startAutoCleanup(); + } + + // Configure pinning rules for models + setPinningRule(modelName: string, rule: Partial<PinningRule>): void { + const existingRule = this.pinningRules.get(modelName); + const newRule: PinningRule = { + modelName, + strategy: 'popularity' as const, + factor: 1, + ...existingRule, + ...rule, + }; + + this.pinningRules.set(modelName, newRule); + console.log( + `📌 Set pinning rule for ${modelName}: ${newRule.strategy} (factor: ${newRule.factor})`, + ); + } + + // Pin content based on configured strategy + async pinContent( + hash: string, + modelName: string, + itemId: string, + metadata: any = {}, + ): Promise<boolean> { + try { + // Check if already pinned + if (this.pinnedItems.has(hash)) { + await this.recordAccess(hash); + return true; + } + + const rule = this.pinningRules.get(modelName); + if (!rule) { + console.warn(`No pinning rule found for model ${modelName}, skipping pin`); + return false; + } + + // Get content size + const size = await this.getContentSize(hash); + + // Calculate priority based on strategy + const priority = this.calculatePinningPriority(rule, metadata, size); + + // Check if we should pin based on priority and limits + const shouldPin = await this.shouldPinContent(rule, priority, size); + + if (!shouldPin) { + console.log( + `⏭️ Skipping pin for ${hash} (${modelName}): priority too low or limits exceeded`, + ); + return false; + } + + // Perform the actual pinning + await this.ipfsService.pin(hash); + + // Record the pinned item + const pinnedItem: PinnedItem = { + hash, + modelName, + itemId, + pinnedAt: Date.now(), + lastAccessed: Date.now(), + accessCount: 1, + size, + priority, + metadata, + }; + + this.pinnedItems.set(hash, pinnedItem); + this.recordAccess(hash); + + console.log( + `📌 Pinned ${hash} (${modelName}:${itemId}) with priority ${priority.toFixed(2)}`, + ); + + // Cleanup if we've exceeded limits + await this.enforceGlobalLimits(); + + return true; + } catch (error) { + console.error(`Failed to pin ${hash}:`, error); + return false; + } + } + + // Unpin content + async unpinContent(hash: string, force: boolean = false): Promise<boolean> { + try { + const pinnedItem = this.pinnedItems.get(hash); + if (!pinnedItem) { + console.warn(`Hash ${hash} is not tracked as pinned`); + return false; + } + + // Check if content should be protected from unpinning + if (!force && (await this.isProtectedFromUnpinning(pinnedItem))) { + console.log(`🔒 Content ${hash} is protected from unpinning`); + return false; + } + + await this.ipfsService.unpin(hash); + this.pinnedItems.delete(hash); + this.accessLog.delete(hash); + + console.log(`📌❌ Unpinned ${hash} (${pinnedItem.modelName}:${pinnedItem.itemId})`); + return true; + } catch (error) { + console.error(`Failed to unpin ${hash}:`, error); + return false; + } + } + + // Record access to pinned content + async recordAccess(hash: string): Promise<void> { + const pinnedItem = this.pinnedItems.get(hash); + if (pinnedItem) { + pinnedItem.lastAccessed = Date.now(); + pinnedItem.accessCount++; + } + + // Update access log + const accessInfo = this.accessLog.get(hash) || { count: 0, lastAccess: 0 }; + accessInfo.count++; + accessInfo.lastAccess = Date.now(); + this.accessLog.set(hash, accessInfo); + } + + // Calculate pinning priority based on strategy + private calculatePinningPriority(rule: PinningRule, metadata: any, size: number): number { + const now = Date.now(); + let priority = 0; + + switch (rule.strategy || 'popularity') { + case 'fixed': + // Fixed strategy: all items have equal priority + priority = rule.factor || 1; + break; + + case 'popularity': + // Popularity-based: recent access + total access count + const accessInfo = this.accessLog.get(metadata.hash) || { count: 0, lastAccess: 0 }; + const recencyScore = Math.max(0, 1 - (now - accessInfo.lastAccess) / (24 * 60 * 60 * 1000)); // 24h decay + const accessScore = Math.min(1, accessInfo.count / 100); // Cap at 100 accesses + priority = (recencyScore * 0.6 + accessScore * 0.4) * (rule.factor || 1); + break; + + case 'size': + // Size-based: prefer smaller content (inverse relationship) + const maxSize = 100 * 1024 * 1024; // 100MB + const sizeScore = Math.max(0.1, 1 - size / maxSize); + priority = sizeScore * (rule.factor || 1); + break; + + case 'age': + // Age-based: prefer newer content + const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days + const age = now - (metadata.createdAt || now); + const ageScore = Math.max(0.1, 1 - age / maxAge); + priority = ageScore * (rule.factor || 1); + break; + + case 'custom': + // Custom logic provided by user + if (rule.customLogic) { + priority = + rule.customLogic(metadata, { + size, + accessInfo: this.accessLog.get(metadata.hash), + now, + }) * (rule.factor || 1); + } else { + priority = rule.factor || 1; + } + break; + + default: + priority = rule.factor || 1; + } + + return Math.max(0, priority); + } + + // Determine if content should be pinned + private async shouldPinContent( + rule: PinningRule, + priority: number, + size: number, + ): Promise<boolean> { + // Check rule-specific limits + if (rule.maxPins) { + const currentPinsForModel = Array.from(this.pinnedItems.values()).filter( + (item) => item.modelName === rule.modelName, + ).length; + + if (currentPinsForModel >= rule.maxPins) { + // Find lowest priority item for this model to potentially replace + const lowestPriorityItem = Array.from(this.pinnedItems.values()) + .filter((item) => item.modelName === rule.modelName) + .sort((a, b) => a.priority - b.priority)[0]; + + if (!lowestPriorityItem || priority <= lowestPriorityItem.priority) { + return false; + } + + // Unpin the lowest priority item to make room + await this.unpinContent(lowestPriorityItem.hash, true); + } + } + + // Check global limits + const metrics = this.getMetrics(); + + if (metrics.totalPinned >= this.maxTotalPins) { + // Find globally lowest priority item to replace + const lowestPriorityItem = Array.from(this.pinnedItems.values()).sort( + (a, b) => a.priority - b.priority, + )[0]; + + if (!lowestPriorityItem || priority <= lowestPriorityItem.priority) { + return false; + } + + await this.unpinContent(lowestPriorityItem.hash, true); + } + + if (metrics.totalSize + size > this.maxTotalSize) { + // Need to free up space + const spaceNeeded = metrics.totalSize + size - this.maxTotalSize; + await this.freeUpSpace(spaceNeeded); + } + + return true; + } + + // Check if content is protected from unpinning + private async isProtectedFromUnpinning(pinnedItem: PinnedItem): Promise<boolean> { + const rule = this.pinningRules.get(pinnedItem.modelName); + if (!rule) return false; + + // Recently accessed content is protected + const timeSinceAccess = Date.now() - pinnedItem.lastAccessed; + if (timeSinceAccess < 60 * 60 * 1000) { + // 1 hour + return true; + } + + // High-priority content is protected + if (pinnedItem.priority > 0.8) { + return true; + } + + // Content with high access count is protected + if (pinnedItem.accessCount > 50) { + return true; + } + + return false; + } + + // Free up space by unpinning least important content + private async freeUpSpace(spaceNeeded: number): Promise<void> { + let freedSpace = 0; + + // Sort by priority (lowest first) + const sortedItems = Array.from(this.pinnedItems.values()) + .filter((item) => !this.isProtectedFromUnpinning(item)) + .sort((a, b) => a.priority - b.priority); + + for (const item of sortedItems) { + if (freedSpace >= spaceNeeded) break; + + await this.unpinContent(item.hash, true); + freedSpace += item.size; + } + + console.log(`🧹 Freed up ${(freedSpace / 1024 / 1024).toFixed(2)} MB of space`); + } + + // Enforce global pinning limits + private async enforceGlobalLimits(): Promise<void> { + const metrics = this.getMetrics(); + + // Check total pins limit + if (metrics.totalPinned > this.maxTotalPins) { + const excess = metrics.totalPinned - this.maxTotalPins; + const itemsToUnpin = Array.from(this.pinnedItems.values()) + .sort((a, b) => a.priority - b.priority) + .slice(0, excess); + + for (const item of itemsToUnpin) { + await this.unpinContent(item.hash, true); + } + } + + // Check total size limit + if (metrics.totalSize > this.maxTotalSize) { + const excessSize = metrics.totalSize - this.maxTotalSize; + await this.freeUpSpace(excessSize); + } + } + + // Automatic cleanup of old/unused pins + private async performCleanup(): Promise<void> { + const now = Date.now(); + const itemsToCleanup: PinnedItem[] = []; + + for (const item of this.pinnedItems.values()) { + const rule = this.pinningRules.get(item.modelName); + if (!rule) continue; + + let shouldCleanup = false; + + // Age-based cleanup + if (rule.maxAge) { + const age = now - item.pinnedAt; + if (age > rule.maxAge) { + shouldCleanup = true; + } + } + + // Access-based cleanup + if (rule.minAccessCount) { + if (item.accessCount < rule.minAccessCount) { + shouldCleanup = true; + } + } + + // Inactivity-based cleanup (not accessed for 7 days) + const inactivityThreshold = 7 * 24 * 60 * 60 * 1000; + if (now - item.lastAccessed > inactivityThreshold && item.priority < 0.3) { + shouldCleanup = true; + } + + if (shouldCleanup && !(await this.isProtectedFromUnpinning(item))) { + itemsToCleanup.push(item); + } + } + + // Unpin items marked for cleanup + for (const item of itemsToCleanup) { + await this.unpinContent(item.hash, true); + } + + if (itemsToCleanup.length > 0) { + console.log(`🧹 Cleaned up ${itemsToCleanup.length} old/unused pins`); + } + } + + // Start automatic cleanup + private startAutoCleanup(): void { + this.cleanupInterval = setInterval(() => { + this.performCleanup().catch((error) => { + console.error('Cleanup failed:', error); + }); + }, this.cleanupIntervalMs); + } + + // Stop automatic cleanup + stopAutoCleanup(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval as any); + this.cleanupInterval = null; + } + } + + // Get content size from IPFS + private async getContentSize(hash: string): Promise<number> { + try { + const stats = await this.ipfsService.object.stat(hash); + return stats.CumulativeSize || stats.BlockSize || 0; + } catch (error) { + console.warn(`Could not get size for ${hash}:`, error); + return 1024; // Default size + } + } + + // Get comprehensive metrics + getMetrics(): PinningMetrics { + const items = Array.from(this.pinnedItems.values()); + const totalSize = items.reduce((sum, item) => sum + item.size, 0); + const strategyBreakdown = new Map<PinningStrategy, number>(); + + // Count by strategy + for (const item of items) { + const rule = this.pinningRules.get(item.modelName); + if (rule) { + const strategy = rule.strategy || 'popularity'; + const count = strategyBreakdown.get(strategy) || 0; + strategyBreakdown.set(strategy, count + 1); + } + } + + // Find most/least accessed + const sortedByAccess = items.sort((a, b) => b.accessCount - a.accessCount); + + return { + totalPinned: items.length, + totalSize, + averageSize: items.length > 0 ? totalSize / items.length : 0, + oldestPin: items.length > 0 ? Math.min(...items.map((i) => i.pinnedAt)) : 0, + newestPin: items.length > 0 ? Math.max(...items.map((i) => i.pinnedAt)) : 0, + mostAccessed: sortedByAccess[0] || null, + leastAccessed: sortedByAccess[sortedByAccess.length - 1] || null, + strategyBreakdown, + }; + } + + // Get pinning statistics + getStats(): PinningStats { + const metrics = this.getMetrics(); + return { + totalPinned: metrics.totalPinned, + totalSize: metrics.totalSize, + averageSize: metrics.averageSize, + strategies: Object.fromEntries(metrics.strategyBreakdown), + oldestPin: metrics.oldestPin, + recentActivity: this.getRecentActivity(), + }; + } + + // Get recent pinning activity + private getRecentActivity(): Array<{ action: string; hash: string; timestamp: number }> { + // This would typically be implemented with a proper activity log + // For now, we'll return recent pins + const recentItems = Array.from(this.pinnedItems.values()) + .filter((item) => Date.now() - item.pinnedAt < 24 * 60 * 60 * 1000) // Last 24 hours + .sort((a, b) => b.pinnedAt - a.pinnedAt) + .slice(0, 10) + .map((item) => ({ + action: 'pinned', + hash: item.hash, + timestamp: item.pinnedAt, + })); + + return recentItems; + } + + // Analyze pinning performance + analyzePerformance(): any { + const metrics = this.getMetrics(); + const now = Date.now(); + + // Calculate hit rate (items accessed recently) + const recentlyAccessedCount = Array.from(this.pinnedItems.values()).filter( + (item) => now - item.lastAccessed < 60 * 60 * 1000, + ).length; // Last hour + + const hitRate = metrics.totalPinned > 0 ? recentlyAccessedCount / metrics.totalPinned : 0; + + // Calculate average priority + const averagePriority = + Array.from(this.pinnedItems.values()).reduce((sum, item) => sum + item.priority, 0) / + metrics.totalPinned || 0; + + // Storage efficiency + const storageEfficiency = + this.maxTotalSize > 0 ? (this.maxTotalSize - metrics.totalSize) / this.maxTotalSize : 0; + + return { + hitRate, + averagePriority, + storageEfficiency, + utilizationRate: metrics.totalPinned / this.maxTotalPins, + averageItemAge: now - (metrics.oldestPin + metrics.newestPin) / 2, + totalRules: this.pinningRules.size, + accessDistribution: this.getAccessDistribution(), + }; + } + + // Get access distribution statistics + private getAccessDistribution(): any { + const items = Array.from(this.pinnedItems.values()); + const accessCounts = items.map((item) => item.accessCount).sort((a, b) => a - b); + + if (accessCounts.length === 0) { + return { min: 0, max: 0, median: 0, q1: 0, q3: 0 }; + } + + const min = accessCounts[0]; + const max = accessCounts[accessCounts.length - 1]; + const median = accessCounts[Math.floor(accessCounts.length / 2)]; + const q1 = accessCounts[Math.floor(accessCounts.length / 4)]; + const q3 = accessCounts[Math.floor((accessCounts.length * 3) / 4)]; + + return { min, max, median, q1, q3 }; + } + + // Get pinned items for a specific model + getPinnedItemsForModel(modelName: string): PinnedItem[] { + return Array.from(this.pinnedItems.values()).filter((item) => item.modelName === modelName); + } + + // Check if specific content is pinned + isPinned(hash: string): boolean { + return this.pinnedItems.has(hash); + } + + // Clear all pins (for testing/reset) + async clearAllPins(): Promise<void> { + const hashes = Array.from(this.pinnedItems.keys()); + + for (const hash of hashes) { + await this.unpinContent(hash, true); + } + + this.pinnedItems.clear(); + this.accessLog.clear(); + + console.log(`🧹 Cleared all ${hashes.length} pins`); + } + + // Shutdown + async shutdown(): Promise<void> { + this.stopAutoCleanup(); + console.log('📌 PinningManager shut down'); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
PinningManager.ts | +
+
+ |
+ 0% | +0/227 | +0% | +0/132 | +0% | +0/44 | +0% | +0/218 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** + * PubSubManager - Automatic Event Publishing and Subscription + * + * This class handles automatic publishing of model changes and database events + * to IPFS PubSub topics, enabling real-time synchronization across nodes: + * - Model-level events (create, update, delete) + * - Database-level events (replication, sync) + * - Custom application events + * - Topic management and subscription handling + * - Event filtering and routing + */ + +import { BaseModel } from '../models/BaseModel'; + +// Node.js types for compatibility +declare global { + namespace NodeJS { + interface Timeout {} + } +} + +export interface PubSubConfig { + enabled: boolean; + autoPublishModelEvents: boolean; + autoPublishDatabaseEvents: boolean; + topicPrefix: string; + maxRetries: number; + retryDelay: number; + eventBuffer: { + enabled: boolean; + maxSize: number; + flushInterval: number; + }; + compression: { + enabled: boolean; + threshold: number; // bytes + }; + encryption: { + enabled: boolean; + publicKey?: string; + privateKey?: string; + }; +} + +export interface PubSubEvent { + id: string; + type: string; + topic: string; + data: any; + timestamp: number; + source: string; + metadata?: any; +} + +export interface TopicSubscription { + topic: string; + handler: (event: PubSubEvent) => void | Promise<void>; + filter?: (event: PubSubEvent) => boolean; + options: { + autoAck: boolean; + maxRetries: number; + deadLetterTopic?: string; + }; +} + +export interface PubSubStats { + totalPublished: number; + totalReceived: number; + totalSubscriptions: number; + publishErrors: number; + receiveErrors: number; + averageLatency: number; + topicStats: Map< + string, + { + published: number; + received: number; + subscribers: number; + lastActivity: number; + } + >; +} + +export class PubSubManager { + private ipfsService: any; + private config: PubSubConfig; + private subscriptions: Map<string, TopicSubscription[]> = new Map(); + private eventBuffer: PubSubEvent[] = []; + private bufferFlushInterval: any = null; + private stats: PubSubStats; + private latencyMeasurements: number[] = []; + private nodeId: string; + private isInitialized: boolean = false; + private eventListeners: Map<string, Function[]> = new Map(); + + constructor(ipfsService: any, config: Partial<PubSubConfig> = {}) { + this.ipfsService = ipfsService; + this.nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + this.config = { + enabled: true, + autoPublishModelEvents: true, + autoPublishDatabaseEvents: true, + topicPrefix: 'debros', + maxRetries: 3, + retryDelay: 1000, + eventBuffer: { + enabled: true, + maxSize: 100, + flushInterval: 5000, + }, + compression: { + enabled: true, + threshold: 1024, + }, + encryption: { + enabled: false, + }, + ...config, + }; + + this.stats = { + totalPublished: 0, + totalReceived: 0, + totalSubscriptions: 0, + publishErrors: 0, + receiveErrors: 0, + averageLatency: 0, + topicStats: new Map(), + }; + } + + // Simple event emitter functionality + emit(event: string, ...args: any[]): boolean { + const listeners = this.eventListeners.get(event) || []; + listeners.forEach((listener) => { + try { + listener(...args); + } catch (error) { + console.error(`Error in event listener for ${event}:`, error); + } + }); + return listeners.length > 0; + } + + on(event: string, listener: Function): this { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, []); + } + this.eventListeners.get(event)!.push(listener); + return this; + } + + off(event: string, listener?: Function): this { + if (!listener) { + this.eventListeners.delete(event); + } else { + const listeners = this.eventListeners.get(event) || []; + const index = listeners.indexOf(listener); + if (index >= 0) { + listeners.splice(index, 1); + } + } + return this; + } + + // Initialize PubSub system + async initialize(): Promise<void> { + if (!this.config.enabled) { + console.log('📡 PubSub disabled in configuration'); + return; + } + + try { + console.log('📡 Initializing PubSubManager...'); + + // Start event buffer flushing if enabled + if (this.config.eventBuffer.enabled) { + this.startEventBuffering(); + } + + // Subscribe to model events if auto-publishing is enabled + if (this.config.autoPublishModelEvents) { + this.setupModelEventPublishing(); + } + + // Subscribe to database events if auto-publishing is enabled + if (this.config.autoPublishDatabaseEvents) { + this.setupDatabaseEventPublishing(); + } + + this.isInitialized = true; + console.log('✅ PubSubManager initialized successfully'); + } catch (error) { + console.error('❌ Failed to initialize PubSubManager:', error); + throw error; + } + } + + // Publish event to a topic + async publish( + topic: string, + data: any, + options: { + priority?: 'low' | 'normal' | 'high'; + retries?: number; + compress?: boolean; + encrypt?: boolean; + metadata?: any; + } = {}, + ): Promise<boolean> { + if (!this.config.enabled || !this.isInitialized) { + return false; + } + + const event: PubSubEvent = { + id: this.generateEventId(), + type: this.extractEventType(topic), + topic: this.prefixTopic(topic), + data, + timestamp: Date.now(), + source: this.nodeId, + metadata: options.metadata, + }; + + try { + // Process event (compression, encryption, etc.) + const processedData = await this.processEventForPublishing(event, options); + + // Publish with buffering or directly + if (this.config.eventBuffer.enabled && options.priority !== 'high') { + return this.bufferEvent(event, processedData); + } else { + return await this.publishDirect(event.topic, processedData, options.retries); + } + } catch (error) { + this.stats.publishErrors++; + console.error(`❌ Failed to publish to ${topic}:`, error); + this.emit('publishError', { topic, error, event }); + return false; + } + } + + // Subscribe to a topic + async subscribe( + topic: string, + handler: (event: PubSubEvent) => void | Promise<void>, + options: { + filter?: (event: PubSubEvent) => boolean; + autoAck?: boolean; + maxRetries?: number; + deadLetterTopic?: string; + } = {}, + ): Promise<boolean> { + if (!this.config.enabled || !this.isInitialized) { + return false; + } + + const fullTopic = this.prefixTopic(topic); + + try { + const subscription: TopicSubscription = { + topic: fullTopic, + handler, + filter: options.filter, + options: { + autoAck: options.autoAck !== false, + maxRetries: options.maxRetries || this.config.maxRetries, + deadLetterTopic: options.deadLetterTopic, + }, + }; + + // Add to subscriptions map + if (!this.subscriptions.has(fullTopic)) { + this.subscriptions.set(fullTopic, []); + + // Subscribe to IPFS PubSub topic + await this.ipfsService.pubsub.subscribe(fullTopic, (message: any) => { + this.handleIncomingMessage(fullTopic, message); + }); + } + + this.subscriptions.get(fullTopic)!.push(subscription); + this.stats.totalSubscriptions++; + + // Update topic stats + this.updateTopicStats(fullTopic, 'subscribers', 1); + + console.log(`📡 Subscribed to topic: ${fullTopic}`); + this.emit('subscribed', { topic: fullTopic, subscription }); + + return true; + } catch (error) { + console.error(`❌ Failed to subscribe to ${topic}:`, error); + this.emit('subscribeError', { topic, error }); + return false; + } + } + + // Unsubscribe from a topic + async unsubscribe(topic: string, handler?: Function): Promise<boolean> { + const fullTopic = this.prefixTopic(topic); + const subscriptions = this.subscriptions.get(fullTopic); + + if (!subscriptions) { + return false; + } + + try { + if (handler) { + // Remove specific handler + const index = subscriptions.findIndex((sub) => sub.handler === handler); + if (index >= 0) { + subscriptions.splice(index, 1); + this.stats.totalSubscriptions--; + } + } else { + // Remove all handlers for this topic + this.stats.totalSubscriptions -= subscriptions.length; + subscriptions.length = 0; + } + + // If no more subscriptions, unsubscribe from IPFS + if (subscriptions.length === 0) { + await this.ipfsService.pubsub.unsubscribe(fullTopic); + this.subscriptions.delete(fullTopic); + this.stats.topicStats.delete(fullTopic); + } + + console.log(`📡 Unsubscribed from topic: ${fullTopic}`); + this.emit('unsubscribed', { topic: fullTopic }); + + return true; + } catch (error) { + console.error(`❌ Failed to unsubscribe from ${topic}:`, error); + return false; + } + } + + // Setup automatic model event publishing + private setupModelEventPublishing(): void { + const topics = { + create: 'model.created', + update: 'model.updated', + delete: 'model.deleted', + save: 'model.saved', + }; + + // Listen for model events on the global framework instance + this.on('modelEvent', async (eventType: string, model: BaseModel, changes?: any) => { + const topic = topics[eventType as keyof typeof topics]; + if (!topic) return; + + const eventData = { + modelName: model.constructor.name, + modelId: model.id, + userId: (model as any).userId, + changes, + timestamp: Date.now(), + }; + + await this.publish(topic, eventData, { + priority: eventType === 'delete' ? 'high' : 'normal', + metadata: { + modelType: model.constructor.name, + scope: (model.constructor as any).scope, + }, + }); + }); + } + + // Setup automatic database event publishing + private setupDatabaseEventPublishing(): void { + const databaseTopics = { + replication: 'database.replicated', + sync: 'database.synced', + conflict: 'database.conflict', + error: 'database.error', + }; + + // Listen for database events + this.on('databaseEvent', async (eventType: string, data: any) => { + const topic = databaseTopics[eventType as keyof typeof databaseTopics]; + if (!topic) return; + + await this.publish(topic, data, { + priority: eventType === 'error' ? 'high' : 'normal', + metadata: { + eventType, + source: 'database', + }, + }); + }); + } + + // Handle incoming PubSub messages + private async handleIncomingMessage(topic: string, message: any): Promise<void> { + try { + const startTime = Date.now(); + + // Parse and validate message + const event = await this.processIncomingMessage(message); + if (!event) return; + + // Update stats + this.stats.totalReceived++; + this.updateTopicStats(topic, 'received', 1); + + // Calculate latency + const latency = Date.now() - event.timestamp; + this.latencyMeasurements.push(latency); + if (this.latencyMeasurements.length > 100) { + this.latencyMeasurements.shift(); + } + this.stats.averageLatency = + this.latencyMeasurements.reduce((a, b) => a + b, 0) / this.latencyMeasurements.length; + + // Route to subscribers + const subscriptions = this.subscriptions.get(topic) || []; + + for (const subscription of subscriptions) { + try { + // Apply filter if present + if (subscription.filter && !subscription.filter(event)) { + continue; + } + + // Call handler + await this.callHandlerWithRetry(subscription, event); + } catch (error: any) { + this.stats.receiveErrors++; + console.error(`❌ Handler error for ${topic}:`, error); + + // Send to dead letter topic if configured + if (subscription.options.deadLetterTopic) { + await this.publish(subscription.options.deadLetterTopic, { + originalTopic: topic, + originalEvent: event, + error: error?.message || String(error), + timestamp: Date.now(), + }); + } + } + } + + this.emit('messageReceived', { topic, event, processingTime: Date.now() - startTime }); + } catch (error) { + this.stats.receiveErrors++; + console.error(`❌ Failed to handle message from ${topic}:`, error); + this.emit('messageError', { topic, error }); + } + } + + // Call handler with retry logic + private async callHandlerWithRetry( + subscription: TopicSubscription, + event: PubSubEvent, + attempt: number = 1, + ): Promise<void> { + try { + await subscription.handler(event); + } catch (error) { + if (attempt < subscription.options.maxRetries) { + console.warn( + `🔄 Retrying handler (attempt ${attempt + 1}/${subscription.options.maxRetries})`, + ); + await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay * attempt)); + return this.callHandlerWithRetry(subscription, event, attempt + 1); + } + throw error; + } + } + + // Process event for publishing (compression, encryption, etc.) + private async processEventForPublishing(event: PubSubEvent, options: any): Promise<string> { + let data = JSON.stringify(event); + + // Compression + if ( + options.compress !== false && + this.config.compression.enabled && + data.length > this.config.compression.threshold + ) { + // In a real implementation, you'd use a compression library like zlib + // data = await compress(data); + } + + // Encryption + if ( + options.encrypt !== false && + this.config.encryption.enabled && + this.config.encryption.publicKey + ) { + // In a real implementation, you'd encrypt with the public key + // data = await encrypt(data, this.config.encryption.publicKey); + } + + return data; + } + + // Process incoming message + private async processIncomingMessage(message: any): Promise<PubSubEvent | null> { + try { + let data = message.data.toString(); + + // Decryption + if (this.config.encryption.enabled && this.config.encryption.privateKey) { + // In a real implementation, you'd decrypt with the private key + // data = await decrypt(data, this.config.encryption.privateKey); + } + + // Decompression + if (this.config.compression.enabled) { + // In a real implementation, you'd detect and decompress + // data = await decompress(data); + } + + const event = JSON.parse(data) as PubSubEvent; + + // Validate event structure + if (!event.id || !event.topic || !event.timestamp) { + console.warn('❌ Invalid event structure received'); + return null; + } + + // Ignore our own messages + if (event.source === this.nodeId) { + return null; + } + + return event; + } catch (error) { + console.error('❌ Failed to process incoming message:', error); + return null; + } + } + + // Direct publish without buffering + private async publishDirect( + topic: string, + data: string, + retries: number = this.config.maxRetries, + ): Promise<boolean> { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + await this.ipfsService.pubsub.publish(topic, data); + + this.stats.totalPublished++; + this.updateTopicStats(topic, 'published', 1); + + return true; + } catch (error) { + if (attempt === retries) { + throw error; + } + + console.warn(`🔄 Retrying publish (attempt ${attempt + 1}/${retries})`); + await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay * attempt)); + } + } + + return false; + } + + // Buffer event for batch publishing + private bufferEvent(event: PubSubEvent, _data: string): boolean { + if (this.eventBuffer.length >= this.config.eventBuffer.maxSize) { + // Buffer is full, flush immediately + this.flushEventBuffer(); + } + + this.eventBuffer.push(event); + return true; + } + + // Start event buffering + private startEventBuffering(): void { + this.bufferFlushInterval = setInterval(() => { + this.flushEventBuffer(); + }, this.config.eventBuffer.flushInterval); + } + + // Flush event buffer + private async flushEventBuffer(): Promise<void> { + if (this.eventBuffer.length === 0) return; + + const events = [...this.eventBuffer]; + this.eventBuffer.length = 0; + + console.log(`📡 Flushing ${events.length} buffered events`); + + // Group events by topic for efficiency + const eventsByTopic = new Map<string, PubSubEvent[]>(); + for (const event of events) { + if (!eventsByTopic.has(event.topic)) { + eventsByTopic.set(event.topic, []); + } + eventsByTopic.get(event.topic)!.push(event); + } + + // Publish batches + for (const [topic, topicEvents] of eventsByTopic) { + try { + for (const event of topicEvents) { + const data = await this.processEventForPublishing(event, {}); + await this.publishDirect(topic, data); + } + } catch (error) { + console.error(`❌ Failed to flush events for ${topic}:`, error); + this.stats.publishErrors += topicEvents.length; + } + } + } + + // Update topic statistics + private updateTopicStats( + topic: string, + metric: 'published' | 'received' | 'subscribers', + delta: number, + ): void { + if (!this.stats.topicStats.has(topic)) { + this.stats.topicStats.set(topic, { + published: 0, + received: 0, + subscribers: 0, + lastActivity: Date.now(), + }); + } + + const stats = this.stats.topicStats.get(topic)!; + stats[metric] += delta; + stats.lastActivity = Date.now(); + } + + // Utility methods + private generateEventId(): string { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + + private extractEventType(topic: string): string { + const parts = topic.split('.'); + return parts[parts.length - 1]; + } + + private prefixTopic(topic: string): string { + return `${this.config.topicPrefix}.${topic}`; + } + + // Get PubSub statistics + getStats(): PubSubStats { + return { ...this.stats }; + } + + // Get list of active topics + getActiveTopics(): string[] { + return Array.from(this.subscriptions.keys()); + } + + // Get subscribers for a topic + getTopicSubscribers(topic: string): number { + const fullTopic = this.prefixTopic(topic); + return this.subscriptions.get(fullTopic)?.length || 0; + } + + // Check if topic exists + hasSubscriptions(topic: string): boolean { + const fullTopic = this.prefixTopic(topic); + return this.subscriptions.has(fullTopic) && this.subscriptions.get(fullTopic)!.length > 0; + } + + // Clear all subscriptions + async clearAllSubscriptions(): Promise<void> { + const topics = Array.from(this.subscriptions.keys()); + + for (const topic of topics) { + try { + await this.ipfsService.pubsub.unsubscribe(topic); + } catch (error) { + console.error(`Failed to unsubscribe from ${topic}:`, error); + } + } + + this.subscriptions.clear(); + this.stats.topicStats.clear(); + this.stats.totalSubscriptions = 0; + + console.log(`📡 Cleared all ${topics.length} subscriptions`); + } + + // Shutdown + async shutdown(): Promise<void> { + console.log('📡 Shutting down PubSubManager...'); + + // Stop event buffering + if (this.bufferFlushInterval) { + clearInterval(this.bufferFlushInterval as any); + this.bufferFlushInterval = null; + } + + // Flush remaining events + await this.flushEventBuffer(); + + // Clear all subscriptions + await this.clearAllSubscriptions(); + + // Clear event listeners + this.eventListeners.clear(); + + this.isInitialized = false; + console.log('✅ PubSubManager shut down successfully'); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
PubSubManager.ts | +
+
+ |
+ 0% | +0/228 | +0% | +0/110 | +0% | +0/37 | +0% | +0/220 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { QueryCondition, SortConfig } from '../types/queries'; +import { QueryExecutor } from './QueryExecutor'; + +export class QueryBuilder<T extends BaseModel> { + private model: typeof BaseModel; + private conditions: QueryCondition[] = []; + private relations: string[] = []; + private sorting: SortConfig[] = []; + private limitation?: number; + private offsetValue?: number; + private groupByFields: string[] = []; + private havingConditions: QueryCondition[] = []; + private distinctFields: string[] = []; + + constructor(model: typeof BaseModel) { + this.model = model; + } + + // Basic filtering + where(field: string, operator: string, value: any): this { + this.conditions.push({ field, operator, value }); + return this; + } + + whereIn(field: string, values: any[]): this { + return this.where(field, 'in', values); + } + + whereNotIn(field: string, values: any[]): this { + return this.where(field, 'not_in', values); + } + + whereNull(field: string): this { + return this.where(field, 'is_null', null); + } + + whereNotNull(field: string): this { + return this.where(field, 'is_not_null', null); + } + + whereBetween(field: string, min: any, max: any): this { + return this.where(field, 'between', [min, max]); + } + + whereNot(field: string, operator: string, value: any): this { + return this.where(field, `not_${operator}`, value); + } + + whereLike(field: string, pattern: string): this { + return this.where(field, 'like', pattern); + } + + whereILike(field: string, pattern: string): this { + return this.where(field, 'ilike', pattern); + } + + // Date filtering + whereDate(field: string, operator: string, date: Date | string | number): this { + return this.where(field, `date_${operator}`, date); + } + + whereDateBetween( + field: string, + startDate: Date | string | number, + endDate: Date | string | number, + ): this { + return this.where(field, 'date_between', [startDate, endDate]); + } + + whereYear(field: string, year: number): this { + return this.where(field, 'year', year); + } + + whereMonth(field: string, month: number): this { + return this.where(field, 'month', month); + } + + whereDay(field: string, day: number): this { + return this.where(field, 'day', day); + } + + // User-specific filtering (for user-scoped queries) + whereUser(userId: string): this { + return this.where('userId', '=', userId); + } + + whereUserIn(userIds: string[]): this { + this.conditions.push({ + field: 'userId', + operator: 'userIn', + value: userIds, + }); + return this; + } + + // Advanced filtering with OR conditions + orWhere(callback: (query: QueryBuilder<T>) => void): this { + const subQuery = new QueryBuilder<T>(this.model); + callback(subQuery); + + this.conditions.push({ + field: '__or__', + operator: 'or', + value: subQuery.getConditions(), + }); + + return this; + } + + // Array and object field queries + whereArrayContains(field: string, value: any): this { + return this.where(field, 'array_contains', value); + } + + whereArrayLength(field: string, operator: string, length: number): this { + return this.where(field, `array_length_${operator}`, length); + } + + whereObjectHasKey(field: string, key: string): this { + return this.where(field, 'object_has_key', key); + } + + whereObjectPath(field: string, path: string, operator: string, value: any): this { + return this.where(field, `object_path_${operator}`, { path, value }); + } + + // Sorting + orderBy(field: string, direction: 'asc' | 'desc' = 'asc'): this { + this.sorting.push({ field, direction }); + return this; + } + + orderByDesc(field: string): this { + return this.orderBy(field, 'desc'); + } + + orderByRaw(expression: string): this { + this.sorting.push({ field: expression, direction: 'asc' }); + return this; + } + + // Multiple field sorting + orderByMultiple(sorts: Array<{ field: string; direction: 'asc' | 'desc' }>): this { + sorts.forEach((sort) => this.orderBy(sort.field, sort.direction)); + return this; + } + + // Pagination + limit(count: number): this { + this.limitation = count; + return this; + } + + offset(count: number): this { + this.offsetValue = count; + return this; + } + + skip(count: number): this { + return this.offset(count); + } + + take(count: number): this { + return this.limit(count); + } + + // Pagination helpers + page(pageNumber: number, pageSize: number): this { + this.limitation = pageSize; + this.offsetValue = (pageNumber - 1) * pageSize; + return this; + } + + // Relationship loading + load(relationships: string[]): this { + this.relations = [...this.relations, ...relationships]; + return this; + } + + with(relationships: string[]): this { + return this.load(relationships); + } + + loadNested(relationship: string, _callback: (query: QueryBuilder<any>) => void): this { + // For nested relationship loading with constraints + this.relations.push(relationship); + // Store callback for nested query (implementation in QueryExecutor) + return this; + } + + // Aggregation + groupBy(...fields: string[]): this { + this.groupByFields.push(...fields); + return this; + } + + having(field: string, operator: string, value: any): this { + this.havingConditions.push({ field, operator, value }); + return this; + } + + // Distinct + distinct(...fields: string[]): this { + this.distinctFields.push(...fields); + return this; + } + + // Execution methods + async exec(): Promise<T[]> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.execute(); + } + + async get(): Promise<T[]> { + return await this.exec(); + } + + async first(): Promise<T | null> { + const results = await this.limit(1).exec(); + return results[0] || null; + } + + async firstOrFail(): Promise<T> { + const result = await this.first(); + if (!result) { + throw new Error(`No ${this.model.name} found matching the query`); + } + return result; + } + + async find(id: string): Promise<T | null> { + return await this.where('id', '=', id).first(); + } + + async findOrFail(id: string): Promise<T> { + const result = await this.find(id); + if (!result) { + throw new Error(`${this.model.name} with id ${id} not found`); + } + return result; + } + + async count(): Promise<number> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.count(); + } + + async exists(): Promise<boolean> { + const count = await this.count(); + return count > 0; + } + + async sum(field: string): Promise<number> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.sum(field); + } + + async avg(field: string): Promise<number> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.avg(field); + } + + async min(field: string): Promise<any> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.min(field); + } + + async max(field: string): Promise<any> { + const executor = new QueryExecutor<T>(this.model, this); + return await executor.max(field); + } + + // Pagination with metadata + async paginate( + page: number = 1, + perPage: number = 15, + ): Promise<{ + data: T[]; + total: number; + perPage: number; + currentPage: number; + lastPage: number; + hasNextPage: boolean; + hasPrevPage: boolean; + }> { + const total = await this.count(); + const lastPage = Math.ceil(total / perPage); + + const data = await this.page(page, perPage).exec(); + + return { + data, + total, + perPage, + currentPage: page, + lastPage, + hasNextPage: page < lastPage, + hasPrevPage: page > 1, + }; + } + + // Chunked processing + async chunk( + size: number, + callback: (items: T[], page: number) => Promise<void | boolean>, + ): Promise<void> { + let page = 1; + let hasMore = true; + + while (hasMore) { + const items = await this.page(page, size).exec(); + + if (items.length === 0) { + break; + } + + const result = await callback(items, page); + + // If callback returns false, stop processing + if (result === false) { + break; + } + + hasMore = items.length === size; + page++; + } + } + + // Query optimization hints + useIndex(indexName: string): this { + // Hint for query optimizer (implementation in QueryExecutor) + (this as any)._indexHint = indexName; + return this; + } + + preferShard(shardIndex: number): this { + // Force query to specific shard (for global sharded models) + (this as any)._preferredShard = shardIndex; + return this; + } + + // Raw queries (for advanced users) + whereRaw(expression: string, bindings: any[] = []): this { + this.conditions.push({ + field: '__raw__', + operator: 'raw', + value: { expression, bindings }, + }); + return this; + } + + // Getters for query configuration (used by QueryExecutor) + getConditions(): QueryCondition[] { + return [...this.conditions]; + } + + getRelations(): string[] { + return [...this.relations]; + } + + getSorting(): SortConfig[] { + return [...this.sorting]; + } + + getLimit(): number | undefined { + return this.limitation; + } + + getOffset(): number | undefined { + return this.offsetValue; + } + + getGroupBy(): string[] { + return [...this.groupByFields]; + } + + getHaving(): QueryCondition[] { + return [...this.havingConditions]; + } + + getDistinct(): string[] { + return [...this.distinctFields]; + } + + getModel(): typeof BaseModel { + return this.model; + } + + // Clone query for reuse + clone(): QueryBuilder<T> { + const cloned = new QueryBuilder<T>(this.model); + cloned.conditions = [...this.conditions]; + cloned.relations = [...this.relations]; + cloned.sorting = [...this.sorting]; + cloned.limitation = this.limitation; + cloned.offsetValue = this.offsetValue; + cloned.groupByFields = [...this.groupByFields]; + cloned.havingConditions = [...this.havingConditions]; + cloned.distinctFields = [...this.distinctFields]; + + return cloned; + } + + // Debug methods + toSQL(): string { + // Generate SQL-like representation for debugging + let sql = `SELECT * FROM ${this.model.name}`; + + if (this.conditions.length > 0) { + const whereClause = this.conditions + .map((c) => `${c.field} ${c.operator} ${JSON.stringify(c.value)}`) + .join(' AND '); + sql += ` WHERE ${whereClause}`; + } + + if (this.sorting.length > 0) { + const orderClause = this.sorting + .map((s) => `${s.field} ${s.direction.toUpperCase()}`) + .join(', '); + sql += ` ORDER BY ${orderClause}`; + } + + if (this.limitation) { + sql += ` LIMIT ${this.limitation}`; + } + + if (this.offsetValue) { + sql += ` OFFSET ${this.offsetValue}`; + } + + return sql; + } + + explain(): any { + return { + model: this.model.name, + scope: this.model.scope, + conditions: this.conditions, + relations: this.relations, + sorting: this.sorting, + limit: this.limitation, + offset: this.offsetValue, + sql: this.toSQL(), + }; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { QueryBuilder } from './QueryBuilder'; +import { BaseModel } from '../models/BaseModel'; + +export interface CacheEntry<T> { + key: string; + data: T[]; + timestamp: number; + ttl: number; + hitCount: number; +} + +export interface CacheStats { + totalRequests: number; + cacheHits: number; + cacheMisses: number; + hitRate: number; + size: number; + maxSize: number; +} + +export class QueryCache { + private cache: Map<string, CacheEntry<any>> = new Map(); + private maxSize: number; + private defaultTTL: number; + private stats: CacheStats; + + constructor(maxSize: number = 1000, defaultTTL: number = 300000) { + // 5 minutes default + this.maxSize = maxSize; + this.defaultTTL = defaultTTL; + this.stats = { + totalRequests: 0, + cacheHits: 0, + cacheMisses: 0, + hitRate: 0, + size: 0, + maxSize, + }; + } + + generateKey<T extends BaseModel>(query: QueryBuilder<T>): string { + const model = query.getModel(); + const conditions = query.getConditions(); + const relations = query.getRelations(); + const sorting = query.getSorting(); + const limit = query.getLimit(); + const offset = query.getOffset(); + + // Create a deterministic cache key + const keyParts = [ + model.name, + model.scope, + JSON.stringify(conditions.sort((a, b) => a.field.localeCompare(b.field))), + JSON.stringify(relations.sort()), + JSON.stringify(sorting), + limit?.toString() || 'no-limit', + offset?.toString() || 'no-offset', + ]; + + // Create hash of the key parts + return this.hashString(keyParts.join('|')); + } + + async get<T extends BaseModel>(query: QueryBuilder<T>): Promise<T[] | null> { + this.stats.totalRequests++; + + const key = this.generateKey(query); + const entry = this.cache.get(key); + + if (!entry) { + this.stats.cacheMisses++; + this.updateHitRate(); + return null; + } + + // Check if entry has expired + if (Date.now() - entry.timestamp > entry.ttl) { + this.cache.delete(key); + this.stats.cacheMisses++; + this.updateHitRate(); + return null; + } + + // Update hit count and stats + entry.hitCount++; + this.stats.cacheHits++; + this.updateHitRate(); + + // Convert cached data back to model instances + const modelClass = query.getModel() as any; // Type assertion for abstract class + return entry.data.map((item) => new modelClass(item)); + } + + set<T extends BaseModel>(query: QueryBuilder<T>, data: T[], customTTL?: number): void { + const key = this.generateKey(query); + const ttl = customTTL || this.defaultTTL; + + // Serialize model instances to plain objects for caching + const serializedData = data.map((item) => item.toJSON()); + + const entry: CacheEntry<any> = { + key, + data: serializedData, + timestamp: Date.now(), + ttl, + hitCount: 0, + }; + + // Check if we need to evict entries + if (this.cache.size >= this.maxSize) { + this.evictLeastUsed(); + } + + this.cache.set(key, entry); + this.stats.size = this.cache.size; + } + + invalidate<T extends BaseModel>(query: QueryBuilder<T>): boolean { + const key = this.generateKey(query); + const deleted = this.cache.delete(key); + this.stats.size = this.cache.size; + return deleted; + } + + invalidateByModel(modelName: string): number { + let deletedCount = 0; + + for (const [key, _entry] of this.cache.entries()) { + if (key.startsWith(this.hashString(modelName))) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.size = this.cache.size; + return deletedCount; + } + + invalidateByUser(userId: string): number { + let deletedCount = 0; + + for (const [key, entry] of this.cache.entries()) { + // Check if the cached entry contains user-specific data + if (this.entryContainsUser(entry, userId)) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.size = this.cache.size; + return deletedCount; + } + + clear(): void { + this.cache.clear(); + this.stats.size = 0; + this.stats.totalRequests = 0; + this.stats.cacheHits = 0; + this.stats.cacheMisses = 0; + this.stats.hitRate = 0; + } + + getStats(): CacheStats { + return { ...this.stats }; + } + + // Cache warming - preload frequently used queries + async warmup<T extends BaseModel>(queries: QueryBuilder<T>[]): Promise<void> { + console.log(`🔥 Warming up cache with ${queries.length} queries...`); + + const promises = queries.map(async (query) => { + try { + const results = await query.exec(); + this.set(query, results); + console.log(`✓ Cached query for ${query.getModel().name}`); + } catch (error) { + console.warn(`Failed to warm cache for ${query.getModel().name}:`, error); + } + }); + + await Promise.all(promises); + console.log(`✅ Cache warmup completed`); + } + + // Get cache entries sorted by various criteria + getPopularEntries(limit: number = 10): Array<{ key: string; hitCount: number; age: number }> { + return Array.from(this.cache.entries()) + .map(([key, entry]) => ({ + key, + hitCount: entry.hitCount, + age: Date.now() - entry.timestamp, + })) + .sort((a, b) => b.hitCount - a.hitCount) + .slice(0, limit); + } + + getExpiredEntries(): string[] { + const now = Date.now(); + const expired: string[] = []; + + for (const [key, entry] of this.cache.entries()) { + if (now - entry.timestamp > entry.ttl) { + expired.push(key); + } + } + + return expired; + } + + // Cleanup expired entries + cleanup(): number { + const expired = this.getExpiredEntries(); + + for (const key of expired) { + this.cache.delete(key); + } + + this.stats.size = this.cache.size; + return expired.length; + } + + // Configure cache behavior + setMaxSize(size: number): void { + this.maxSize = size; + this.stats.maxSize = size; + + // Evict entries if current size exceeds new max + while (this.cache.size > size) { + this.evictLeastUsed(); + } + } + + setDefaultTTL(ttl: number): void { + this.defaultTTL = ttl; + } + + // Cache analysis + analyzeUsage(): { + totalEntries: number; + averageHitCount: number; + averageAge: number; + memoryUsage: number; + } { + const entries = Array.from(this.cache.values()); + const now = Date.now(); + + const totalHits = entries.reduce((sum, entry) => sum + entry.hitCount, 0); + const totalAge = entries.reduce((sum, entry) => sum + (now - entry.timestamp), 0); + + // Rough memory usage estimation + const memoryUsage = entries.reduce((sum, entry) => { + return sum + JSON.stringify(entry.data).length; + }, 0); + + return { + totalEntries: entries.length, + averageHitCount: entries.length > 0 ? totalHits / entries.length : 0, + averageAge: entries.length > 0 ? totalAge / entries.length : 0, + memoryUsage, + }; + } + + private evictLeastUsed(): void { + if (this.cache.size === 0) return; + + // Find entry with lowest hit count and oldest timestamp + let leastUsedKey: string | null = null; + let leastUsedScore = Infinity; + + for (const [key, entry] of this.cache.entries()) { + // Score based on hit count and age (lower is worse) + const age = Date.now() - entry.timestamp; + const score = entry.hitCount - age / 1000000; // Age penalty + + if (score < leastUsedScore) { + leastUsedScore = score; + leastUsedKey = key; + } + } + + if (leastUsedKey) { + this.cache.delete(leastUsedKey); + this.stats.size = this.cache.size; + } + } + + private entryContainsUser(entry: CacheEntry<any>, userId: string): boolean { + // Check if the cached data contains user-specific information + try { + const dataStr = JSON.stringify(entry.data); + return dataStr.includes(userId); + } catch { + return false; + } + } + + private updateHitRate(): void { + if (this.stats.totalRequests > 0) { + this.stats.hitRate = this.stats.cacheHits / this.stats.totalRequests; + } + } + + private hashString(str: string): string { + let hash = 0; + if (str.length === 0) return hash.toString(); + + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + + return Math.abs(hash).toString(36); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { QueryBuilder } from './QueryBuilder'; +import { QueryCondition } from '../types/queries'; +import { StoreType } from '../types/framework'; +import { QueryOptimizer, QueryPlan } from './QueryOptimizer'; + +export class QueryExecutor<T extends BaseModel> { + private model: typeof BaseModel; + private query: QueryBuilder<T>; + private framework: any; // Will be properly typed later + private queryPlan?: QueryPlan; + private useCache: boolean = true; + + constructor(model: typeof BaseModel, query: QueryBuilder<T>) { + this.model = model; + this.query = query; + this.framework = this.getFrameworkInstance(); + } + + async execute(): Promise<T[]> { + const startTime = Date.now(); + console.log(`🔍 Executing query for ${this.model.name} (${this.model.scope})`); + + // Generate query plan for optimization + this.queryPlan = QueryOptimizer.analyzeQuery(this.query); + console.log( + `📊 Query plan: ${this.queryPlan.strategy} (cost: ${this.queryPlan.estimatedCost})`, + ); + + // Check cache first if enabled + if (this.useCache && this.framework.queryCache) { + const cached = await this.framework.queryCache.get(this.query); + if (cached) { + console.log(`⚡ Cache hit for ${this.model.name} query`); + return cached; + } + } + + // Execute query based on scope + let results: T[]; + if (this.model.scope === 'user') { + results = await this.executeUserScopedQuery(); + } else { + results = await this.executeGlobalQuery(); + } + + // Cache results if enabled + if (this.useCache && this.framework.queryCache && results.length > 0) { + this.framework.queryCache.set(this.query, results); + } + + const duration = Date.now() - startTime; + console.log(`✅ Query completed in ${duration}ms, returned ${results.length} results`); + + return results; + } + + async count(): Promise<number> { + const results = await this.execute(); + return results.length; + } + + async sum(field: string): Promise<number> { + const results = await this.execute(); + return results.reduce((sum, item) => { + const value = this.getNestedValue(item, field); + return sum + (typeof value === 'number' ? value : 0); + }, 0); + } + + async avg(field: string): Promise<number> { + const results = await this.execute(); + if (results.length === 0) return 0; + + const sum = await this.sum(field); + return sum / results.length; + } + + async min(field: string): Promise<any> { + const results = await this.execute(); + if (results.length === 0) return null; + + return results.reduce((min, item) => { + const value = this.getNestedValue(item, field); + return min === null || value < min ? value : min; + }, null); + } + + async max(field: string): Promise<any> { + const results = await this.execute(); + if (results.length === 0) return null; + + return results.reduce((max, item) => { + const value = this.getNestedValue(item, field); + return max === null || value > max ? value : max; + }, null); + } + + private async executeUserScopedQuery(): Promise<T[]> { + const conditions = this.query.getConditions(); + + // Check if we have user-specific filters + const userFilter = conditions.find((c) => c.field === 'userId' || c.operator === 'userIn'); + + if (userFilter) { + return await this.executeUserSpecificQuery(userFilter); + } else { + // Global query on user-scoped data - use global index + return await this.executeGlobalIndexQuery(); + } + } + + private async executeUserSpecificQuery(userFilter: QueryCondition): Promise<T[]> { + const userIds = userFilter.operator === 'userIn' ? userFilter.value : [userFilter.value]; + + console.log(`👤 Querying user databases for ${userIds.length} users`); + + const results: T[] = []; + + // Query each user's database in parallel + const promises = userIds.map(async (userId: string) => { + try { + const userDB = await this.framework.databaseManager.getUserDatabase( + userId, + this.model.modelName, + ); + + return await this.queryDatabase(userDB, this.model.dbType); + } catch (error) { + console.warn(`Failed to query user ${userId} database:`, error); + return []; + } + }); + + const userResults = await Promise.all(promises); + + // Flatten and combine results + for (const userResult of userResults) { + results.push(...userResult); + } + + return this.postProcessResults(results); + } + + private async executeGlobalIndexQuery(): Promise<T[]> { + console.log(`📇 Querying global index for ${this.model.name}`); + + // Query global index for user-scoped models + const globalIndexName = `${this.model.modelName}GlobalIndex`; + const indexShards = this.framework.shardManager.getAllShards(globalIndexName); + + if (!indexShards || indexShards.length === 0) { + console.warn(`No global index found for ${this.model.name}, falling back to all users query`); + return await this.executeAllUsersQuery(); + } + + const indexResults: any[] = []; + + // Query all index shards in parallel + const promises = indexShards.map((shard: any) => + this.queryDatabase(shard.database, 'keyvalue'), + ); + const shardResults = await Promise.all(promises); + + for (const shardResult of shardResults) { + indexResults.push(...shardResult); + } + + // Now fetch actual documents from user databases + return await this.fetchActualDocuments(indexResults); + } + + private async executeAllUsersQuery(): Promise<T[]> { + // This is a fallback for when global index is not available + // It's expensive but ensures completeness + console.warn(`⚠️ Executing expensive all-users query for ${this.model.name}`); + + // This would require getting all user IDs from the directory + // For now, return empty array and log warning + console.warn('All-users query not implemented - please ensure global indexes are set up'); + return []; + } + + private async executeGlobalQuery(): Promise<T[]> { + // For globally scoped models + if (this.model.sharding) { + return await this.executeShardedQuery(); + } else { + const db = await this.framework.databaseManager.getGlobalDatabase(this.model.modelName); + return await this.queryDatabase(db, this.model.dbType); + } + } + + private async executeShardedQuery(): Promise<T[]> { + console.log(`🔀 Executing sharded query for ${this.model.name}`); + + const conditions = this.query.getConditions(); + const shardingConfig = this.model.sharding!; + + // Check if we can route to specific shard(s) + const shardKeyCondition = conditions.find((c) => c.field === shardingConfig.key); + + if (shardKeyCondition && shardKeyCondition.operator === '=') { + // Single shard query + const shard = this.framework.shardManager.getShardForKey( + this.model.modelName, + shardKeyCondition.value, + ); + return await this.queryDatabase(shard.database, this.model.dbType); + } else if (shardKeyCondition && shardKeyCondition.operator === 'in') { + // Multiple specific shards + const results: T[] = []; + const shardKeys = shardKeyCondition.value; + + const shardQueries = shardKeys.map(async (key: string) => { + const shard = this.framework.shardManager.getShardForKey(this.model.modelName, key); + return await this.queryDatabase(shard.database, this.model.dbType); + }); + + const shardResults = await Promise.all(shardQueries); + for (const shardResult of shardResults) { + results.push(...shardResult); + } + + return this.postProcessResults(results); + } else { + // Query all shards + const results: T[] = []; + const allShards = this.framework.shardManager.getAllShards(this.model.modelName); + + const promises = allShards.map((shard: any) => + this.queryDatabase(shard.database, this.model.dbType), + ); + const shardResults = await Promise.all(promises); + + for (const shardResult of shardResults) { + results.push(...shardResult); + } + + return this.postProcessResults(results); + } + } + + private async queryDatabase(database: any, dbType: StoreType): Promise<T[]> { + // Get all documents from OrbitDB based on database type + let documents: any[]; + + try { + documents = await this.framework.databaseManager.getAllDocuments(database, dbType); + } catch (error) { + console.error(`Error querying ${dbType} database:`, error); + return []; + } + + // Apply filters in memory + documents = this.applyFilters(documents); + + // Apply sorting + documents = this.applySorting(documents); + + // Apply limit/offset + documents = this.applyLimitOffset(documents); + + // Convert to model instances + const ModelClass = this.model as any; // Type assertion for abstract class + return documents.map((doc) => new ModelClass(doc) as T); + } + + private async fetchActualDocuments(indexResults: any[]): Promise<T[]> { + console.log(`📄 Fetching ${indexResults.length} documents from user databases`); + + const results: T[] = []; + + // Group by userId for efficient database access + const userGroups = new Map<string, any[]>(); + + for (const indexEntry of indexResults) { + const userId = indexEntry.userId; + if (!userGroups.has(userId)) { + userGroups.set(userId, []); + } + userGroups.get(userId)!.push(indexEntry); + } + + // Fetch documents from each user's database + const promises = Array.from(userGroups.entries()).map(async ([userId, entries]) => { + try { + const userDB = await this.framework.databaseManager.getUserDatabase( + userId, + this.model.modelName, + ); + + const userResults: T[] = []; + + // Fetch specific documents by ID + for (const entry of entries) { + try { + const doc = await this.getDocumentById(userDB, this.model.dbType, entry.id); + if (doc) { + const ModelClass = this.model as any; // Type assertion for abstract class + userResults.push(new ModelClass(doc) as T); + } + } catch (error) { + console.warn(`Failed to fetch document ${entry.id} from user ${userId}:`, error); + } + } + + return userResults; + } catch (error) { + console.warn(`Failed to access user ${userId} database:`, error); + return []; + } + }); + + const userResults = await Promise.all(promises); + + // Flatten results + for (const userResult of userResults) { + results.push(...userResult); + } + + return this.postProcessResults(results); + } + + private async getDocumentById(database: any, dbType: StoreType, id: string): Promise<any | null> { + try { + switch (dbType) { + case 'keyvalue': + return await database.get(id); + + case 'docstore': + return await database.get(id); + + case 'eventlog': + case 'feed': + // For append-only stores, we need to search through entries + const iterator = database.iterator(); + const entries = iterator.collect(); + return ( + entries.find((entry: any) => entry.payload?.value?.id === id)?.payload?.value || null + ); + + default: + return null; + } + } catch (error) { + console.warn(`Error fetching document ${id} from ${dbType}:`, error); + return null; + } + } + + private applyFilters(documents: any[]): any[] { + const conditions = this.query.getConditions(); + + return documents.filter((doc) => { + return conditions.every((condition) => { + return this.evaluateCondition(doc, condition); + }); + }); + } + + private evaluateCondition(doc: any, condition: QueryCondition): boolean { + const { field, operator, value } = condition; + + // Handle special operators + if (operator === 'or') { + return value.some((subCondition: QueryCondition) => + this.evaluateCondition(doc, subCondition), + ); + } + + if (field === '__raw__') { + // Raw conditions would need custom evaluation + console.warn('Raw conditions not fully implemented'); + return true; + } + + const docValue = this.getNestedValue(doc, field); + + switch (operator) { + case '=': + case '==': + return docValue === value; + + case '!=': + case '<>': + return docValue !== value; + + case '>': + return docValue > value; + + case '>=': + case 'gte': + return docValue >= value; + + case '<': + return docValue < value; + + case '<=': + case 'lte': + return docValue <= value; + + case 'in': + return Array.isArray(value) && value.includes(docValue); + + case 'not_in': + return Array.isArray(value) && !value.includes(docValue); + + case 'contains': + return Array.isArray(docValue) && docValue.includes(value); + + case 'like': + return String(docValue).toLowerCase().includes(String(value).toLowerCase()); + + case 'ilike': + return String(docValue).toLowerCase().includes(String(value).toLowerCase()); + + case 'is_null': + return docValue === null || docValue === undefined; + + case 'is_not_null': + return docValue !== null && docValue !== undefined; + + case 'between': + return Array.isArray(value) && docValue >= value[0] && docValue <= value[1]; + + case 'array_contains': + return Array.isArray(docValue) && docValue.includes(value); + + case 'array_length_=': + return Array.isArray(docValue) && docValue.length === value; + + case 'array_length_>': + return Array.isArray(docValue) && docValue.length > value; + + case 'array_length_<': + return Array.isArray(docValue) && docValue.length < value; + + case 'object_has_key': + return typeof docValue === 'object' && docValue !== null && value in docValue; + + case 'date_=': + return this.compareDates(docValue, '=', value); + + case 'date_>': + return this.compareDates(docValue, '>', value); + + case 'date_<': + return this.compareDates(docValue, '<', value); + + case 'date_between': + return ( + this.compareDates(docValue, '>=', value[0]) && this.compareDates(docValue, '<=', value[1]) + ); + + case 'year': + return this.getDatePart(docValue, 'year') === value; + + case 'month': + return this.getDatePart(docValue, 'month') === value; + + case 'day': + return this.getDatePart(docValue, 'day') === value; + + default: + console.warn(`Unsupported operator: ${operator}`); + return true; + } + } + + private compareDates(docValue: any, operator: string, compareValue: any): boolean { + const docDate = this.normalizeDate(docValue); + const compDate = this.normalizeDate(compareValue); + + if (!docDate || !compDate) return false; + + switch (operator) { + case '=': + return docDate.getTime() === compDate.getTime(); + case '>': + return docDate.getTime() > compDate.getTime(); + case '<': + return docDate.getTime() < compDate.getTime(); + case '>=': + return docDate.getTime() >= compDate.getTime(); + case '<=': + return docDate.getTime() <= compDate.getTime(); + default: + return false; + } + } + + private normalizeDate(value: any): Date | null { + if (value instanceof Date) return value; + if (typeof value === 'number') return new Date(value); + if (typeof value === 'string') return new Date(value); + return null; + } + + private getDatePart(value: any, part: 'year' | 'month' | 'day'): number | null { + const date = this.normalizeDate(value); + if (!date) return null; + + switch (part) { + case 'year': + return date.getFullYear(); + case 'month': + return date.getMonth() + 1; // 1-based month + case 'day': + return date.getDate(); + default: + return null; + } + } + + private applySorting(documents: any[]): any[] { + const sorting = this.query.getSorting(); + + if (sorting.length === 0) { + return documents; + } + + return documents.sort((a, b) => { + for (const sort of sorting) { + const aValue = this.getNestedValue(a, sort.field); + const bValue = this.getNestedValue(b, sort.field); + + let comparison = 0; + + if (aValue < bValue) comparison = -1; + else if (aValue > bValue) comparison = 1; + + if (comparison !== 0) { + return sort.direction === 'desc' ? -comparison : comparison; + } + } + + return 0; + }); + } + + private applyLimitOffset(documents: any[]): any[] { + const limit = this.query.getLimit(); + const offset = this.query.getOffset(); + + let result = documents; + + if (offset && offset > 0) { + result = result.slice(offset); + } + + if (limit && limit > 0) { + result = result.slice(0, limit); + } + + return result; + } + + private postProcessResults(results: T[]): T[] { + // Apply global sorting across all results + results = this.applySorting(results); + + // Apply global limit/offset + results = this.applyLimitOffset(results); + + return results; + } + + private getNestedValue(obj: any, path: string): any { + if (!path) return obj; + + const keys = path.split('.'); + let current = obj; + + for (const key of keys) { + if (current === null || current === undefined) { + return undefined; + } + current = current[key]; + } + + return current; + } + + // Public methods for query control + disableCache(): this { + this.useCache = false; + return this; + } + + enableCache(): this { + this.useCache = true; + return this; + } + + getQueryPlan(): QueryPlan | undefined { + return this.queryPlan; + } + + explain(): any { + const plan = this.queryPlan || QueryOptimizer.analyzeQuery(this.query); + const suggestions = QueryOptimizer.suggestOptimizations(this.query); + + return { + query: this.query.explain(), + plan, + suggestions, + estimatedResultSize: QueryOptimizer.estimateResultSize(this.query), + }; + } + + private getFrameworkInstance(): any { + const framework = (globalThis as any).__debrosFramework; + if (!framework) { + throw new Error('Framework not initialized. Call framework.initialize() first.'); + } + return framework; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { QueryBuilder } from './QueryBuilder'; +import { QueryCondition } from '../types/queries'; +import { BaseModel } from '../models/BaseModel'; + +export interface QueryPlan { + strategy: 'single_user' | 'multi_user' | 'global_index' | 'all_shards' | 'specific_shards'; + targetDatabases: string[]; + estimatedCost: number; + indexHints: string[]; + optimizations: string[]; +} + +export class QueryOptimizer { + static analyzeQuery<T extends BaseModel>(query: QueryBuilder<T>): QueryPlan { + const model = query.getModel(); + const conditions = query.getConditions(); + const relations = query.getRelations(); + const limit = query.getLimit(); + + let strategy: QueryPlan['strategy'] = 'all_shards'; + let targetDatabases: string[] = []; + let estimatedCost = 100; // Base cost + let indexHints: string[] = []; + let optimizations: string[] = []; + + // Analyze based on model scope + if (model.scope === 'user') { + const userConditions = conditions.filter( + (c) => c.field === 'userId' || c.operator === 'userIn', + ); + + if (userConditions.length > 0) { + const userCondition = userConditions[0]; + + if (userCondition.operator === 'userIn') { + strategy = 'multi_user'; + targetDatabases = userCondition.value.map( + (userId: string) => `${userId}-${model.modelName.toLowerCase()}`, + ); + estimatedCost = 20 * userCondition.value.length; + optimizations.push('Direct user database access'); + } else { + strategy = 'single_user'; + targetDatabases = [`${userCondition.value}-${model.modelName.toLowerCase()}`]; + estimatedCost = 10; + optimizations.push('Single user database access'); + } + } else { + strategy = 'global_index'; + targetDatabases = [`${model.modelName}GlobalIndex`]; + estimatedCost = 50; + indexHints.push(`${model.modelName}GlobalIndex`); + optimizations.push('Global index lookup'); + } + } else { + // Global model + if (model.sharding) { + const shardKeyCondition = conditions.find((c) => c.field === model.sharding!.key); + + if (shardKeyCondition) { + if (shardKeyCondition.operator === '=') { + strategy = 'specific_shards'; + targetDatabases = [`${model.modelName}-shard-specific`]; + estimatedCost = 15; + optimizations.push('Single shard access'); + } else if (shardKeyCondition.operator === 'in') { + strategy = 'specific_shards'; + targetDatabases = shardKeyCondition.value.map( + (_: any, i: number) => `${model.modelName}-shard-${i}`, + ); + estimatedCost = 15 * shardKeyCondition.value.length; + optimizations.push('Multiple specific shards'); + } + } else { + strategy = 'all_shards'; + estimatedCost = 30 * (model.sharding.count || 4); + optimizations.push('All shards scan'); + } + } else { + strategy = 'single_user'; // Actually single global database + targetDatabases = [`global-${model.modelName.toLowerCase()}`]; + estimatedCost = 25; + optimizations.push('Single global database'); + } + } + + // Adjust cost based on other factors + if (limit && limit < 100) { + estimatedCost *= 0.8; + optimizations.push(`Limit optimization (${limit})`); + } + + if (relations.length > 0) { + estimatedCost *= 1 + relations.length * 0.3; + optimizations.push(`Relationship loading (${relations.length})`); + } + + // Suggest indexes based on conditions + const indexedFields = conditions + .filter((c) => c.field !== 'userId' && c.field !== '__or__' && c.field !== '__raw__') + .map((c) => c.field); + + if (indexedFields.length > 0) { + indexHints.push(...indexedFields.map((field) => `${model.modelName}_${field}_idx`)); + } + + return { + strategy, + targetDatabases, + estimatedCost, + indexHints, + optimizations, + }; + } + + static optimizeConditions(conditions: QueryCondition[]): QueryCondition[] { + const optimized = [...conditions]; + + // Remove redundant conditions + const seen = new Set(); + const filtered = optimized.filter((condition) => { + const key = `${condition.field}_${condition.operator}_${JSON.stringify(condition.value)}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + + // Sort conditions by selectivity (most selective first) + return filtered.sort((a, b) => { + const selectivityA = this.getConditionSelectivity(a); + const selectivityB = this.getConditionSelectivity(b); + return selectivityA - selectivityB; + }); + } + + private static getConditionSelectivity(condition: QueryCondition): number { + // Lower numbers = more selective (better to evaluate first) + switch (condition.operator) { + case '=': + return 1; + case 'in': + return Array.isArray(condition.value) ? condition.value.length : 10; + case '>': + case '<': + case '>=': + case '<=': + return 50; + case 'like': + case 'ilike': + return 75; + case 'is_not_null': + return 90; + default: + return 100; + } + } + + static shouldUseIndex(field: string, operator: string, model: typeof BaseModel): boolean { + // Check if field has index configuration + const fieldConfig = model.fields?.get(field); + if (fieldConfig?.index) { + return true; + } + + // Certain operators benefit from indexes + const indexBeneficialOps = ['=', 'in', '>', '<', '>=', '<=', 'between']; + return indexBeneficialOps.includes(operator); + } + + static estimateResultSize(query: QueryBuilder<any>): number { + const conditions = query.getConditions(); + const limit = query.getLimit(); + + // If there's a limit, that's our upper bound + if (limit) { + return limit; + } + + // Estimate based on conditions + let estimate = 1000; // Base estimate + + for (const condition of conditions) { + switch (condition.operator) { + case '=': + estimate *= 0.1; // Very selective + break; + case 'in': + estimate *= Array.isArray(condition.value) ? condition.value.length * 0.1 : 0.1; + break; + case '>': + case '<': + case '>=': + case '<=': + estimate *= 0.5; // Moderately selective + break; + case 'like': + estimate *= 0.3; // Somewhat selective + break; + default: + estimate *= 0.8; + } + } + + return Math.max(1, Math.round(estimate)); + } + + static suggestOptimizations<T extends BaseModel>(query: QueryBuilder<T>): string[] { + const suggestions: string[] = []; + const conditions = query.getConditions(); + const model = query.getModel(); + const limit = query.getLimit(); + + // Check for missing userId in user-scoped queries + if (model.scope === 'user') { + const hasUserFilter = conditions.some((c) => c.field === 'userId' || c.operator === 'userIn'); + if (!hasUserFilter) { + suggestions.push('Add userId filter to avoid expensive global index query'); + } + } + + // Check for missing limit on potentially large result sets + if (!limit) { + const estimatedSize = this.estimateResultSize(query); + if (estimatedSize > 100) { + suggestions.push('Add limit() to prevent large result sets'); + } + } + + // Check for unindexed field queries + for (const condition of conditions) { + if (!this.shouldUseIndex(condition.field, condition.operator, model)) { + suggestions.push(`Consider adding index for field: ${condition.field}`); + } + } + + // Check for expensive operations + const expensiveOps = conditions.filter((c) => + ['like', 'ilike', 'array_contains'].includes(c.operator), + ); + if (expensiveOps.length > 0) { + suggestions.push('Consider using more selective filters before expensive operations'); + } + + // Check for OR conditions + const orConditions = conditions.filter((c) => c.operator === 'or'); + if (orConditions.length > 0) { + suggestions.push('OR conditions can be expensive, consider restructuring query'); + } + + return suggestions; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
QueryBuilder.ts | +
+
+ |
+ 0% | +0/142 | +0% | +0/22 | +0% | +0/69 | +0% | +0/141 | +
QueryCache.ts | +
+
+ |
+ 0% | +0/130 | +0% | +0/35 | +0% | +0/29 | +0% | +0/123 | +
QueryExecutor.ts | +
+
+ |
+ 0% | +0/270 | +0% | +0/171 | +0% | +0/46 | +0% | +0/256 | +
QueryOptimizer.ts | +
+
+ |
+ 0% | +0/130 | +0% | +0/73 | +0% | +0/18 | +0% | +0/126 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { RelationshipConfig } from '../types/models'; +import { RelationshipManager, RelationshipLoadOptions } from './RelationshipManager'; + +export interface LazyLoadPromise<T> extends Promise<T> { + isLoaded(): boolean; + getLoadedValue(): T | undefined; + reload(options?: RelationshipLoadOptions): Promise<T>; +} + +export class LazyLoader { + private relationshipManager: RelationshipManager; + + constructor(relationshipManager: RelationshipManager) { + this.relationshipManager = relationshipManager; + } + + createLazyProperty<T>( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): LazyLoadPromise<T> { + let loadPromise: Promise<T> | null = null; + let loadedValue: T | undefined = undefined; + let isLoaded = false; + + const loadRelationship = async (): Promise<T> => { + if (loadPromise) { + return loadPromise; + } + + loadPromise = this.relationshipManager + .loadRelationship(instance, relationshipName, options) + .then((result: T) => { + loadedValue = result; + isLoaded = true; + return result; + }) + .catch((error) => { + loadPromise = null; // Reset so it can be retried + throw error; + }); + + return loadPromise; + }; + + const reload = async (newOptions?: RelationshipLoadOptions): Promise<T> => { + // Clear cache for this relationship + this.relationshipManager.invalidateRelationshipCache(instance, relationshipName); + + // Reset state + loadPromise = null; + loadedValue = undefined; + isLoaded = false; + + // Load with new options + const finalOptions = newOptions ? { ...options, ...newOptions } : options; + return this.relationshipManager.loadRelationship(instance, relationshipName, finalOptions); + }; + + // Create the main promise + const promise = loadRelationship() as LazyLoadPromise<T>; + + // Add custom methods + promise.isLoaded = () => isLoaded; + promise.getLoadedValue = () => loadedValue; + promise.reload = reload; + + return promise; + } + + createLazyPropertyWithProxy<T>( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): T { + const lazyPromise = this.createLazyProperty<T>(instance, relationshipName, config, options); + + // For single relationships, return a proxy that loads on property access + if (config.type === 'belongsTo' || config.type === 'hasOne') { + return new Proxy({} as any, { + get(target: any, prop: string | symbol) { + // Special methods + if (prop === 'then') { + return lazyPromise.then.bind(lazyPromise); + } + if (prop === 'catch') { + return lazyPromise.catch.bind(lazyPromise); + } + if (prop === 'finally') { + return lazyPromise.finally.bind(lazyPromise); + } + if (prop === 'isLoaded') { + return lazyPromise.isLoaded; + } + if (prop === 'reload') { + return lazyPromise.reload; + } + + // If already loaded, return the property from loaded value + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue(); + return loadedValue ? (loadedValue as any)[prop] : undefined; + } + + // Trigger loading and return undefined for now + lazyPromise.catch(() => {}); // Prevent unhandled promise rejection + return undefined; + }, + + has(target: any, prop: string | symbol) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue(); + return loadedValue ? prop in (loadedValue as any) : false; + } + return false; + }, + + ownKeys(_target: any) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue(); + return loadedValue ? Object.keys(loadedValue as any) : []; + } + return []; + }, + }); + } + + // For collection relationships, return a proxy array + if (config.type === 'hasMany' || config.type === 'manyToMany') { + return new Proxy([] as any, { + get(target: any[], prop: string | symbol) { + // Array methods and properties + if (prop === 'length') { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? loadedValue.length : 0; + } + return 0; + } + + // Promise methods + if (prop === 'then') { + return lazyPromise.then.bind(lazyPromise); + } + if (prop === 'catch') { + return lazyPromise.catch.bind(lazyPromise); + } + if (prop === 'finally') { + return lazyPromise.finally.bind(lazyPromise); + } + if (prop === 'isLoaded') { + return lazyPromise.isLoaded; + } + if (prop === 'reload') { + return lazyPromise.reload; + } + + // Array methods that should trigger loading + if ( + typeof prop === 'string' && + [ + 'forEach', + 'map', + 'filter', + 'find', + 'some', + 'every', + 'reduce', + 'slice', + 'indexOf', + 'includes', + ].includes(prop) + ) { + return async (...args: any[]) => { + const loadedValue = await lazyPromise; + return (loadedValue as any)[prop](...args); + }; + } + + // Numeric index access + if (typeof prop === 'string' && /^\d+$/.test(prop)) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? loadedValue[parseInt(prop, 10)] : undefined; + } + // Trigger loading + lazyPromise.catch(() => {}); + return undefined; + } + + // If already loaded, delegate to the actual array + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? (loadedValue as any)[prop] : undefined; + } + + return undefined; + }, + + has(target: any[], prop: string | symbol) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? prop in loadedValue : false; + } + return false; + }, + + ownKeys(_target: any[]) { + if (lazyPromise.isLoaded()) { + const loadedValue = lazyPromise.getLoadedValue() as any[]; + return loadedValue ? Object.keys(loadedValue) : []; + } + return []; + }, + }) as T; + } + + // Fallback to promise for other types + return lazyPromise as any; + } + + // Helper method to check if a value is a lazy-loaded relationship + static isLazyLoaded(value: any): value is LazyLoadPromise<any> { + return ( + value && + typeof value === 'object' && + typeof value.then === 'function' && + typeof value.isLoaded === 'function' && + typeof value.reload === 'function' + ); + } + + // Helper method to await all lazy relationships in an object + static async resolveAllLazy(obj: any): Promise<any> { + if (!obj || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return Promise.all(obj.map((item) => this.resolveAllLazy(item))); + } + + const resolved: any = {}; + const promises: Array<Promise<void>> = []; + + for (const [key, value] of Object.entries(obj)) { + if (this.isLazyLoaded(value)) { + promises.push( + value.then((resolvedValue) => { + resolved[key] = resolvedValue; + }), + ); + } else { + resolved[key] = value; + } + } + + await Promise.all(promises); + return resolved; + } + + // Helper method to get loaded relationships without triggering loading + static getLoadedRelationships(instance: BaseModel): Record<string, any> { + const loaded: Record<string, any> = {}; + + const loadedRelations = instance.getLoadedRelations(); + for (const relationName of loadedRelations) { + const value = instance.getRelation(relationName); + if (this.isLazyLoaded(value)) { + if (value.isLoaded()) { + loaded[relationName] = value.getLoadedValue(); + } + } else { + loaded[relationName] = value; + } + } + + return loaded; + } + + // Helper method to preload specific relationships + static async preloadRelationships( + instances: BaseModel[], + relationships: string[], + relationshipManager: RelationshipManager, + ): Promise<void> { + await relationshipManager.eagerLoadRelationships(instances, relationships); + } + + // Helper method to create lazy collection with advanced features + createLazyCollection<T extends BaseModel>( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): LazyCollection<T> { + return new LazyCollection<T>( + instance, + relationshipName, + config, + options, + this.relationshipManager, + ); + } +} + +// Advanced lazy collection with pagination and filtering +export class LazyCollection<T extends BaseModel> { + private instance: BaseModel; + private relationshipName: string; + private config: RelationshipConfig; + private options: RelationshipLoadOptions; + private relationshipManager: RelationshipManager; + private loadedItems: T[] = []; + private isFullyLoaded = false; + private currentPage = 1; + private pageSize = 20; + + constructor( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + relationshipManager: RelationshipManager, + ) { + this.instance = instance; + this.relationshipName = relationshipName; + this.config = config; + this.options = options; + this.relationshipManager = relationshipManager; + } + + async loadPage(page: number = 1, pageSize: number = this.pageSize): Promise<T[]> { + const offset = (page - 1) * pageSize; + + const pageOptions: RelationshipLoadOptions = { + ...this.options, + constraints: (query) => { + let q = query.offset(offset).limit(pageSize); + if (this.options.constraints) { + q = this.options.constraints(q); + } + return q; + }, + }; + + const pageItems = (await this.relationshipManager.loadRelationship( + this.instance, + this.relationshipName, + pageOptions, + )) as T[]; + + // Update loaded items if this is sequential loading + if (page === this.currentPage) { + this.loadedItems.push(...pageItems); + this.currentPage++; + + if (pageItems.length < pageSize) { + this.isFullyLoaded = true; + } + } + + return pageItems; + } + + async loadMore(count: number = this.pageSize): Promise<T[]> { + return this.loadPage(this.currentPage, count); + } + + async loadAll(): Promise<T[]> { + if (this.isFullyLoaded) { + return this.loadedItems; + } + + const allItems = (await this.relationshipManager.loadRelationship( + this.instance, + this.relationshipName, + this.options, + )) as T[]; + + this.loadedItems = allItems; + this.isFullyLoaded = true; + + return allItems; + } + + getLoadedItems(): T[] { + return [...this.loadedItems]; + } + + isLoaded(): boolean { + return this.loadedItems.length > 0; + } + + isCompletelyLoaded(): boolean { + return this.isFullyLoaded; + } + + async filter(predicate: (item: T) => boolean): Promise<T[]> { + if (!this.isFullyLoaded) { + await this.loadAll(); + } + return this.loadedItems.filter(predicate); + } + + async find(predicate: (item: T) => boolean): Promise<T | undefined> { + // Try loaded items first + const found = this.loadedItems.find(predicate); + if (found) { + return found; + } + + // If not fully loaded, load all and search + if (!this.isFullyLoaded) { + await this.loadAll(); + return this.loadedItems.find(predicate); + } + + return undefined; + } + + async count(): Promise<number> { + if (this.isFullyLoaded) { + return this.loadedItems.length; + } + + // For a complete count, we need to load all items + // In a more sophisticated implementation, we might have a separate count query + await this.loadAll(); + return this.loadedItems.length; + } + + clear(): void { + this.loadedItems = []; + this.isFullyLoaded = false; + this.currentPage = 1; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; + +export interface RelationshipCacheEntry { + key: string; + data: any; + timestamp: number; + ttl: number; + modelType: string; + relationshipType: string; +} + +export interface RelationshipCacheStats { + totalEntries: number; + hitCount: number; + missCount: number; + hitRate: number; + memoryUsage: number; +} + +export class RelationshipCache { + private cache: Map<string, RelationshipCacheEntry> = new Map(); + private maxSize: number; + private defaultTTL: number; + private stats: RelationshipCacheStats; + + constructor(maxSize: number = 1000, defaultTTL: number = 600000) { + // 10 minutes default + this.maxSize = maxSize; + this.defaultTTL = defaultTTL; + this.stats = { + totalEntries: 0, + hitCount: 0, + missCount: 0, + hitRate: 0, + memoryUsage: 0, + }; + } + + generateKey(instance: BaseModel, relationshipName: string, extraData?: any): string { + const baseKey = `${instance.constructor.name}:${instance.id}:${relationshipName}`; + + if (extraData) { + const extraStr = JSON.stringify(extraData); + return `${baseKey}:${this.hashString(extraStr)}`; + } + + return baseKey; + } + + get(key: string): any | null { + const entry = this.cache.get(key); + + if (!entry) { + this.stats.missCount++; + this.updateHitRate(); + return null; + } + + // Check if entry has expired + if (Date.now() - entry.timestamp > entry.ttl) { + this.cache.delete(key); + this.stats.missCount++; + this.updateHitRate(); + return null; + } + + this.stats.hitCount++; + this.updateHitRate(); + + return this.deserializeData(entry.data, entry.modelType); + } + + set( + key: string, + data: any, + modelType: string, + relationshipType: string, + customTTL?: number, + ): void { + const ttl = customTTL || this.defaultTTL; + + // Check if we need to evict entries + if (this.cache.size >= this.maxSize) { + this.evictOldest(); + } + + const entry: RelationshipCacheEntry = { + key, + data: this.serializeData(data), + timestamp: Date.now(), + ttl, + modelType, + relationshipType, + }; + + this.cache.set(key, entry); + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + } + + invalidate(key: string): boolean { + const deleted = this.cache.delete(key); + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return deleted; + } + + invalidateByInstance(instance: BaseModel): number { + const prefix = `${instance.constructor.name}:${instance.id}:`; + let deletedCount = 0; + + for (const [key] of this.cache.entries()) { + if (key.startsWith(prefix)) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return deletedCount; + } + + invalidateByModel(modelName: string): number { + let deletedCount = 0; + + for (const [key, entry] of this.cache.entries()) { + if (key.startsWith(`${modelName}:`) || entry.modelType === modelName) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return deletedCount; + } + + invalidateByRelationship(relationshipType: string): number { + let deletedCount = 0; + + for (const [key, entry] of this.cache.entries()) { + if (entry.relationshipType === relationshipType) { + this.cache.delete(key); + deletedCount++; + } + } + + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return deletedCount; + } + + clear(): void { + this.cache.clear(); + this.stats = { + totalEntries: 0, + hitCount: 0, + missCount: 0, + hitRate: 0, + memoryUsage: 0, + }; + } + + getStats(): RelationshipCacheStats { + return { ...this.stats }; + } + + // Preload relationships for multiple instances + async warmup( + instances: BaseModel[], + relationships: string[], + loadFunction: (instance: BaseModel, relationshipName: string) => Promise<any>, + ): Promise<void> { + console.log(`🔥 Warming relationship cache for ${instances.length} instances...`); + + const promises: Promise<void>[] = []; + + for (const instance of instances) { + for (const relationshipName of relationships) { + promises.push( + loadFunction(instance, relationshipName) + .then((data) => { + const key = this.generateKey(instance, relationshipName); + const modelType = data?.constructor?.name || 'unknown'; + this.set(key, data, modelType, relationshipName); + }) + .catch((error) => { + console.warn( + `Failed to warm cache for ${instance.constructor.name}:${instance.id}:${relationshipName}:`, + error, + ); + }), + ); + } + } + + await Promise.allSettled(promises); + console.log(`✅ Relationship cache warmed with ${promises.length} entries`); + } + + // Get cache entries by relationship type + getEntriesByRelationship(relationshipType: string): RelationshipCacheEntry[] { + return Array.from(this.cache.values()).filter( + (entry) => entry.relationshipType === relationshipType, + ); + } + + // Get expired entries + getExpiredEntries(): string[] { + const now = Date.now(); + const expired: string[] = []; + + for (const [key, entry] of this.cache.entries()) { + if (now - entry.timestamp > entry.ttl) { + expired.push(key); + } + } + + return expired; + } + + // Cleanup expired entries + cleanup(): number { + const expired = this.getExpiredEntries(); + + for (const key of expired) { + this.cache.delete(key); + } + + this.stats.totalEntries = this.cache.size; + this.updateMemoryUsage(); + return expired.length; + } + + // Performance analysis + analyzePerformance(): { + averageAge: number; + oldestEntry: number; + newestEntry: number; + relationshipTypes: Map<string, number>; + } { + const now = Date.now(); + let totalAge = 0; + let oldestAge = 0; + let newestAge = Infinity; + const relationshipTypes = new Map<string, number>(); + + for (const entry of this.cache.values()) { + const age = now - entry.timestamp; + totalAge += age; + + if (age > oldestAge) oldestAge = age; + if (age < newestAge) newestAge = age; + + const count = relationshipTypes.get(entry.relationshipType) || 0; + relationshipTypes.set(entry.relationshipType, count + 1); + } + + return { + averageAge: this.cache.size > 0 ? totalAge / this.cache.size : 0, + oldestEntry: oldestAge, + newestEntry: newestAge === Infinity ? 0 : newestAge, + relationshipTypes, + }; + } + + private serializeData(data: any): any { + if (Array.isArray(data)) { + return data.map((item) => this.serializeItem(item)); + } else { + return this.serializeItem(data); + } + } + + private serializeItem(item: any): any { + if (item && typeof item.toJSON === 'function') { + return { + __type: item.constructor.name, + __data: item.toJSON(), + }; + } + return item; + } + + private deserializeData(data: any, expectedType: string): any { + if (Array.isArray(data)) { + return data.map((item) => this.deserializeItem(item, expectedType)); + } else { + return this.deserializeItem(data, expectedType); + } + } + + private deserializeItem(item: any, _expectedType: string): any { + if (item && item.__type && item.__data) { + // For now, return the raw data + // In a full implementation, we would reconstruct the model instance + return item.__data; + } + return item; + } + + private evictOldest(): void { + if (this.cache.size === 0) return; + + let oldestKey: string | null = null; + let oldestTime = Infinity; + + for (const [key, entry] of this.cache.entries()) { + if (entry.timestamp < oldestTime) { + oldestTime = entry.timestamp; + oldestKey = key; + } + } + + if (oldestKey) { + this.cache.delete(oldestKey); + } + } + + private updateHitRate(): void { + const total = this.stats.hitCount + this.stats.missCount; + this.stats.hitRate = total > 0 ? this.stats.hitCount / total : 0; + } + + private updateMemoryUsage(): void { + // Rough estimation of memory usage + let size = 0; + for (const entry of this.cache.values()) { + size += JSON.stringify(entry.data).length; + } + this.stats.memoryUsage = size; + } + + private hashString(str: string): string { + let hash = 0; + if (str.length === 0) return hash.toString(); + + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + + return Math.abs(hash).toString(36); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { RelationshipConfig } from '../types/models'; +import { RelationshipCache } from './RelationshipCache'; +import { QueryBuilder } from '../query/QueryBuilder'; + +export interface RelationshipLoadOptions { + useCache?: boolean; + constraints?: (query: QueryBuilder<any>) => QueryBuilder<any>; + limit?: number; + orderBy?: { field: string; direction: 'asc' | 'desc' }; +} + +export interface EagerLoadPlan { + relationshipName: string; + config: RelationshipConfig; + instances: BaseModel[]; + options?: RelationshipLoadOptions; +} + +export class RelationshipManager { + private framework: any; + private cache: RelationshipCache; + + constructor(framework: any) { + this.framework = framework; + this.cache = new RelationshipCache(); + } + + async loadRelationship( + instance: BaseModel, + relationshipName: string, + options: RelationshipLoadOptions = {}, + ): Promise<any> { + const modelClass = instance.constructor as typeof BaseModel; + const relationConfig = modelClass.relationships?.get(relationshipName); + + if (!relationConfig) { + throw new Error(`Relationship '${relationshipName}' not found on ${modelClass.name}`); + } + + console.log( + `🔗 Loading ${relationConfig.type} relationship: ${modelClass.name}.${relationshipName}`, + ); + + // Check cache first if enabled + if (options.useCache !== false) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const cached = this.cache.get(cacheKey); + if (cached) { + console.log(`⚡ Cache hit for relationship ${relationshipName}`); + instance._loadedRelations.set(relationshipName, cached); + return cached; + } + } + + // Load relationship based on type + let result: any; + switch (relationConfig.type) { + case 'belongsTo': + result = await this.loadBelongsTo(instance, relationConfig, options); + break; + case 'hasMany': + result = await this.loadHasMany(instance, relationConfig, options); + break; + case 'hasOne': + result = await this.loadHasOne(instance, relationConfig, options); + break; + case 'manyToMany': + result = await this.loadManyToMany(instance, relationConfig, options); + break; + default: + throw new Error(`Unsupported relationship type: ${relationConfig.type}`); + } + + // Cache the result if enabled + if (options.useCache !== false && result) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const modelType = Array.isArray(result) + ? result[0]?.constructor?.name || 'unknown' + : result.constructor?.name || 'unknown'; + + this.cache.set(cacheKey, result, modelType, relationConfig.type); + } + + // Store in instance + instance.setRelation(relationshipName, result); + + console.log( + `✅ Loaded ${relationConfig.type} relationship: ${Array.isArray(result) ? result.length : 1} item(s)`, + ); + return result; + } + + private async loadBelongsTo( + instance: BaseModel, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<BaseModel | null> { + const foreignKeyValue = (instance as any)[config.foreignKey]; + + if (!foreignKeyValue) { + return null; + } + + // Build query for the related model + let query = (config.model as any).where('id', '=', foreignKeyValue); + + // Apply constraints if provided + if (options.constraints) { + query = options.constraints(query); + } + + const result = await query.first(); + return result; + } + + private async loadHasMany( + instance: BaseModel, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<BaseModel[]> { + if (config.through) { + return await this.loadManyToMany(instance, config, options); + } + + const localKeyValue = (instance as any)[config.localKey || 'id']; + + if (!localKeyValue) { + return []; + } + + // Build query for the related model + let query = (config.model as any).where(config.foreignKey, '=', localKeyValue); + + // Apply constraints if provided + if (options.constraints) { + query = options.constraints(query); + } + + // Apply default ordering and limiting + if (options.orderBy) { + query = query.orderBy(options.orderBy.field, options.orderBy.direction); + } + + if (options.limit) { + query = query.limit(options.limit); + } + + return await query.exec(); + } + + private async loadHasOne( + instance: BaseModel, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<BaseModel | null> { + const results = await this.loadHasMany( + instance, + { ...config, type: 'hasMany' }, + { + ...options, + limit: 1, + }, + ); + + return results[0] || null; + } + + private async loadManyToMany( + instance: BaseModel, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<BaseModel[]> { + if (!config.through) { + throw new Error('Many-to-many relationships require a through model'); + } + + const localKeyValue = (instance as any)[config.localKey || 'id']; + + if (!localKeyValue) { + return []; + } + + // Step 1: Get junction table records + let junctionQuery = (config.through as any).where(config.localKey || 'id', '=', localKeyValue); + + // Apply constraints to junction if needed + if (options.constraints) { + // Note: This is simplified - in a full implementation we'd need to handle + // constraints that apply to the final model vs the junction model + } + + const junctionRecords = await junctionQuery.exec(); + + if (junctionRecords.length === 0) { + return []; + } + + // Step 2: Extract foreign keys + const foreignKeys = junctionRecords.map((record: any) => record[config.foreignKey]); + + // Step 3: Get related models + let relatedQuery = (config.model as any).whereIn('id', foreignKeys); + + // Apply constraints if provided + if (options.constraints) { + relatedQuery = options.constraints(relatedQuery); + } + + // Apply ordering and limiting + if (options.orderBy) { + relatedQuery = relatedQuery.orderBy(options.orderBy.field, options.orderBy.direction); + } + + if (options.limit) { + relatedQuery = relatedQuery.limit(options.limit); + } + + return await relatedQuery.exec(); + } + + // Eager loading for multiple instances + async eagerLoadRelationships( + instances: BaseModel[], + relationships: string[], + options: Record<string, RelationshipLoadOptions> = {}, + ): Promise<void> { + if (instances.length === 0) return; + + console.log( + `🚀 Eager loading ${relationships.length} relationships for ${instances.length} instances`, + ); + + // Group instances by model type for efficient processing + const instanceGroups = this.groupInstancesByModel(instances); + + // Load each relationship for each model group + for (const relationshipName of relationships) { + await this.eagerLoadSingleRelationship( + instanceGroups, + relationshipName, + options[relationshipName] || {}, + ); + } + + console.log(`✅ Eager loading completed for ${relationships.length} relationships`); + } + + private async eagerLoadSingleRelationship( + instanceGroups: Map<string, BaseModel[]>, + relationshipName: string, + options: RelationshipLoadOptions, + ): Promise<void> { + for (const [modelName, instances] of instanceGroups) { + if (instances.length === 0) continue; + + const firstInstance = instances[0]; + const modelClass = firstInstance.constructor as typeof BaseModel; + const relationConfig = modelClass.relationships?.get(relationshipName); + + if (!relationConfig) { + console.warn(`Relationship '${relationshipName}' not found on ${modelName}`); + continue; + } + + console.log( + `🔗 Eager loading ${relationConfig.type} for ${instances.length} ${modelName} instances`, + ); + + switch (relationConfig.type) { + case 'belongsTo': + await this.eagerLoadBelongsTo(instances, relationshipName, relationConfig, options); + break; + case 'hasMany': + await this.eagerLoadHasMany(instances, relationshipName, relationConfig, options); + break; + case 'hasOne': + await this.eagerLoadHasOne(instances, relationshipName, relationConfig, options); + break; + case 'manyToMany': + await this.eagerLoadManyToMany(instances, relationshipName, relationConfig, options); + break; + } + } + } + + private async eagerLoadBelongsTo( + instances: BaseModel[], + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<void> { + // Get all foreign key values + const foreignKeys = instances + .map((instance) => (instance as any)[config.foreignKey]) + .filter((key) => key != null); + + if (foreignKeys.length === 0) { + // Set null for all instances + instances.forEach((instance) => { + instance._loadedRelations.set(relationshipName, null); + }); + return; + } + + // Remove duplicates + const uniqueForeignKeys = [...new Set(foreignKeys)]; + + // Load all related models at once + let query = (config.model as any).whereIn('id', uniqueForeignKeys); + + if (options.constraints) { + query = options.constraints(query); + } + + const relatedModels = await query.exec(); + + // Create lookup map + const relatedMap = new Map(); + relatedModels.forEach((model: any) => relatedMap.set(model.id, model)); + + // Assign to instances and cache + instances.forEach((instance) => { + const foreignKeyValue = (instance as any)[config.foreignKey]; + const related = relatedMap.get(foreignKeyValue) || null; + instance.setRelation(relationshipName, related); + + // Cache individual relationship + if (options.useCache !== false) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const modelType = related?.constructor?.name || 'null'; + this.cache.set(cacheKey, related, modelType, config.type); + } + }); + } + + private async eagerLoadHasMany( + instances: BaseModel[], + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<void> { + if (config.through) { + return await this.eagerLoadManyToMany(instances, relationshipName, config, options); + } + + // Get all local key values + const localKeys = instances + .map((instance) => (instance as any)[config.localKey || 'id']) + .filter((key) => key != null); + + if (localKeys.length === 0) { + instances.forEach((instance) => { + instance.setRelation(relationshipName, []); + }); + return; + } + + // Load all related models + let query = (config.model as any).whereIn(config.foreignKey, localKeys); + + if (options.constraints) { + query = options.constraints(query); + } + + if (options.orderBy) { + query = query.orderBy(options.orderBy.field, options.orderBy.direction); + } + + const relatedModels = await query.exec(); + + // Group by foreign key + const relatedGroups = new Map<string, BaseModel[]>(); + relatedModels.forEach((model: any) => { + const foreignKeyValue = model[config.foreignKey]; + if (!relatedGroups.has(foreignKeyValue)) { + relatedGroups.set(foreignKeyValue, []); + } + relatedGroups.get(foreignKeyValue)!.push(model); + }); + + // Apply limit per instance if specified + if (options.limit) { + relatedGroups.forEach((group) => { + if (group.length > options.limit!) { + group.splice(options.limit!); + } + }); + } + + // Assign to instances and cache + instances.forEach((instance) => { + const localKeyValue = (instance as any)[config.localKey || 'id']; + const related = relatedGroups.get(localKeyValue) || []; + instance.setRelation(relationshipName, related); + + // Cache individual relationship + if (options.useCache !== false) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const modelType = related[0]?.constructor?.name || 'array'; + this.cache.set(cacheKey, related, modelType, config.type); + } + }); + } + + private async eagerLoadHasOne( + instances: BaseModel[], + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<void> { + // Load as hasMany but take only the first result for each instance + await this.eagerLoadHasMany(instances, relationshipName, config, { + ...options, + limit: 1, + }); + + // Convert arrays to single items + instances.forEach((instance) => { + const relatedArray = instance._loadedRelations.get(relationshipName) || []; + const relatedItem = relatedArray[0] || null; + instance._loadedRelations.set(relationshipName, relatedItem); + }); + } + + private async eagerLoadManyToMany( + instances: BaseModel[], + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions, + ): Promise<void> { + if (!config.through) { + throw new Error('Many-to-many relationships require a through model'); + } + + // Get all local key values + const localKeys = instances + .map((instance) => (instance as any)[config.localKey || 'id']) + .filter((key) => key != null); + + if (localKeys.length === 0) { + instances.forEach((instance) => { + instance.setRelation(relationshipName, []); + }); + return; + } + + // Step 1: Get all junction records + const junctionRecords = await (config.through as any) + .whereIn(config.localKey || 'id', localKeys) + .exec(); + + if (junctionRecords.length === 0) { + instances.forEach((instance) => { + instance.setRelation(relationshipName, []); + }); + return; + } + + // Step 2: Group junction records by local key + const junctionGroups = new Map<string, any[]>(); + junctionRecords.forEach((record: any) => { + const localKeyValue = (record as any)[config.localKey || 'id']; + if (!junctionGroups.has(localKeyValue)) { + junctionGroups.set(localKeyValue, []); + } + junctionGroups.get(localKeyValue)!.push(record); + }); + + // Step 3: Get all foreign keys + const allForeignKeys = junctionRecords.map((record: any) => (record as any)[config.foreignKey]); + const uniqueForeignKeys = [...new Set(allForeignKeys)]; + + // Step 4: Load all related models + let relatedQuery = (config.model as any).whereIn('id', uniqueForeignKeys); + + if (options.constraints) { + relatedQuery = options.constraints(relatedQuery); + } + + if (options.orderBy) { + relatedQuery = relatedQuery.orderBy(options.orderBy.field, options.orderBy.direction); + } + + const relatedModels = await relatedQuery.exec(); + + // Create lookup map for related models + const relatedMap = new Map(); + relatedModels.forEach((model: any) => relatedMap.set(model.id, model)); + + // Step 5: Assign to instances + instances.forEach((instance) => { + const localKeyValue = (instance as any)[config.localKey || 'id']; + const junctionRecordsForInstance = junctionGroups.get(localKeyValue) || []; + + const relatedForInstance = junctionRecordsForInstance + .map((junction) => { + const foreignKeyValue = (junction as any)[config.foreignKey]; + return relatedMap.get(foreignKeyValue); + }) + .filter((related) => related != null); + + // Apply limit if specified + const finalRelated = options.limit + ? relatedForInstance.slice(0, options.limit) + : relatedForInstance; + + instance.setRelation(relationshipName, finalRelated); + + // Cache individual relationship + if (options.useCache !== false) { + const cacheKey = this.cache.generateKey(instance, relationshipName, options.constraints); + const modelType = finalRelated[0]?.constructor?.name || 'array'; + this.cache.set(cacheKey, finalRelated, modelType, config.type); + } + }); + } + + private groupInstancesByModel(instances: BaseModel[]): Map<string, BaseModel[]> { + const groups = new Map<string, BaseModel[]>(); + + instances.forEach((instance) => { + const modelName = instance.constructor.name; + if (!groups.has(modelName)) { + groups.set(modelName, []); + } + groups.get(modelName)!.push(instance); + }); + + return groups; + } + + // Cache management methods + invalidateRelationshipCache(instance: BaseModel, relationshipName?: string): number { + if (relationshipName) { + const key = this.cache.generateKey(instance, relationshipName); + return this.cache.invalidate(key) ? 1 : 0; + } else { + return this.cache.invalidateByInstance(instance); + } + } + + invalidateModelCache(modelName: string): number { + return this.cache.invalidateByModel(modelName); + } + + getRelationshipCacheStats(): any { + return { + cache: this.cache.getStats(), + performance: this.cache.analyzePerformance(), + }; + } + + // Preload relationships for better performance + async warmupRelationshipCache(instances: BaseModel[], relationships: string[]): Promise<void> { + await this.cache.warmup(instances, relationships, (instance, relationshipName) => + this.loadRelationship(instance, relationshipName, { useCache: false }), + ); + } + + // Cleanup and maintenance + cleanupExpiredCache(): number { + return this.cache.cleanup(); + } + + clearRelationshipCache(): void { + this.cache.clear(); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
LazyLoader.ts | +
+
+ |
+ 0% | +0/169 | +0% | +0/113 | +0% | +0/37 | +0% | +0/166 | +
RelationshipCache.ts | +
+
+ |
+ 0% | +0/140 | +0% | +0/57 | +0% | +0/28 | +0% | +0/133 | +
RelationshipManager.ts | +
+
+ |
+ 0% | +0/223 | +0% | +0/145 | +0% | +0/44 | +0% | +0/217 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { StoreType } from '../types/framework'; + +export interface OrbitDBInstance { + openDB(name: string, type: string): Promise<any>; + getOrbitDB(): any; + init(): Promise<any>; + stop?(): Promise<void>; +} + +export interface IPFSInstance { + init(): Promise<any>; + getHelia(): any; + getLibp2pInstance(): any; + stop?(): Promise<void>; + pubsub?: { + publish(topic: string, data: string): Promise<void>; + subscribe(topic: string, handler: (message: any) => void): Promise<void>; + unsubscribe(topic: string): Promise<void>; + }; +} + +export class FrameworkOrbitDBService { + private orbitDBService: OrbitDBInstance; + + constructor(orbitDBService: OrbitDBInstance) { + this.orbitDBService = orbitDBService; + } + + async openDatabase(name: string, type: StoreType): Promise<any> { + return await this.orbitDBService.openDB(name, type); + } + + async init(): Promise<void> { + await this.orbitDBService.init(); + } + + async stop(): Promise<void> { + if (this.orbitDBService.stop) { + await this.orbitDBService.stop(); + } + } + + getOrbitDB(): any { + return this.orbitDBService.getOrbitDB(); + } +} + +export class FrameworkIPFSService { + private ipfsService: IPFSInstance; + + constructor(ipfsService: IPFSInstance) { + this.ipfsService = ipfsService; + } + + async init(): Promise<void> { + await this.ipfsService.init(); + } + + async stop(): Promise<void> { + if (this.ipfsService.stop) { + await this.ipfsService.stop(); + } + } + + getHelia(): any { + return this.ipfsService.getHelia(); + } + + getLibp2p(): any { + return this.ipfsService.getLibp2pInstance(); + } + + async getConnectedPeers(): Promise<Map<string, any>> { + const libp2p = this.getLibp2p(); + if (!libp2p) { + return new Map(); + } + + const peers = libp2p.getPeers(); + const peerMap = new Map(); + + for (const peerId of peers) { + peerMap.set(peerId.toString(), peerId); + } + + return peerMap; + } + + async pinOnNode(nodeId: string, cid: string): Promise<void> { + // Implementation depends on your specific pinning setup + // This is a placeholder for the pinning functionality + console.log(`Pinning ${cid} on node ${nodeId}`); + } + + get pubsub() { + return this.ipfsService.pubsub; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
OrbitDBService.ts | +
+
+ |
+ 0% | +0/22 | +0% | +0/6 | +0% | +0/13 | +0% | +0/22 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { ShardingConfig, StoreType } from '../types/framework'; +import { FrameworkOrbitDBService } from '../services/OrbitDBService'; + +export interface ShardInfo { + name: string; + index: number; + database: any; + address: string; +} + +export class ShardManager { + private orbitDBService?: FrameworkOrbitDBService; + private shards: Map<string, ShardInfo[]> = new Map(); + private shardConfigs: Map<string, ShardingConfig> = new Map(); + + setOrbitDBService(service: FrameworkOrbitDBService): void { + this.orbitDBService = service; + } + + async createShards( + modelName: string, + config: ShardingConfig, + dbType: StoreType = 'docstore', + ): Promise<void> { + if (!this.orbitDBService) { + throw new Error('OrbitDB service not initialized'); + } + + console.log(`🔀 Creating ${config.count} shards for model: ${modelName}`); + + const shards: ShardInfo[] = []; + this.shardConfigs.set(modelName, config); + + for (let i = 0; i < config.count; i++) { + const shardName = `${modelName.toLowerCase()}-shard-${i}`; + + try { + const shard = await this.createShard(shardName, i, dbType); + shards.push(shard); + + console.log(`✓ Created shard: ${shardName} (${shard.address})`); + } catch (error) { + console.error(`❌ Failed to create shard ${shardName}:`, error); + throw error; + } + } + + this.shards.set(modelName, shards); + console.log(`✅ Created ${shards.length} shards for ${modelName}`); + } + + getShardForKey(modelName: string, key: string): ShardInfo { + const shards = this.shards.get(modelName); + if (!shards || shards.length === 0) { + throw new Error(`No shards found for model ${modelName}`); + } + + const config = this.shardConfigs.get(modelName); + if (!config) { + throw new Error(`No shard configuration found for model ${modelName}`); + } + + const shardIndex = this.calculateShardIndex(key, shards.length, config.strategy); + return shards[shardIndex]; + } + + getAllShards(modelName: string): ShardInfo[] { + return this.shards.get(modelName) || []; + } + + getShardByIndex(modelName: string, index: number): ShardInfo | undefined { + const shards = this.shards.get(modelName); + if (!shards || index < 0 || index >= shards.length) { + return undefined; + } + return shards[index]; + } + + getShardCount(modelName: string): number { + const shards = this.shards.get(modelName); + return shards ? shards.length : 0; + } + + private calculateShardIndex( + key: string, + shardCount: number, + strategy: ShardingConfig['strategy'], + ): number { + switch (strategy) { + case 'hash': + return this.hashSharding(key, shardCount); + + case 'range': + return this.rangeSharding(key, shardCount); + + case 'user': + return this.userSharding(key, shardCount); + + default: + throw new Error(`Unsupported sharding strategy: ${strategy}`); + } + } + + private hashSharding(key: string, shardCount: number): number { + // Consistent hash-based sharding + let hash = 0; + for (let i = 0; i < key.length; i++) { + hash = ((hash << 5) - hash + key.charCodeAt(i)) & 0xffffffff; + } + return Math.abs(hash) % shardCount; + } + + private rangeSharding(key: string, shardCount: number): number { + // Range-based sharding (alphabetical) + const firstChar = key.charAt(0).toLowerCase(); + const charCode = firstChar.charCodeAt(0); + + // Map a-z (97-122) to shard indices + const normalizedCode = Math.max(97, Math.min(122, charCode)); + const range = (normalizedCode - 97) / 25; // 0-1 range + + return Math.floor(range * shardCount); + } + + private userSharding(key: string, shardCount: number): number { + // User-based sharding - similar to hash but optimized for user IDs + return this.hashSharding(key, shardCount); + } + + private async createShard( + shardName: string, + index: number, + dbType: StoreType, + ): Promise<ShardInfo> { + if (!this.orbitDBService) { + throw new Error('OrbitDB service not initialized'); + } + + const database = await this.orbitDBService.openDatabase(shardName, dbType); + + return { + name: shardName, + index, + database, + address: database.address.toString(), + }; + } + + // Global indexing support + async createGlobalIndex(modelName: string, indexName: string): Promise<void> { + if (!this.orbitDBService) { + throw new Error('OrbitDB service not initialized'); + } + + console.log(`📇 Creating global index: ${indexName} for model: ${modelName}`); + + // Create sharded global index + const INDEX_SHARD_COUNT = 4; // Configurable + const indexShards: ShardInfo[] = []; + + for (let i = 0; i < INDEX_SHARD_COUNT; i++) { + const indexShardName = `${indexName}-shard-${i}`; + + try { + const shard = await this.createShard(indexShardName, i, 'keyvalue'); + indexShards.push(shard); + + console.log(`✓ Created index shard: ${indexShardName}`); + } catch (error) { + console.error(`❌ Failed to create index shard ${indexShardName}:`, error); + throw error; + } + } + + // Store index shards + this.shards.set(indexName, indexShards); + + console.log(`✅ Created global index ${indexName} with ${indexShards.length} shards`); + } + + async addToGlobalIndex(indexName: string, key: string, value: any): Promise<void> { + const indexShards = this.shards.get(indexName); + if (!indexShards) { + throw new Error(`Global index ${indexName} not found`); + } + + // Determine which shard to use for this key + const shardIndex = this.hashSharding(key, indexShards.length); + const shard = indexShards[shardIndex]; + + try { + // For keyvalue stores, we use set + await shard.database.set(key, value); + } catch (error) { + console.error(`Failed to add to global index ${indexName}:`, error); + throw error; + } + } + + async getFromGlobalIndex(indexName: string, key: string): Promise<any> { + const indexShards = this.shards.get(indexName); + if (!indexShards) { + throw new Error(`Global index ${indexName} not found`); + } + + // Determine which shard contains this key + const shardIndex = this.hashSharding(key, indexShards.length); + const shard = indexShards[shardIndex]; + + try { + return await shard.database.get(key); + } catch (error) { + console.error(`Failed to get from global index ${indexName}:`, error); + return null; + } + } + + async removeFromGlobalIndex(indexName: string, key: string): Promise<void> { + const indexShards = this.shards.get(indexName); + if (!indexShards) { + throw new Error(`Global index ${indexName} not found`); + } + + // Determine which shard contains this key + const shardIndex = this.hashSharding(key, indexShards.length); + const shard = indexShards[shardIndex]; + + try { + await shard.database.del(key); + } catch (error) { + console.error(`Failed to remove from global index ${indexName}:`, error); + throw error; + } + } + + // Query all shards for a model + async queryAllShards( + modelName: string, + queryFn: (database: any) => Promise<any[]>, + ): Promise<any[]> { + const shards = this.shards.get(modelName); + if (!shards) { + throw new Error(`No shards found for model ${modelName}`); + } + + const results: any[] = []; + + // Query all shards in parallel + const promises = shards.map(async (shard) => { + try { + return await queryFn(shard.database); + } catch (error) { + console.warn(`Query failed on shard ${shard.name}:`, error); + return []; + } + }); + + const shardResults = await Promise.all(promises); + + // Flatten results + for (const shardResult of shardResults) { + results.push(...shardResult); + } + + return results; + } + + // Statistics and monitoring + getShardStatistics(modelName: string): any { + const shards = this.shards.get(modelName); + if (!shards) { + return null; + } + + return { + modelName, + shardCount: shards.length, + shards: shards.map((shard) => ({ + name: shard.name, + index: shard.index, + address: shard.address, + })), + }; + } + + getAllModelsWithShards(): string[] { + return Array.from(this.shards.keys()); + } + + // Cleanup + async stop(): Promise<void> { + console.log('🛑 Stopping ShardManager...'); + + this.shards.clear(); + this.shardConfigs.clear(); + + console.log('✅ ShardManager stopped'); + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
ShardManager.ts | +
+
+ |
+ 0% | +0/120 | +0% | +0/36 | +0% | +0/21 | +0% | +0/117 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
models.ts | +
+
+ |
+ 0% | +0/3 | +100% | +0/0 | +0% | +0/1 | +0% | +0/3 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { BaseModel } from '../models/BaseModel'; +import { StoreType, ShardingConfig, PinningConfig, PubSubConfig } from './framework'; + +export interface ModelConfig { + type?: StoreType; + scope?: 'user' | 'global'; + sharding?: ShardingConfig; + pinning?: PinningConfig; + pubsub?: PubSubConfig; + tableName?: string; +} + +export interface FieldConfig { + type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'date'; + required?: boolean; + unique?: boolean; + index?: boolean | 'global'; + default?: any; + validate?: (value: any) => boolean | string; + transform?: (value: any) => any; +} + +export interface RelationshipConfig { + type: 'belongsTo' | 'hasMany' | 'hasOne' | 'manyToMany'; + model: typeof BaseModel; + foreignKey: string; + localKey?: string; + through?: typeof BaseModel; + lazy?: boolean; +} + +export interface UserMappings { + userId: string; + databases: Record<string, string>; +} + +export class ValidationError extends Error { + public errors: string[]; + + constructor(errors: string[]) { + super(`Validation failed: ${errors.join(', ')}`); + this.errors = errors; + this.name = 'ValidationError'; + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
framework | +
+
+ |
+ 0% | +0/249 | +0% | +0/129 | +0% | +0/49 | +0% | +0/247 | +
framework/core | +
+
+ |
+ 0% | +0/235 | +0% | +0/110 | +0% | +0/48 | +0% | +0/230 | +
framework/migrations | +
+
+ |
+ 0% | +0/435 | +0% | +0/199 | +0% | +0/89 | +0% | +0/417 | +
framework/models | +
+
+ |
+ 0% | +0/200 | +0% | +0/97 | +0% | +0/44 | +0% | +0/199 | +
framework/models/decorators | +
+
+ |
+ 0% | +0/113 | +0% | +0/93 | +0% | +0/33 | +0% | +0/113 | +
framework/pinning | +
+
+ |
+ 0% | +0/227 | +0% | +0/132 | +0% | +0/44 | +0% | +0/218 | +
framework/pubsub | +
+
+ |
+ 0% | +0/228 | +0% | +0/110 | +0% | +0/37 | +0% | +0/220 | +
framework/query | +
+
+ |
+ 0% | +0/672 | +0% | +0/301 | +0% | +0/162 | +0% | +0/646 | +
framework/relationships | +
+
+ |
+ 0% | +0/532 | +0% | +0/315 | +0% | +0/109 | +0% | +0/516 | +
framework/services | +
+
+ |
+ 0% | +0/22 | +0% | +0/6 | +0% | +0/13 | +0% | +0/22 | +
framework/sharding | +
+
+ |
+ 0% | +0/120 | +0% | +0/36 | +0% | +0/21 | +0% | +0/117 | +
framework/types | +
+
+ |
+ 0% | +0/3 | +100% | +0/0 | +0% | +0/1 | +0% | +0/3 | +