From f58fa0caf7013c0a18c0924e3559954d51033eb4 Mon Sep 17 00:00:00 2001 From: anonpenguin Date: Thu, 19 Jun 2025 11:20:26 +0300 Subject: [PATCH] Add coverage prettify styles and scripts, sorting functionality, and search feature - Added prettify.css for code highlighting in coverage reports. - Introduced prettify.js for syntax highlighting functionality. - Included sort-arrow-sprite.png for sorting indicators in the coverage summary. - Implemented sorter.js to enable sorting of coverage summary table columns. - Added a search box to filter coverage summary rows based on user input. --- coverage/base.css | 224 + coverage/block-navigation.js | 87 + coverage/favicon.png | Bin 0 -> 445 bytes coverage/framework/DebrosFramework.ts.html | 2386 +++++++ coverage/framework/core/ConfigManager.ts.html | 676 ++ .../framework/core/DatabaseManager.ts.html | 1189 ++++ coverage/framework/core/ModelRegistry.ts.html | 397 ++ coverage/framework/core/index.html | 146 + coverage/framework/index.html | 116 + .../migrations/MigrationBuilder.ts.html | 1465 ++++ .../migrations/MigrationManager.ts.html | 3001 +++++++++ coverage/framework/migrations/index.html | 131 + coverage/framework/models/BaseModel.ts.html | 1672 +++++ .../framework/models/decorators/Field.ts.html | 442 ++ .../framework/models/decorators/Model.ts.html | 250 + .../framework/models/decorators/hooks.ts.html | 277 + .../framework/models/decorators/index.html | 161 + .../models/decorators/relationships.ts.html | 586 ++ coverage/framework/models/index.html | 116 + .../framework/pinning/PinningManager.ts.html | 1879 ++++++ coverage/framework/pinning/index.html | 116 + .../framework/pubsub/PubSubManager.ts.html | 2221 ++++++ coverage/framework/pubsub/index.html | 116 + coverage/framework/query/QueryBuilder.ts.html | 1426 ++++ coverage/framework/query/QueryCache.ts.html | 1030 +++ .../framework/query/QueryExecutor.ts.html | 1942 ++++++ .../framework/query/QueryOptimizer.ts.html | 847 +++ coverage/framework/query/index.html | 161 + .../relationships/LazyLoader.ts.html | 1408 ++++ .../relationships/RelationshipCache.ts.html | 1126 ++++ .../relationships/RelationshipManager.ts.html | 1792 +++++ coverage/framework/relationships/index.html | 146 + .../framework/services/OrbitDBService.ts.html | 379 ++ coverage/framework/services/index.html | 116 + .../framework/sharding/ShardManager.ts.html | 982 +++ coverage/framework/sharding/index.html | 116 + coverage/framework/types/index.html | 116 + coverage/framework/types/models.ts.html | 220 + coverage/index.html | 281 + coverage/lcov-report/base.css | 224 + coverage/lcov-report/block-navigation.js | 87 + coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes .../framework/DebrosFramework.ts.html | 2386 +++++++ .../framework/core/ConfigManager.ts.html | 676 ++ .../framework/core/DatabaseManager.ts.html | 1189 ++++ .../framework/core/ModelRegistry.ts.html | 397 ++ .../lcov-report/framework/core/index.html | 146 + coverage/lcov-report/framework/index.html | 116 + .../migrations/MigrationBuilder.ts.html | 1465 ++++ .../migrations/MigrationManager.ts.html | 3001 +++++++++ .../framework/migrations/index.html | 131 + .../framework/models/BaseModel.ts.html | 1672 +++++ .../framework/models/decorators/Field.ts.html | 442 ++ .../framework/models/decorators/Model.ts.html | 250 + .../framework/models/decorators/hooks.ts.html | 277 + .../framework/models/decorators/index.html | 161 + .../models/decorators/relationships.ts.html | 586 ++ .../lcov-report/framework/models/index.html | 116 + .../framework/pinning/PinningManager.ts.html | 1879 ++++++ .../lcov-report/framework/pinning/index.html | 116 + .../framework/pubsub/PubSubManager.ts.html | 2221 ++++++ .../lcov-report/framework/pubsub/index.html | 116 + .../framework/query/QueryBuilder.ts.html | 1426 ++++ .../framework/query/QueryCache.ts.html | 1030 +++ .../framework/query/QueryExecutor.ts.html | 1942 ++++++ .../framework/query/QueryOptimizer.ts.html | 847 +++ .../lcov-report/framework/query/index.html | 161 + .../relationships/LazyLoader.ts.html | 1408 ++++ .../relationships/RelationshipCache.ts.html | 1126 ++++ .../relationships/RelationshipManager.ts.html | 1792 +++++ .../framework/relationships/index.html | 146 + .../framework/services/OrbitDBService.ts.html | 379 ++ .../lcov-report/framework/services/index.html | 116 + .../framework/sharding/ShardManager.ts.html | 982 +++ .../lcov-report/framework/sharding/index.html | 116 + .../lcov-report/framework/types/index.html | 116 + .../framework/types/models.ts.html | 220 + coverage/lcov-report/index.html | 281 + coverage/lcov-report/prettify.css | 1 + coverage/lcov-report/prettify.js | 2 + coverage/lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/lcov-report/sorter.js | 196 + coverage/lcov.info | 5983 +++++++++++++++++ coverage/prettify.css | 1 + coverage/prettify.js | 2 + coverage/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/sorter.js | 196 + 87 files changed, 65865 insertions(+) create mode 100644 coverage/base.css create mode 100644 coverage/block-navigation.js create mode 100644 coverage/favicon.png create mode 100644 coverage/framework/DebrosFramework.ts.html create mode 100644 coverage/framework/core/ConfigManager.ts.html create mode 100644 coverage/framework/core/DatabaseManager.ts.html create mode 100644 coverage/framework/core/ModelRegistry.ts.html create mode 100644 coverage/framework/core/index.html create mode 100644 coverage/framework/index.html create mode 100644 coverage/framework/migrations/MigrationBuilder.ts.html create mode 100644 coverage/framework/migrations/MigrationManager.ts.html create mode 100644 coverage/framework/migrations/index.html create mode 100644 coverage/framework/models/BaseModel.ts.html create mode 100644 coverage/framework/models/decorators/Field.ts.html create mode 100644 coverage/framework/models/decorators/Model.ts.html create mode 100644 coverage/framework/models/decorators/hooks.ts.html create mode 100644 coverage/framework/models/decorators/index.html create mode 100644 coverage/framework/models/decorators/relationships.ts.html create mode 100644 coverage/framework/models/index.html create mode 100644 coverage/framework/pinning/PinningManager.ts.html create mode 100644 coverage/framework/pinning/index.html create mode 100644 coverage/framework/pubsub/PubSubManager.ts.html create mode 100644 coverage/framework/pubsub/index.html create mode 100644 coverage/framework/query/QueryBuilder.ts.html create mode 100644 coverage/framework/query/QueryCache.ts.html create mode 100644 coverage/framework/query/QueryExecutor.ts.html create mode 100644 coverage/framework/query/QueryOptimizer.ts.html create mode 100644 coverage/framework/query/index.html create mode 100644 coverage/framework/relationships/LazyLoader.ts.html create mode 100644 coverage/framework/relationships/RelationshipCache.ts.html create mode 100644 coverage/framework/relationships/RelationshipManager.ts.html create mode 100644 coverage/framework/relationships/index.html create mode 100644 coverage/framework/services/OrbitDBService.ts.html create mode 100644 coverage/framework/services/index.html create mode 100644 coverage/framework/sharding/ShardManager.ts.html create mode 100644 coverage/framework/sharding/index.html create mode 100644 coverage/framework/types/index.html create mode 100644 coverage/framework/types/models.ts.html create mode 100644 coverage/index.html create mode 100644 coverage/lcov-report/base.css create mode 100644 coverage/lcov-report/block-navigation.js create mode 100644 coverage/lcov-report/favicon.png create mode 100644 coverage/lcov-report/framework/DebrosFramework.ts.html create mode 100644 coverage/lcov-report/framework/core/ConfigManager.ts.html create mode 100644 coverage/lcov-report/framework/core/DatabaseManager.ts.html create mode 100644 coverage/lcov-report/framework/core/ModelRegistry.ts.html create mode 100644 coverage/lcov-report/framework/core/index.html create mode 100644 coverage/lcov-report/framework/index.html create mode 100644 coverage/lcov-report/framework/migrations/MigrationBuilder.ts.html create mode 100644 coverage/lcov-report/framework/migrations/MigrationManager.ts.html create mode 100644 coverage/lcov-report/framework/migrations/index.html create mode 100644 coverage/lcov-report/framework/models/BaseModel.ts.html create mode 100644 coverage/lcov-report/framework/models/decorators/Field.ts.html create mode 100644 coverage/lcov-report/framework/models/decorators/Model.ts.html create mode 100644 coverage/lcov-report/framework/models/decorators/hooks.ts.html create mode 100644 coverage/lcov-report/framework/models/decorators/index.html create mode 100644 coverage/lcov-report/framework/models/decorators/relationships.ts.html create mode 100644 coverage/lcov-report/framework/models/index.html create mode 100644 coverage/lcov-report/framework/pinning/PinningManager.ts.html create mode 100644 coverage/lcov-report/framework/pinning/index.html create mode 100644 coverage/lcov-report/framework/pubsub/PubSubManager.ts.html create mode 100644 coverage/lcov-report/framework/pubsub/index.html create mode 100644 coverage/lcov-report/framework/query/QueryBuilder.ts.html create mode 100644 coverage/lcov-report/framework/query/QueryCache.ts.html create mode 100644 coverage/lcov-report/framework/query/QueryExecutor.ts.html create mode 100644 coverage/lcov-report/framework/query/QueryOptimizer.ts.html create mode 100644 coverage/lcov-report/framework/query/index.html create mode 100644 coverage/lcov-report/framework/relationships/LazyLoader.ts.html create mode 100644 coverage/lcov-report/framework/relationships/RelationshipCache.ts.html create mode 100644 coverage/lcov-report/framework/relationships/RelationshipManager.ts.html create mode 100644 coverage/lcov-report/framework/relationships/index.html create mode 100644 coverage/lcov-report/framework/services/OrbitDBService.ts.html create mode 100644 coverage/lcov-report/framework/services/index.html create mode 100644 coverage/lcov-report/framework/sharding/ShardManager.ts.html create mode 100644 coverage/lcov-report/framework/sharding/index.html create mode 100644 coverage/lcov-report/framework/types/index.html create mode 100644 coverage/lcov-report/framework/types/models.ts.html create mode 100644 coverage/lcov-report/index.html create mode 100644 coverage/lcov-report/prettify.css create mode 100644 coverage/lcov-report/prettify.js create mode 100644 coverage/lcov-report/sort-arrow-sprite.png create mode 100644 coverage/lcov-report/sorter.js create mode 100644 coverage/lcov.info create mode 100644 coverage/prettify.css create mode 100644 coverage/prettify.js create mode 100644 coverage/sort-arrow-sprite.png create mode 100644 coverage/sorter.js 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 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for framework/DebrosFramework.ts + + + + + + + + + +
+
+

All files / framework DebrosFramework.ts

+
+ +
+ 0% + Statements + 0/249 +
+ + +
+ 0% + Branches + 0/129 +
+ + +
+ 0% + Functions + 0/49 +
+ + +
+ 0% + Lines + 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 +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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/core/ConfigManager.ts.html b/coverage/framework/core/ConfigManager.ts.html new file mode 100644 index 0000000..cb05107 --- /dev/null +++ b/coverage/framework/core/ConfigManager.ts.html @@ -0,0 +1,676 @@ + + + + + + Code coverage report for framework/core/ConfigManager.ts + + + + + + + + + +
+
+

All files / framework/core ConfigManager.ts

+
+ +
+ 0% + Statements + 0/29 +
+ + +
+ 0% + Branches + 0/35 +
+ + +
+ 0% + Functions + 0/14 +
+ + +
+ 0% + Lines + 0/29 +
+ + +
+

+ 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
+    };
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/core/DatabaseManager.ts.html b/coverage/framework/core/DatabaseManager.ts.html new file mode 100644 index 0000000..1396ee0 --- /dev/null +++ b/coverage/framework/core/DatabaseManager.ts.html @@ -0,0 +1,1189 @@ + + + + + + Code coverage report for framework/core/DatabaseManager.ts + + + + + + + + + +
+
+

All files / framework/core DatabaseManager.ts

+
+ +
+ 0% + Statements + 0/168 +
+ + +
+ 0% + Branches + 0/40 +
+ + +
+ 0% + Functions + 0/20 +
+ + +
+ 0% + Lines + 0/165 +
+ + +
+

+ 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');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/core/ModelRegistry.ts.html b/coverage/framework/core/ModelRegistry.ts.html new file mode 100644 index 0000000..e8fd798 --- /dev/null +++ b/coverage/framework/core/ModelRegistry.ts.html @@ -0,0 +1,397 @@ + + + + + + Code coverage report for framework/core/ModelRegistry.ts + + + + + + + + + +
+
+

All files / framework/core ModelRegistry.ts

+
+ +
+ 0% + Statements + 0/38 +
+ + +
+ 0% + Branches + 0/35 +
+ + +
+ 0% + Functions + 0/14 +
+ + +
+ 0% + Lines + 0/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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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');
+    }
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/core/index.html b/coverage/framework/core/index.html new file mode 100644 index 0000000..87fcfa5 --- /dev/null +++ b/coverage/framework/core/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for framework/core + + + + + + + + + +
+
+

All files framework/core

+
+ +
+ 0% + Statements + 0/235 +
+ + +
+ 0% + Branches + 0/110 +
+ + +
+ 0% + Functions + 0/48 +
+ + +
+ 0% + Lines + 0/230 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
ConfigManager.ts +
+
0%0/290%0/350%0/140%0/29
DatabaseManager.ts +
+
0%0/1680%0/400%0/200%0/165
ModelRegistry.ts +
+
0%0/380%0/350%0/140%0/36
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/index.html b/coverage/framework/index.html new file mode 100644 index 0000000..e4ce419 --- /dev/null +++ b/coverage/framework/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework + + + + + + + + + +
+
+

All files framework

+
+ +
+ 0% + Statements + 0/249 +
+ + +
+ 0% + Branches + 0/129 +
+ + +
+ 0% + Functions + 0/49 +
+ + +
+ 0% + Lines + 0/247 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
DebrosFramework.ts +
+
0%0/2490%0/1290%0/490%0/247
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/migrations/MigrationBuilder.ts.html b/coverage/framework/migrations/MigrationBuilder.ts.html new file mode 100644 index 0000000..2351dd4 --- /dev/null +++ b/coverage/framework/migrations/MigrationBuilder.ts.html @@ -0,0 +1,1465 @@ + + + + + + Code coverage report for framework/migrations/MigrationBuilder.ts + + + + + + + + + +
+
+

All files / framework/migrations MigrationBuilder.ts

+
+ +
+ 0% + Statements + 0/103 +
+ + +
+ 0% + Branches + 0/34 +
+ + +
+ 0% + Functions + 0/38 +
+ + +
+ 0% + Lines + 0/102 +
+ + +
+

+ 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);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/migrations/MigrationManager.ts.html b/coverage/framework/migrations/MigrationManager.ts.html new file mode 100644 index 0000000..7125ec2 --- /dev/null +++ b/coverage/framework/migrations/MigrationManager.ts.html @@ -0,0 +1,3001 @@ + + + + + + Code coverage report for framework/migrations/MigrationManager.ts + + + + + + + + + +
+
+

All files / framework/migrations MigrationManager.ts

+
+ +
+ 0% + Statements + 0/332 +
+ + +
+ 0% + Branches + 0/165 +
+ + +
+ 0% + Functions + 0/51 +
+ + +
+ 0% + Lines + 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 +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();
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/migrations/index.html b/coverage/framework/migrations/index.html new file mode 100644 index 0000000..a25f82d --- /dev/null +++ b/coverage/framework/migrations/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for framework/migrations + + + + + + + + + +
+
+

All files framework/migrations

+
+ +
+ 0% + Statements + 0/435 +
+ + +
+ 0% + Branches + 0/199 +
+ + +
+ 0% + Functions + 0/89 +
+ + +
+ 0% + Lines + 0/417 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
MigrationBuilder.ts +
+
0%0/1030%0/340%0/380%0/102
MigrationManager.ts +
+
0%0/3320%0/1650%0/510%0/315
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/models/BaseModel.ts.html b/coverage/framework/models/BaseModel.ts.html new file mode 100644 index 0000000..54bf34b --- /dev/null +++ b/coverage/framework/models/BaseModel.ts.html @@ -0,0 +1,1672 @@ + + + + + + Code coverage report for framework/models/BaseModel.ts + + + + + + + + + +
+
+

All files / framework/models BaseModel.ts

+
+ +
+ 0% + Statements + 0/200 +
+ + +
+ 0% + Branches + 0/97 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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 || [];
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/models/decorators/Field.ts.html b/coverage/framework/models/decorators/Field.ts.html new file mode 100644 index 0000000..dd21046 --- /dev/null +++ b/coverage/framework/models/decorators/Field.ts.html @@ -0,0 +1,442 @@ + + + + + + Code coverage report for framework/models/decorators/Field.ts + + + + + + + + + +
+
+

All files / framework/models/decorators Field.ts

+
+ +
+ 0% + Statements + 0/43 +
+ + +
+ 0% + Branches + 0/44 +
+ + +
+ 0% + Functions + 0/7 +
+ + +
+ 0% + Lines + 0/43 +
+ + +
+

+ 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/models/decorators/Model.ts.html b/coverage/framework/models/decorators/Model.ts.html new file mode 100644 index 0000000..a6afdd2 --- /dev/null +++ b/coverage/framework/models/decorators/Model.ts.html @@ -0,0 +1,250 @@ + + + + + + Code coverage report for framework/models/decorators/Model.ts + + + + + + + + + +
+
+

All files / framework/models/decorators Model.ts

+
+ +
+ 0% + Statements + 0/20 +
+ + +
+ 0% + Branches + 0/17 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/20 +
+ + +
+

+ 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/models/decorators/hooks.ts.html b/coverage/framework/models/decorators/hooks.ts.html new file mode 100644 index 0000000..d598848 --- /dev/null +++ b/coverage/framework/models/decorators/hooks.ts.html @@ -0,0 +1,277 @@ + + + + + + Code coverage report for framework/models/decorators/hooks.ts + + + + + + + + + +
+
+

All files / framework/models/decorators hooks.ts

+
+ +
+ 0% + Statements + 0/17 +
+ + +
+ 0% + Branches + 0/8 +
+ + +
+ 0% + Functions + 0/10 +
+ + +
+ 0% + Lines + 0/17 +
+ + +
+

+ 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/models/decorators/index.html b/coverage/framework/models/decorators/index.html new file mode 100644 index 0000000..959707b --- /dev/null +++ b/coverage/framework/models/decorators/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for framework/models/decorators + + + + + + + + + +
+
+

All files framework/models/decorators

+
+ +
+ 0% + Statements + 0/113 +
+ + +
+ 0% + Branches + 0/93 +
+ + +
+ 0% + Functions + 0/33 +
+ + +
+ 0% + Lines + 0/113 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
Field.ts +
+
0%0/430%0/440%0/70%0/43
Model.ts +
+
0%0/200%0/170%0/30%0/20
hooks.ts +
+
0%0/170%0/80%0/100%0/17
relationships.ts +
+
0%0/330%0/240%0/130%0/33
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/models/decorators/relationships.ts.html b/coverage/framework/models/decorators/relationships.ts.html new file mode 100644 index 0000000..0de5def --- /dev/null +++ b/coverage/framework/models/decorators/relationships.ts.html @@ -0,0 +1,586 @@ + + + + + + Code coverage report for framework/models/decorators/relationships.ts + + + + + + + + + +
+
+

All files / framework/models/decorators relationships.ts

+
+ +
+ 0% + Statements + 0/33 +
+ + +
+ 0% + Branches + 0/24 +
+ + +
+ 0% + Functions + 0/13 +
+ + +
+ 0% + Lines + 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/models/index.html b/coverage/framework/models/index.html new file mode 100644 index 0000000..1cf6da6 --- /dev/null +++ b/coverage/framework/models/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/models + + + + + + + + + +
+
+

All files framework/models

+
+ +
+ 0% + Statements + 0/200 +
+ + +
+ 0% + Branches + 0/97 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 0/199 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
BaseModel.ts +
+
0%0/2000%0/970%0/440%0/199
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/pinning/PinningManager.ts.html b/coverage/framework/pinning/PinningManager.ts.html new file mode 100644 index 0000000..a0eba2a --- /dev/null +++ b/coverage/framework/pinning/PinningManager.ts.html @@ -0,0 +1,1879 @@ + + + + + + Code coverage report for framework/pinning/PinningManager.ts + + + + + + + + + +
+
+

All files / framework/pinning PinningManager.ts

+
+ +
+ 0% + Statements + 0/227 +
+ + +
+ 0% + Branches + 0/132 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * 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');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/pinning/index.html b/coverage/framework/pinning/index.html new file mode 100644 index 0000000..7bf112f --- /dev/null +++ b/coverage/framework/pinning/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/pinning + + + + + + + + + +
+
+

All files framework/pinning

+
+ +
+ 0% + Statements + 0/227 +
+ + +
+ 0% + Branches + 0/132 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 0/218 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
PinningManager.ts +
+
0%0/2270%0/1320%0/440%0/218
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/pubsub/PubSubManager.ts.html b/coverage/framework/pubsub/PubSubManager.ts.html new file mode 100644 index 0000000..94cb988 --- /dev/null +++ b/coverage/framework/pubsub/PubSubManager.ts.html @@ -0,0 +1,2221 @@ + + + + + + Code coverage report for framework/pubsub/PubSubManager.ts + + + + + + + + + +
+
+

All files / framework/pubsub PubSubManager.ts

+
+ +
+ 0% + Statements + 0/228 +
+ + +
+ 0% + Branches + 0/110 +
+ + +
+ 0% + Functions + 0/37 +
+ + +
+ 0% + Lines + 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 +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');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/pubsub/index.html b/coverage/framework/pubsub/index.html new file mode 100644 index 0000000..05497ae --- /dev/null +++ b/coverage/framework/pubsub/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/pubsub + + + + + + + + + +
+
+

All files framework/pubsub

+
+ +
+ 0% + Statements + 0/228 +
+ + +
+ 0% + Branches + 0/110 +
+ + +
+ 0% + Functions + 0/37 +
+ + +
+ 0% + Lines + 0/220 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
PubSubManager.ts +
+
0%0/2280%0/1100%0/370%0/220
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/query/QueryBuilder.ts.html b/coverage/framework/query/QueryBuilder.ts.html new file mode 100644 index 0000000..e9ca422 --- /dev/null +++ b/coverage/framework/query/QueryBuilder.ts.html @@ -0,0 +1,1426 @@ + + + + + + Code coverage report for framework/query/QueryBuilder.ts + + + + + + + + + +
+
+

All files / framework/query QueryBuilder.ts

+
+ +
+ 0% + Statements + 0/142 +
+ + +
+ 0% + Branches + 0/22 +
+ + +
+ 0% + Functions + 0/69 +
+ + +
+ 0% + Lines + 0/141 +
+ + +
+

+ 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(),
+    };
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/query/QueryCache.ts.html b/coverage/framework/query/QueryCache.ts.html new file mode 100644 index 0000000..6aef14e --- /dev/null +++ b/coverage/framework/query/QueryCache.ts.html @@ -0,0 +1,1030 @@ + + + + + + Code coverage report for framework/query/QueryCache.ts + + + + + + + + + +
+
+

All files / framework/query QueryCache.ts

+
+ +
+ 0% + Statements + 0/130 +
+ + +
+ 0% + Branches + 0/35 +
+ + +
+ 0% + Functions + 0/29 +
+ + +
+ 0% + Lines + 0/123 +
+ + +
+

+ 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);
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/query/QueryExecutor.ts.html b/coverage/framework/query/QueryExecutor.ts.html new file mode 100644 index 0000000..760c5af --- /dev/null +++ b/coverage/framework/query/QueryExecutor.ts.html @@ -0,0 +1,1942 @@ + + + + + + Code coverage report for framework/query/QueryExecutor.ts + + + + + + + + + +
+
+

All files / framework/query QueryExecutor.ts

+
+ +
+ 0% + Statements + 0/270 +
+ + +
+ 0% + Branches + 0/171 +
+ + +
+ 0% + Functions + 0/46 +
+ + +
+ 0% + Lines + 0/256 +
+ + +
+

+ 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;
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/query/QueryOptimizer.ts.html b/coverage/framework/query/QueryOptimizer.ts.html new file mode 100644 index 0000000..5fba623 --- /dev/null +++ b/coverage/framework/query/QueryOptimizer.ts.html @@ -0,0 +1,847 @@ + + + + + + Code coverage report for framework/query/QueryOptimizer.ts + + + + + + + + + +
+
+

All files / framework/query QueryOptimizer.ts

+
+ +
+ 0% + Statements + 0/130 +
+ + +
+ 0% + Branches + 0/73 +
+ + +
+ 0% + Functions + 0/18 +
+ + +
+ 0% + Lines + 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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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;
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/query/index.html b/coverage/framework/query/index.html new file mode 100644 index 0000000..88ce7e4 --- /dev/null +++ b/coverage/framework/query/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for framework/query + + + + + + + + + +
+
+

All files framework/query

+
+ +
+ 0% + Statements + 0/672 +
+ + +
+ 0% + Branches + 0/301 +
+ + +
+ 0% + Functions + 0/162 +
+ + +
+ 0% + Lines + 0/646 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
QueryBuilder.ts +
+
0%0/1420%0/220%0/690%0/141
QueryCache.ts +
+
0%0/1300%0/350%0/290%0/123
QueryExecutor.ts +
+
0%0/2700%0/1710%0/460%0/256
QueryOptimizer.ts +
+
0%0/1300%0/730%0/180%0/126
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/relationships/LazyLoader.ts.html b/coverage/framework/relationships/LazyLoader.ts.html new file mode 100644 index 0000000..48a15f0 --- /dev/null +++ b/coverage/framework/relationships/LazyLoader.ts.html @@ -0,0 +1,1408 @@ + + + + + + Code coverage report for framework/relationships/LazyLoader.ts + + + + + + + + + +
+
+

All files / framework/relationships LazyLoader.ts

+
+ +
+ 0% + Statements + 0/169 +
+ + +
+ 0% + Branches + 0/113 +
+ + +
+ 0% + Functions + 0/37 +
+ + +
+ 0% + Lines + 0/166 +
+ + +
+

+ 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;
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/relationships/RelationshipCache.ts.html b/coverage/framework/relationships/RelationshipCache.ts.html new file mode 100644 index 0000000..ee93df1 --- /dev/null +++ b/coverage/framework/relationships/RelationshipCache.ts.html @@ -0,0 +1,1126 @@ + + + + + + Code coverage report for framework/relationships/RelationshipCache.ts + + + + + + + + + +
+
+

All files / framework/relationships RelationshipCache.ts

+
+ +
+ 0% + Statements + 0/140 +
+ + +
+ 0% + Branches + 0/57 +
+ + +
+ 0% + Functions + 0/28 +
+ + +
+ 0% + Lines + 0/133 +
+ + +
+

+ 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);
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/relationships/RelationshipManager.ts.html b/coverage/framework/relationships/RelationshipManager.ts.html new file mode 100644 index 0000000..61d85f4 --- /dev/null +++ b/coverage/framework/relationships/RelationshipManager.ts.html @@ -0,0 +1,1792 @@ + + + + + + Code coverage report for framework/relationships/RelationshipManager.ts + + + + + + + + + +
+
+

All files / framework/relationships RelationshipManager.ts

+
+ +
+ 0% + Statements + 0/223 +
+ + +
+ 0% + Branches + 0/145 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 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 +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();
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/relationships/index.html b/coverage/framework/relationships/index.html new file mode 100644 index 0000000..0aaed3f --- /dev/null +++ b/coverage/framework/relationships/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for framework/relationships + + + + + + + + + +
+
+

All files framework/relationships

+
+ +
+ 0% + Statements + 0/532 +
+ + +
+ 0% + Branches + 0/315 +
+ + +
+ 0% + Functions + 0/109 +
+ + +
+ 0% + Lines + 0/516 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
LazyLoader.ts +
+
0%0/1690%0/1130%0/370%0/166
RelationshipCache.ts +
+
0%0/1400%0/570%0/280%0/133
RelationshipManager.ts +
+
0%0/2230%0/1450%0/440%0/217
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/services/OrbitDBService.ts.html b/coverage/framework/services/OrbitDBService.ts.html new file mode 100644 index 0000000..30081ec --- /dev/null +++ b/coverage/framework/services/OrbitDBService.ts.html @@ -0,0 +1,379 @@ + + + + + + Code coverage report for framework/services/OrbitDBService.ts + + + + + + + + + +
+
+

All files / framework/services OrbitDBService.ts

+
+ +
+ 0% + Statements + 0/22 +
+ + +
+ 0% + Branches + 0/6 +
+ + +
+ 0% + Functions + 0/13 +
+ + +
+ 0% + Lines + 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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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;
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/services/index.html b/coverage/framework/services/index.html new file mode 100644 index 0000000..ab289f8 --- /dev/null +++ b/coverage/framework/services/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/services + + + + + + + + + +
+
+

All files framework/services

+
+ +
+ 0% + Statements + 0/22 +
+ + +
+ 0% + Branches + 0/6 +
+ + +
+ 0% + Functions + 0/13 +
+ + +
+ 0% + Lines + 0/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
OrbitDBService.ts +
+
0%0/220%0/60%0/130%0/22
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/sharding/ShardManager.ts.html b/coverage/framework/sharding/ShardManager.ts.html new file mode 100644 index 0000000..d508036 --- /dev/null +++ b/coverage/framework/sharding/ShardManager.ts.html @@ -0,0 +1,982 @@ + + + + + + Code coverage report for framework/sharding/ShardManager.ts + + + + + + + + + +
+
+

All files / framework/sharding ShardManager.ts

+
+ +
+ 0% + Statements + 0/120 +
+ + +
+ 0% + Branches + 0/36 +
+ + +
+ 0% + Functions + 0/21 +
+ + +
+ 0% + Lines + 0/117 +
+ + +
+

+ 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');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/sharding/index.html b/coverage/framework/sharding/index.html new file mode 100644 index 0000000..e3cac33 --- /dev/null +++ b/coverage/framework/sharding/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/sharding + + + + + + + + + +
+
+

All files framework/sharding

+
+ +
+ 0% + Statements + 0/120 +
+ + +
+ 0% + Branches + 0/36 +
+ + +
+ 0% + Functions + 0/21 +
+ + +
+ 0% + Lines + 0/117 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
ShardManager.ts +
+
0%0/1200%0/360%0/210%0/117
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/types/index.html b/coverage/framework/types/index.html new file mode 100644 index 0000000..c097098 --- /dev/null +++ b/coverage/framework/types/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/types + + + + + + + + + +
+
+

All files framework/types

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
models.ts +
+
0%0/3100%0/00%0/10%0/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/framework/types/models.ts.html b/coverage/framework/types/models.ts.html new file mode 100644 index 0000000..c4329be --- /dev/null +++ b/coverage/framework/types/models.ts.html @@ -0,0 +1,220 @@ + + + + + + Code coverage report for framework/types/models.ts + + + + + + + + + +
+
+

All files / framework/types models.ts

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 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';
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/index.html b/coverage/index.html new file mode 100644 index 0000000..e016970 --- /dev/null +++ b/coverage/index.html @@ -0,0 +1,281 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 0% + Statements + 0/3036 +
+ + +
+ 0% + Branches + 0/1528 +
+ + +
+ 0% + Functions + 0/650 +
+ + +
+ 0% + Lines + 0/2948 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
framework +
+
0%0/2490%0/1290%0/490%0/247
framework/core +
+
0%0/2350%0/1100%0/480%0/230
framework/migrations +
+
0%0/4350%0/1990%0/890%0/417
framework/models +
+
0%0/2000%0/970%0/440%0/199
framework/models/decorators +
+
0%0/1130%0/930%0/330%0/113
framework/pinning +
+
0%0/2270%0/1320%0/440%0/218
framework/pubsub +
+
0%0/2280%0/1100%0/370%0/220
framework/query +
+
0%0/6720%0/3010%0/1620%0/646
framework/relationships +
+
0%0/5320%0/3150%0/1090%0/516
framework/services +
+
0%0/220%0/60%0/130%0/22
framework/sharding +
+
0%0/1200%0/360%0/210%0/117
framework/types +
+
0%0/3100%0/00%0/10%0/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/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/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/coverage/lcov-report/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/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for framework/DebrosFramework.ts + + + + + + + + + +
+
+

All files / framework DebrosFramework.ts

+
+ +
+ 0% + Statements + 0/249 +
+ + +
+ 0% + Branches + 0/129 +
+ + +
+ 0% + Functions + 0/49 +
+ + +
+ 0% + Lines + 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 +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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/core/ConfigManager.ts.html b/coverage/lcov-report/framework/core/ConfigManager.ts.html new file mode 100644 index 0000000..0358db0 --- /dev/null +++ b/coverage/lcov-report/framework/core/ConfigManager.ts.html @@ -0,0 +1,676 @@ + + + + + + Code coverage report for framework/core/ConfigManager.ts + + + + + + + + + +
+
+

All files / framework/core ConfigManager.ts

+
+ +
+ 0% + Statements + 0/29 +
+ + +
+ 0% + Branches + 0/35 +
+ + +
+ 0% + Functions + 0/14 +
+ + +
+ 0% + Lines + 0/29 +
+ + +
+

+ 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
+    };
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/core/DatabaseManager.ts.html b/coverage/lcov-report/framework/core/DatabaseManager.ts.html new file mode 100644 index 0000000..de1c3ad --- /dev/null +++ b/coverage/lcov-report/framework/core/DatabaseManager.ts.html @@ -0,0 +1,1189 @@ + + + + + + Code coverage report for framework/core/DatabaseManager.ts + + + + + + + + + +
+
+

All files / framework/core DatabaseManager.ts

+
+ +
+ 0% + Statements + 0/168 +
+ + +
+ 0% + Branches + 0/40 +
+ + +
+ 0% + Functions + 0/20 +
+ + +
+ 0% + Lines + 0/165 +
+ + +
+

+ 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');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/core/ModelRegistry.ts.html b/coverage/lcov-report/framework/core/ModelRegistry.ts.html new file mode 100644 index 0000000..7dc22c2 --- /dev/null +++ b/coverage/lcov-report/framework/core/ModelRegistry.ts.html @@ -0,0 +1,397 @@ + + + + + + Code coverage report for framework/core/ModelRegistry.ts + + + + + + + + + +
+
+

All files / framework/core ModelRegistry.ts

+
+ +
+ 0% + Statements + 0/38 +
+ + +
+ 0% + Branches + 0/35 +
+ + +
+ 0% + Functions + 0/14 +
+ + +
+ 0% + Lines + 0/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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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');
+    }
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/core/index.html b/coverage/lcov-report/framework/core/index.html new file mode 100644 index 0000000..1fd2b30 --- /dev/null +++ b/coverage/lcov-report/framework/core/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for framework/core + + + + + + + + + +
+
+

All files framework/core

+
+ +
+ 0% + Statements + 0/235 +
+ + +
+ 0% + Branches + 0/110 +
+ + +
+ 0% + Functions + 0/48 +
+ + +
+ 0% + Lines + 0/230 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
ConfigManager.ts +
+
0%0/290%0/350%0/140%0/29
DatabaseManager.ts +
+
0%0/1680%0/400%0/200%0/165
ModelRegistry.ts +
+
0%0/380%0/350%0/140%0/36
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/index.html b/coverage/lcov-report/framework/index.html new file mode 100644 index 0000000..0ffb514 --- /dev/null +++ b/coverage/lcov-report/framework/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework + + + + + + + + + +
+
+

All files framework

+
+ +
+ 0% + Statements + 0/249 +
+ + +
+ 0% + Branches + 0/129 +
+ + +
+ 0% + Functions + 0/49 +
+ + +
+ 0% + Lines + 0/247 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
DebrosFramework.ts +
+
0%0/2490%0/1290%0/490%0/247
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/migrations/MigrationBuilder.ts.html b/coverage/lcov-report/framework/migrations/MigrationBuilder.ts.html new file mode 100644 index 0000000..58c0c5e --- /dev/null +++ b/coverage/lcov-report/framework/migrations/MigrationBuilder.ts.html @@ -0,0 +1,1465 @@ + + + + + + Code coverage report for framework/migrations/MigrationBuilder.ts + + + + + + + + + +
+
+

All files / framework/migrations MigrationBuilder.ts

+
+ +
+ 0% + Statements + 0/103 +
+ + +
+ 0% + Branches + 0/34 +
+ + +
+ 0% + Functions + 0/38 +
+ + +
+ 0% + Lines + 0/102 +
+ + +
+

+ 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);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/migrations/MigrationManager.ts.html b/coverage/lcov-report/framework/migrations/MigrationManager.ts.html new file mode 100644 index 0000000..4e86536 --- /dev/null +++ b/coverage/lcov-report/framework/migrations/MigrationManager.ts.html @@ -0,0 +1,3001 @@ + + + + + + Code coverage report for framework/migrations/MigrationManager.ts + + + + + + + + + +
+
+

All files / framework/migrations MigrationManager.ts

+
+ +
+ 0% + Statements + 0/332 +
+ + +
+ 0% + Branches + 0/165 +
+ + +
+ 0% + Functions + 0/51 +
+ + +
+ 0% + Lines + 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 +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();
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/migrations/index.html b/coverage/lcov-report/framework/migrations/index.html new file mode 100644 index 0000000..19fee75 --- /dev/null +++ b/coverage/lcov-report/framework/migrations/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for framework/migrations + + + + + + + + + +
+
+

All files framework/migrations

+
+ +
+ 0% + Statements + 0/435 +
+ + +
+ 0% + Branches + 0/199 +
+ + +
+ 0% + Functions + 0/89 +
+ + +
+ 0% + Lines + 0/417 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
MigrationBuilder.ts +
+
0%0/1030%0/340%0/380%0/102
MigrationManager.ts +
+
0%0/3320%0/1650%0/510%0/315
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/models/BaseModel.ts.html b/coverage/lcov-report/framework/models/BaseModel.ts.html new file mode 100644 index 0000000..7889a5d --- /dev/null +++ b/coverage/lcov-report/framework/models/BaseModel.ts.html @@ -0,0 +1,1672 @@ + + + + + + Code coverage report for framework/models/BaseModel.ts + + + + + + + + + +
+
+

All files / framework/models BaseModel.ts

+
+ +
+ 0% + Statements + 0/200 +
+ + +
+ 0% + Branches + 0/97 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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 || [];
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/models/decorators/Field.ts.html b/coverage/lcov-report/framework/models/decorators/Field.ts.html new file mode 100644 index 0000000..6c93d9b --- /dev/null +++ b/coverage/lcov-report/framework/models/decorators/Field.ts.html @@ -0,0 +1,442 @@ + + + + + + Code coverage report for framework/models/decorators/Field.ts + + + + + + + + + +
+
+

All files / framework/models/decorators Field.ts

+
+ +
+ 0% + Statements + 0/43 +
+ + +
+ 0% + Branches + 0/44 +
+ + +
+ 0% + Functions + 0/7 +
+ + +
+ 0% + Lines + 0/43 +
+ + +
+

+ 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/models/decorators/Model.ts.html b/coverage/lcov-report/framework/models/decorators/Model.ts.html new file mode 100644 index 0000000..1220bc2 --- /dev/null +++ b/coverage/lcov-report/framework/models/decorators/Model.ts.html @@ -0,0 +1,250 @@ + + + + + + Code coverage report for framework/models/decorators/Model.ts + + + + + + + + + +
+
+

All files / framework/models/decorators Model.ts

+
+ +
+ 0% + Statements + 0/20 +
+ + +
+ 0% + Branches + 0/17 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/20 +
+ + +
+

+ 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/models/decorators/hooks.ts.html b/coverage/lcov-report/framework/models/decorators/hooks.ts.html new file mode 100644 index 0000000..85b6f7d --- /dev/null +++ b/coverage/lcov-report/framework/models/decorators/hooks.ts.html @@ -0,0 +1,277 @@ + + + + + + Code coverage report for framework/models/decorators/hooks.ts + + + + + + + + + +
+
+

All files / framework/models/decorators hooks.ts

+
+ +
+ 0% + Statements + 0/17 +
+ + +
+ 0% + Branches + 0/8 +
+ + +
+ 0% + Functions + 0/10 +
+ + +
+ 0% + Lines + 0/17 +
+ + +
+

+ 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/models/decorators/index.html b/coverage/lcov-report/framework/models/decorators/index.html new file mode 100644 index 0000000..f65d195 --- /dev/null +++ b/coverage/lcov-report/framework/models/decorators/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for framework/models/decorators + + + + + + + + + +
+
+

All files framework/models/decorators

+
+ +
+ 0% + Statements + 0/113 +
+ + +
+ 0% + Branches + 0/93 +
+ + +
+ 0% + Functions + 0/33 +
+ + +
+ 0% + Lines + 0/113 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
Field.ts +
+
0%0/430%0/440%0/70%0/43
Model.ts +
+
0%0/200%0/170%0/30%0/20
hooks.ts +
+
0%0/170%0/80%0/100%0/17
relationships.ts +
+
0%0/330%0/240%0/130%0/33
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/models/decorators/relationships.ts.html b/coverage/lcov-report/framework/models/decorators/relationships.ts.html new file mode 100644 index 0000000..c208025 --- /dev/null +++ b/coverage/lcov-report/framework/models/decorators/relationships.ts.html @@ -0,0 +1,586 @@ + + + + + + Code coverage report for framework/models/decorators/relationships.ts + + + + + + + + + +
+
+

All files / framework/models/decorators relationships.ts

+
+ +
+ 0% + Statements + 0/33 +
+ + +
+ 0% + Branches + 0/24 +
+ + +
+ 0% + Functions + 0/13 +
+ + +
+ 0% + Lines + 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/models/index.html b/coverage/lcov-report/framework/models/index.html new file mode 100644 index 0000000..6a686f6 --- /dev/null +++ b/coverage/lcov-report/framework/models/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/models + + + + + + + + + +
+
+

All files framework/models

+
+ +
+ 0% + Statements + 0/200 +
+ + +
+ 0% + Branches + 0/97 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 0/199 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
BaseModel.ts +
+
0%0/2000%0/970%0/440%0/199
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/pinning/PinningManager.ts.html b/coverage/lcov-report/framework/pinning/PinningManager.ts.html new file mode 100644 index 0000000..36a2e62 --- /dev/null +++ b/coverage/lcov-report/framework/pinning/PinningManager.ts.html @@ -0,0 +1,1879 @@ + + + + + + Code coverage report for framework/pinning/PinningManager.ts + + + + + + + + + +
+
+

All files / framework/pinning PinningManager.ts

+
+ +
+ 0% + Statements + 0/227 +
+ + +
+ 0% + Branches + 0/132 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * 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');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/pinning/index.html b/coverage/lcov-report/framework/pinning/index.html new file mode 100644 index 0000000..cb7bbe0 --- /dev/null +++ b/coverage/lcov-report/framework/pinning/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/pinning + + + + + + + + + +
+
+

All files framework/pinning

+
+ +
+ 0% + Statements + 0/227 +
+ + +
+ 0% + Branches + 0/132 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 0/218 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
PinningManager.ts +
+
0%0/2270%0/1320%0/440%0/218
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/pubsub/PubSubManager.ts.html b/coverage/lcov-report/framework/pubsub/PubSubManager.ts.html new file mode 100644 index 0000000..c3ac4ba --- /dev/null +++ b/coverage/lcov-report/framework/pubsub/PubSubManager.ts.html @@ -0,0 +1,2221 @@ + + + + + + Code coverage report for framework/pubsub/PubSubManager.ts + + + + + + + + + +
+
+

All files / framework/pubsub PubSubManager.ts

+
+ +
+ 0% + Statements + 0/228 +
+ + +
+ 0% + Branches + 0/110 +
+ + +
+ 0% + Functions + 0/37 +
+ + +
+ 0% + Lines + 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 +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');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/pubsub/index.html b/coverage/lcov-report/framework/pubsub/index.html new file mode 100644 index 0000000..49c6174 --- /dev/null +++ b/coverage/lcov-report/framework/pubsub/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/pubsub + + + + + + + + + +
+
+

All files framework/pubsub

+
+ +
+ 0% + Statements + 0/228 +
+ + +
+ 0% + Branches + 0/110 +
+ + +
+ 0% + Functions + 0/37 +
+ + +
+ 0% + Lines + 0/220 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
PubSubManager.ts +
+
0%0/2280%0/1100%0/370%0/220
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/query/QueryBuilder.ts.html b/coverage/lcov-report/framework/query/QueryBuilder.ts.html new file mode 100644 index 0000000..3ec547a --- /dev/null +++ b/coverage/lcov-report/framework/query/QueryBuilder.ts.html @@ -0,0 +1,1426 @@ + + + + + + Code coverage report for framework/query/QueryBuilder.ts + + + + + + + + + +
+
+

All files / framework/query QueryBuilder.ts

+
+ +
+ 0% + Statements + 0/142 +
+ + +
+ 0% + Branches + 0/22 +
+ + +
+ 0% + Functions + 0/69 +
+ + +
+ 0% + Lines + 0/141 +
+ + +
+

+ 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(),
+    };
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/query/QueryCache.ts.html b/coverage/lcov-report/framework/query/QueryCache.ts.html new file mode 100644 index 0000000..35b542a --- /dev/null +++ b/coverage/lcov-report/framework/query/QueryCache.ts.html @@ -0,0 +1,1030 @@ + + + + + + Code coverage report for framework/query/QueryCache.ts + + + + + + + + + +
+
+

All files / framework/query QueryCache.ts

+
+ +
+ 0% + Statements + 0/130 +
+ + +
+ 0% + Branches + 0/35 +
+ + +
+ 0% + Functions + 0/29 +
+ + +
+ 0% + Lines + 0/123 +
+ + +
+

+ 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);
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/query/QueryExecutor.ts.html b/coverage/lcov-report/framework/query/QueryExecutor.ts.html new file mode 100644 index 0000000..3430c4f --- /dev/null +++ b/coverage/lcov-report/framework/query/QueryExecutor.ts.html @@ -0,0 +1,1942 @@ + + + + + + Code coverage report for framework/query/QueryExecutor.ts + + + + + + + + + +
+
+

All files / framework/query QueryExecutor.ts

+
+ +
+ 0% + Statements + 0/270 +
+ + +
+ 0% + Branches + 0/171 +
+ + +
+ 0% + Functions + 0/46 +
+ + +
+ 0% + Lines + 0/256 +
+ + +
+

+ 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;
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/query/QueryOptimizer.ts.html b/coverage/lcov-report/framework/query/QueryOptimizer.ts.html new file mode 100644 index 0000000..700dda6 --- /dev/null +++ b/coverage/lcov-report/framework/query/QueryOptimizer.ts.html @@ -0,0 +1,847 @@ + + + + + + Code coverage report for framework/query/QueryOptimizer.ts + + + + + + + + + +
+
+

All files / framework/query QueryOptimizer.ts

+
+ +
+ 0% + Statements + 0/130 +
+ + +
+ 0% + Branches + 0/73 +
+ + +
+ 0% + Functions + 0/18 +
+ + +
+ 0% + Lines + 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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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;
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/query/index.html b/coverage/lcov-report/framework/query/index.html new file mode 100644 index 0000000..cbb0ca6 --- /dev/null +++ b/coverage/lcov-report/framework/query/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for framework/query + + + + + + + + + +
+
+

All files framework/query

+
+ +
+ 0% + Statements + 0/672 +
+ + +
+ 0% + Branches + 0/301 +
+ + +
+ 0% + Functions + 0/162 +
+ + +
+ 0% + Lines + 0/646 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
QueryBuilder.ts +
+
0%0/1420%0/220%0/690%0/141
QueryCache.ts +
+
0%0/1300%0/350%0/290%0/123
QueryExecutor.ts +
+
0%0/2700%0/1710%0/460%0/256
QueryOptimizer.ts +
+
0%0/1300%0/730%0/180%0/126
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/relationships/LazyLoader.ts.html b/coverage/lcov-report/framework/relationships/LazyLoader.ts.html new file mode 100644 index 0000000..66f055e --- /dev/null +++ b/coverage/lcov-report/framework/relationships/LazyLoader.ts.html @@ -0,0 +1,1408 @@ + + + + + + Code coverage report for framework/relationships/LazyLoader.ts + + + + + + + + + +
+
+

All files / framework/relationships LazyLoader.ts

+
+ +
+ 0% + Statements + 0/169 +
+ + +
+ 0% + Branches + 0/113 +
+ + +
+ 0% + Functions + 0/37 +
+ + +
+ 0% + Lines + 0/166 +
+ + +
+

+ 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;
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/relationships/RelationshipCache.ts.html b/coverage/lcov-report/framework/relationships/RelationshipCache.ts.html new file mode 100644 index 0000000..0a197ee --- /dev/null +++ b/coverage/lcov-report/framework/relationships/RelationshipCache.ts.html @@ -0,0 +1,1126 @@ + + + + + + Code coverage report for framework/relationships/RelationshipCache.ts + + + + + + + + + +
+
+

All files / framework/relationships RelationshipCache.ts

+
+ +
+ 0% + Statements + 0/140 +
+ + +
+ 0% + Branches + 0/57 +
+ + +
+ 0% + Functions + 0/28 +
+ + +
+ 0% + Lines + 0/133 +
+ + +
+

+ 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);
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/relationships/RelationshipManager.ts.html b/coverage/lcov-report/framework/relationships/RelationshipManager.ts.html new file mode 100644 index 0000000..100e4ce --- /dev/null +++ b/coverage/lcov-report/framework/relationships/RelationshipManager.ts.html @@ -0,0 +1,1792 @@ + + + + + + Code coverage report for framework/relationships/RelationshipManager.ts + + + + + + + + + +
+
+

All files / framework/relationships RelationshipManager.ts

+
+ +
+ 0% + Statements + 0/223 +
+ + +
+ 0% + Branches + 0/145 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 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 +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();
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/relationships/index.html b/coverage/lcov-report/framework/relationships/index.html new file mode 100644 index 0000000..a84b0ab --- /dev/null +++ b/coverage/lcov-report/framework/relationships/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for framework/relationships + + + + + + + + + +
+
+

All files framework/relationships

+
+ +
+ 0% + Statements + 0/532 +
+ + +
+ 0% + Branches + 0/315 +
+ + +
+ 0% + Functions + 0/109 +
+ + +
+ 0% + Lines + 0/516 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
LazyLoader.ts +
+
0%0/1690%0/1130%0/370%0/166
RelationshipCache.ts +
+
0%0/1400%0/570%0/280%0/133
RelationshipManager.ts +
+
0%0/2230%0/1450%0/440%0/217
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/services/OrbitDBService.ts.html b/coverage/lcov-report/framework/services/OrbitDBService.ts.html new file mode 100644 index 0000000..e468c0a --- /dev/null +++ b/coverage/lcov-report/framework/services/OrbitDBService.ts.html @@ -0,0 +1,379 @@ + + + + + + Code coverage report for framework/services/OrbitDBService.ts + + + + + + + + + +
+
+

All files / framework/services OrbitDBService.ts

+
+ +
+ 0% + Statements + 0/22 +
+ + +
+ 0% + Branches + 0/6 +
+ + +
+ 0% + Functions + 0/13 +
+ + +
+ 0% + Lines + 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  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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;
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/services/index.html b/coverage/lcov-report/framework/services/index.html new file mode 100644 index 0000000..ca67c81 --- /dev/null +++ b/coverage/lcov-report/framework/services/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/services + + + + + + + + + +
+
+

All files framework/services

+
+ +
+ 0% + Statements + 0/22 +
+ + +
+ 0% + Branches + 0/6 +
+ + +
+ 0% + Functions + 0/13 +
+ + +
+ 0% + Lines + 0/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
OrbitDBService.ts +
+
0%0/220%0/60%0/130%0/22
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/sharding/ShardManager.ts.html b/coverage/lcov-report/framework/sharding/ShardManager.ts.html new file mode 100644 index 0000000..80c44af --- /dev/null +++ b/coverage/lcov-report/framework/sharding/ShardManager.ts.html @@ -0,0 +1,982 @@ + + + + + + Code coverage report for framework/sharding/ShardManager.ts + + + + + + + + + +
+
+

All files / framework/sharding ShardManager.ts

+
+ +
+ 0% + Statements + 0/120 +
+ + +
+ 0% + Branches + 0/36 +
+ + +
+ 0% + Functions + 0/21 +
+ + +
+ 0% + Lines + 0/117 +
+ + +
+

+ 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');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/sharding/index.html b/coverage/lcov-report/framework/sharding/index.html new file mode 100644 index 0000000..9b3b4e2 --- /dev/null +++ b/coverage/lcov-report/framework/sharding/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/sharding + + + + + + + + + +
+
+

All files framework/sharding

+
+ +
+ 0% + Statements + 0/120 +
+ + +
+ 0% + Branches + 0/36 +
+ + +
+ 0% + Functions + 0/21 +
+ + +
+ 0% + Lines + 0/117 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
ShardManager.ts +
+
0%0/1200%0/360%0/210%0/117
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/types/index.html b/coverage/lcov-report/framework/types/index.html new file mode 100644 index 0000000..c28e66d --- /dev/null +++ b/coverage/lcov-report/framework/types/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for framework/types + + + + + + + + + +
+
+

All files framework/types

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
models.ts +
+
0%0/3100%0/00%0/10%0/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/framework/types/models.ts.html b/coverage/lcov-report/framework/types/models.ts.html new file mode 100644 index 0000000..e3ea24c --- /dev/null +++ b/coverage/lcov-report/framework/types/models.ts.html @@ -0,0 +1,220 @@ + + + + + + Code coverage report for framework/types/models.ts + + + + + + + + + +
+
+

All files / framework/types models.ts

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 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';
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html new file mode 100644 index 0000000..f49b25e --- /dev/null +++ b/coverage/lcov-report/index.html @@ -0,0 +1,281 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 0% + Statements + 0/3036 +
+ + +
+ 0% + Branches + 0/1528 +
+ + +
+ 0% + Functions + 0/650 +
+ + +
+ 0% + Lines + 0/2948 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
framework +
+
0%0/2490%0/1290%0/490%0/247
framework/core +
+
0%0/2350%0/1100%0/480%0/230
framework/migrations +
+
0%0/4350%0/1990%0/890%0/417
framework/models +
+
0%0/2000%0/970%0/440%0/199
framework/models/decorators +
+
0%0/1130%0/930%0/330%0/113
framework/pinning +
+
0%0/2270%0/1320%0/440%0/218
framework/pubsub +
+
0%0/2280%0/1100%0/370%0/220
framework/query +
+
0%0/6720%0/3010%0/1620%0/646
framework/relationships +
+
0%0/5320%0/3150%0/1090%0/516
framework/services +
+
0%0/220%0/60%0/130%0/22
framework/sharding +
+
0%0/1200%0/360%0/210%0/117
framework/types +
+
0%0/3100%0/00%0/10%0/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..4aca04d --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,5983 @@ +TN: +SF:src/framework/DebrosFramework.ts +FN:128,(anonymous_0) +FN:169,(anonymous_1) +FN:222,(anonymous_2) +FN:262,(anonymous_3) +FN:300,(anonymous_4) +FN:346,(anonymous_5) +FN:361,(anonymous_6) +FN:365,(anonymous_7) +FN:371,(anonymous_8) +FN:380,(anonymous_9) +FN:395,(anonymous_10) +FN:416,(anonymous_11) +FN:424,(anonymous_12) +FN:429,(anonymous_13) +FN:438,(anonymous_14) +FN:446,(anonymous_15) +FN:455,(anonymous_16) +FN:465,(anonymous_17) +FN:473,(anonymous_18) +FN:482,(anonymous_19) +FN:488,(anonymous_20) +FN:494,(anonymous_21) +FN:512,(anonymous_22) +FN:524,(anonymous_23) +FN:534,(anonymous_24) +FN:558,(anonymous_25) +FN:562,(anonymous_26) +FN:567,(anonymous_27) +FN:572,(anonymous_28) +FN:576,(anonymous_29) +FN:580,(anonymous_30) +FN:584,(anonymous_31) +FN:588,(anonymous_32) +FN:592,(anonymous_33) +FN:597,(anonymous_34) +FN:617,(anonymous_35) +FN:633,(anonymous_36) +FN:679,(anonymous_37) +FN:704,(anonymous_38) +FN:708,(anonymous_39) +FN:713,(anonymous_40) +FN:718,(anonymous_41) +FN:721,(anonymous_42) +FN:729,(anonymous_43) +FN:737,(anonymous_44) +FN:742,(anonymous_45) +FN:743,(anonymous_46) +FN:750,(anonymous_47) +FN:755,(anonymous_48) +FNF:49 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +FNDA:0,(anonymous_44) +FNDA:0,(anonymous_45) +FNDA:0,(anonymous_46) +FNDA:0,(anonymous_47) +FNDA:0,(anonymous_48) +DA:108,0 +DA:109,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:129,0 +DA:130,0 +DA:132,0 +DA:146,0 +DA:174,0 +DA:175,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:189,0 +DA:192,0 +DA:195,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:205,0 +DA:208,0 +DA:209,0 +DA:210,0 +DA:212,0 +DA:213,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:226,0 +DA:228,0 +DA:230,0 +DA:231,0 +DA:234,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:242,0 +DA:243,0 +DA:246,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:255,0 +DA:256,0 +DA:257,0 +DA:263,0 +DA:266,0 +DA:267,0 +DA:268,0 +DA:271,0 +DA:272,0 +DA:275,0 +DA:276,0 +DA:277,0 +DA:278,0 +DA:281,0 +DA:284,0 +DA:285,0 +DA:286,0 +DA:287,0 +DA:291,0 +DA:296,0 +DA:301,0 +DA:304,0 +DA:305,0 +DA:311,0 +DA:312,0 +DA:313,0 +DA:314,0 +DA:318,0 +DA:319,0 +DA:323,0 +DA:324,0 +DA:331,0 +DA:332,0 +DA:333,0 +DA:337,0 +DA:342,0 +DA:347,0 +DA:362,0 +DA:365,0 +DA:366,0 +DA:370,0 +DA:371,0 +DA:372,0 +DA:376,0 +DA:381,0 +DA:383,0 +DA:384,0 +DA:386,0 +DA:387,0 +DA:388,0 +DA:390,0 +DA:395,0 +DA:396,0 +DA:398,0 +DA:400,0 +DA:403,0 +DA:404,0 +DA:406,0 +DA:408,0 +DA:417,0 +DA:418,0 +DA:420,0 +DA:425,0 +DA:430,0 +DA:431,0 +DA:434,0 +DA:435,0 +DA:439,0 +DA:440,0 +DA:443,0 +DA:447,0 +DA:448,0 +DA:451,0 +DA:456,0 +DA:457,0 +DA:460,0 +DA:461,0 +DA:462,0 +DA:466,0 +DA:467,0 +DA:470,0 +DA:474,0 +DA:475,0 +DA:478,0 +DA:483,0 +DA:484,0 +DA:489,0 +DA:490,0 +DA:495,0 +DA:497,0 +DA:499,0 +DA:500,0 +DA:503,0 +DA:508,0 +DA:513,0 +DA:514,0 +DA:517,0 +DA:518,0 +DA:519,0 +DA:520,0 +DA:523,0 +DA:524,0 +DA:527,0 +DA:529,0 +DA:530,0 +DA:535,0 +DA:536,0 +DA:537,0 +DA:539,0 +DA:540,0 +DA:541,0 +DA:542,0 +DA:543,0 +DA:546,0 +DA:547,0 +DA:548,0 +DA:551,0 +DA:554,0 +DA:559,0 +DA:563,0 +DA:564,0 +DA:568,0 +DA:573,0 +DA:577,0 +DA:581,0 +DA:585,0 +DA:589,0 +DA:593,0 +DA:598,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:605,0 +DA:606,0 +DA:607,0 +DA:608,0 +DA:610,0 +DA:612,0 +DA:613,0 +DA:618,0 +DA:620,0 +DA:621,0 +DA:623,0 +DA:625,0 +DA:626,0 +DA:629,0 +DA:635,0 +DA:636,0 +DA:637,0 +DA:640,0 +DA:641,0 +DA:642,0 +DA:646,0 +DA:647,0 +DA:650,0 +DA:651,0 +DA:654,0 +DA:655,0 +DA:658,0 +DA:659,0 +DA:662,0 +DA:663,0 +DA:666,0 +DA:667,0 +DA:670,0 +DA:671,0 +DA:675,0 +DA:680,0 +DA:705,0 +DA:707,0 +DA:709,0 +DA:710,0 +DA:714,0 +DA:715,0 +DA:719,0 +DA:722,0 +DA:723,0 +DA:730,0 +DA:731,0 +DA:732,0 +DA:733,0 +DA:734,0 +DA:735,0 +DA:737,0 +DA:740,0 +DA:742,0 +DA:743,0 +DA:746,0 +DA:751,0 +DA:752,0 +DA:760,0 +DA:761,0 +DA:762,0 +LF:247 +LH:0 +BRDA:128,0,0,0 +BRDA:136,1,0,0 +BRDA:136,1,1,0 +BRDA:174,2,0,0 +BRDA:174,2,1,0 +BRDA:183,3,0,0 +BRDA:183,3,1,0 +BRDA:204,4,0,0 +BRDA:204,4,1,0 +BRDA:204,5,0,0 +BRDA:204,5,1,0 +BRDA:230,6,0,0 +BRDA:230,6,1,0 +BRDA:242,7,0,0 +BRDA:242,7,1,0 +BRDA:277,8,0,0 +BRDA:277,8,1,0 +BRDA:284,9,0,0 +BRDA:284,9,1,0 +BRDA:286,10,0,0 +BRDA:286,10,1,0 +BRDA:286,11,0,0 +BRDA:286,11,1,0 +BRDA:304,12,0,0 +BRDA:304,12,1,0 +BRDA:306,13,0,0 +BRDA:306,13,1,0 +BRDA:311,14,0,0 +BRDA:311,14,1,0 +BRDA:323,15,0,0 +BRDA:323,15,1,0 +BRDA:328,16,0,0 +BRDA:328,16,1,0 +BRDA:370,17,0,0 +BRDA:370,17,1,0 +BRDA:373,18,0,0 +BRDA:373,18,1,0 +BRDA:381,19,0,0 +BRDA:381,19,1,0 +BRDA:387,20,0,0 +BRDA:387,20,1,0 +BRDA:392,21,0,0 +BRDA:392,21,1,0 +BRDA:404,22,0,0 +BRDA:404,22,1,0 +BRDA:417,23,0,0 +BRDA:417,23,1,0 +BRDA:425,24,0,0 +BRDA:425,24,1,0 +BRDA:430,25,0,0 +BRDA:430,25,1,0 +BRDA:439,26,0,0 +BRDA:439,26,1,0 +BRDA:447,27,0,0 +BRDA:447,27,1,0 +BRDA:456,28,0,0 +BRDA:456,28,1,0 +BRDA:466,29,0,0 +BRDA:466,29,1,0 +BRDA:474,30,0,0 +BRDA:474,30,1,0 +BRDA:483,31,0,0 +BRDA:483,31,1,0 +BRDA:489,32,0,0 +BRDA:489,32,1,0 +BRDA:497,33,0,0 +BRDA:497,33,1,0 +BRDA:503,34,0,0 +BRDA:503,34,1,0 +BRDA:503,35,0,0 +BRDA:503,35,1,0 +BRDA:517,36,0,0 +BRDA:517,36,1,0 +BRDA:518,37,0,0 +BRDA:518,37,1,0 +BRDA:519,38,0,0 +BRDA:519,38,1,0 +BRDA:520,39,0,0 +BRDA:520,39,1,0 +BRDA:524,40,0,0 +BRDA:524,40,1,0 +BRDA:527,41,0,0 +BRDA:527,41,1,0 +BRDA:539,42,0,0 +BRDA:539,42,1,0 +BRDA:546,43,0,0 +BRDA:546,43,1,0 +BRDA:598,44,0,0 +BRDA:598,44,1,0 +BRDA:625,45,0,0 +BRDA:625,45,1,0 +BRDA:635,46,0,0 +BRDA:635,46,1,0 +BRDA:640,47,0,0 +BRDA:640,47,1,0 +BRDA:646,48,0,0 +BRDA:646,48,1,0 +BRDA:650,49,0,0 +BRDA:650,49,1,0 +BRDA:654,50,0,0 +BRDA:654,50,1,0 +BRDA:658,51,0,0 +BRDA:658,51,1,0 +BRDA:662,52,0,0 +BRDA:662,52,1,0 +BRDA:666,53,0,0 +BRDA:666,53,1,0 +BRDA:670,54,0,0 +BRDA:670,54,1,0 +BRDA:705,55,0,0 +BRDA:705,55,1,0 +BRDA:709,56,0,0 +BRDA:709,56,1,0 +BRDA:710,57,0,0 +BRDA:710,57,1,0 +BRDA:714,58,0,0 +BRDA:714,58,1,0 +BRDA:715,59,0,0 +BRDA:715,59,1,0 +BRDA:719,60,0,0 +BRDA:719,60,1,0 +BRDA:722,61,0,0 +BRDA:722,61,1,0 +BRDA:723,62,0,0 +BRDA:723,62,1,0 +BRDA:741,63,0,0 +BRDA:741,63,1,0 +BRDA:750,64,0,0 +BRDA:758,65,0,0 +BRF:129 +BRH:0 +end_of_record +TN: +SF:src/framework/core/ConfigManager.ts +FN:37,(anonymous_0) +FN:42,(anonymous_1) +FN:61,(anonymous_2) +FN:97,(anonymous_3) +FN:101,(anonymous_4) +FN:105,(anonymous_5) +FN:109,(anonymous_6) +FN:113,(anonymous_7) +FN:117,(anonymous_8) +FN:122,(anonymous_9) +FN:131,(anonymous_10) +FN:136,(anonymous_11) +FN:157,(anonymous_12) +FN:178,(anonymous_13) +FNF:14 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +DA:17,0 +DA:38,0 +DA:39,0 +DA:43,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:67,0 +DA:68,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:80,0 +DA:81,0 +DA:85,0 +DA:87,0 +DA:91,0 +DA:98,0 +DA:102,0 +DA:106,0 +DA:110,0 +DA:114,0 +DA:118,0 +DA:123,0 +DA:127,0 +DA:132,0 +DA:137,0 +DA:158,0 +DA:179,0 +LF:29 +LH:0 +BRDA:37,0,0,0 +BRDA:52,1,0,0 +BRDA:52,1,1,0 +BRDA:63,2,0,0 +BRDA:63,2,1,0 +BRDA:64,3,0,0 +BRDA:64,3,1,0 +BRDA:64,4,0,0 +BRDA:64,4,1,0 +BRDA:67,5,0,0 +BRDA:67,5,1,0 +BRDA:67,6,0,0 +BRDA:67,6,1,0 +BRDA:73,7,0,0 +BRDA:73,7,1,0 +BRDA:74,8,0,0 +BRDA:74,8,1,0 +BRDA:74,9,0,0 +BRDA:74,9,1,0 +BRDA:80,10,0,0 +BRDA:80,10,1,0 +BRDA:81,11,0,0 +BRDA:81,11,1,0 +BRDA:82,12,0,0 +BRDA:82,12,1,0 +BRDA:87,13,0,0 +BRDA:87,13,1,0 +BRDA:88,14,0,0 +BRDA:88,14,1,0 +BRDA:110,15,0,0 +BRDA:110,15,1,0 +BRDA:114,16,0,0 +BRDA:114,16,1,0 +BRDA:118,17,0,0 +BRDA:118,17,1,0 +BRF:35 +BRH:0 +end_of_record +TN: +SF:src/framework/core/DatabaseManager.ts +FN:7,(anonymous_0) +FN:21,(anonymous_1) +FN:25,(anonymous_2) +FN:42,(anonymous_3) +FN:62,(anonymous_4) +FN:84,(anonymous_5) +FN:125,(anonymous_6) +FN:147,(anonymous_7) +FN:181,(anonymous_8) +FN:189,(anonymous_9) +FN:193,(anonymous_10) +FN:207,(anonymous_11) +FN:228,(anonymous_12) +FN:245,(anonymous_13) +FN:255,(anonymous_14) +FN:266,(anonymous_15) +FN:284,(anonymous_16) +FN:313,(anonymous_17) +FN:334,(anonymous_18) +FN:356,(anonymous_19) +FNF:20 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +DA:8,0 +DA:9,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:22,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:43,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:63,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:81,0 +DA:85,0 +DA:87,0 +DA:88,0 +DA:91,0 +DA:92,0 +DA:95,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:110,0 +DA:111,0 +DA:114,0 +DA:116,0 +DA:119,0 +DA:121,0 +DA:122,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:130,0 +DA:131,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:141,0 +DA:142,0 +DA:144,0 +DA:149,0 +DA:150,0 +DA:154,0 +DA:155,0 +DA:157,0 +DA:158,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:166,0 +DA:167,0 +DA:169,0 +DA:170,0 +DA:173,0 +DA:176,0 +DA:178,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:186,0 +DA:190,0 +DA:194,0 +DA:195,0 +DA:198,0 +DA:200,0 +DA:202,0 +DA:203,0 +DA:208,0 +DA:210,0 +DA:211,0 +DA:215,0 +DA:216,0 +DA:219,0 +DA:221,0 +DA:223,0 +DA:224,0 +DA:229,0 +DA:230,0 +DA:232,0 +DA:233,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:240,0 +DA:241,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:251,0 +DA:256,0 +DA:257,0 +DA:259,0 +DA:260,0 +DA:263,0 +DA:266,0 +DA:269,0 +DA:270,0 +DA:273,0 +DA:276,0 +DA:279,0 +DA:280,0 +DA:285,0 +DA:286,0 +DA:288,0 +DA:291,0 +DA:292,0 +DA:295,0 +DA:298,0 +DA:301,0 +DA:302,0 +DA:305,0 +DA:308,0 +DA:309,0 +DA:314,0 +DA:315,0 +DA:317,0 +DA:318,0 +DA:321,0 +DA:322,0 +DA:326,0 +DA:329,0 +DA:330,0 +DA:335,0 +DA:336,0 +DA:338,0 +DA:339,0 +DA:342,0 +DA:343,0 +DA:347,0 +DA:350,0 +DA:351,0 +DA:357,0 +DA:360,0 +DA:361,0 +DA:362,0 +DA:363,0 +DA:365,0 +DA:366,0 +LF:165 +LH:0 +BRDA:26,0,0,0 +BRDA:26,0,1,0 +BRDA:130,1,0,0 +BRDA:130,1,1,0 +BRDA:136,2,0,0 +BRDA:136,2,1,0 +BRDA:149,3,0,0 +BRDA:149,3,1,0 +BRDA:157,4,0,0 +BRDA:157,4,1,0 +BRDA:162,5,0,0 +BRDA:162,5,1,0 +BRDA:169,6,0,0 +BRDA:169,6,1,0 +BRDA:183,7,0,0 +BRDA:183,7,1,0 +BRDA:210,8,0,0 +BRDA:210,8,1,0 +BRDA:232,9,0,0 +BRDA:232,9,1,0 +BRDA:257,10,0,0 +BRDA:257,10,1,0 +BRDA:257,10,2,0 +BRDA:257,10,3,0 +BRDA:257,10,4,0 +BRDA:257,10,5,0 +BRDA:286,11,0,0 +BRDA:286,11,1,0 +BRDA:286,11,2,0 +BRDA:286,11,3,0 +BRDA:286,11,4,0 +BRDA:286,11,5,0 +BRDA:301,12,0,0 +BRDA:301,12,1,0 +BRDA:315,13,0,0 +BRDA:315,13,1,0 +BRDA:315,13,2,0 +BRDA:336,14,0,0 +BRDA:336,14,1,0 +BRDA:336,14,2,0 +BRF:40 +BRH:0 +end_of_record +TN: +SF:src/framework/core/ModelRegistry.ts +FN:9,(anonymous_0) +FN:19,(anonymous_1) +FN:23,(anonymous_2) +FN:27,(anonymous_3) +FN:31,(anonymous_4) +FN:32,(anonymous_5) +FN:35,(anonymous_6) +FN:36,(anonymous_7) +FN:39,(anonymous_8) +FN:43,(anonymous_9) +FN:48,(anonymous_10) +FN:77,(anonymous_11) +FN:81,(anonymous_12) +FN:95,(anonymous_13) +FNF:14 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +DA:6,0 +DA:7,0 +DA:10,0 +DA:11,0 +DA:14,0 +DA:16,0 +DA:20,0 +DA:24,0 +DA:28,0 +DA:32,0 +DA:36,0 +DA:40,0 +DA:44,0 +DA:45,0 +DA:50,0 +DA:51,0 +DA:55,0 +DA:56,0 +DA:60,0 +DA:61,0 +DA:65,0 +DA:66,0 +DA:70,0 +DA:71,0 +DA:74,0 +DA:78,0 +DA:82,0 +DA:83,0 +DA:86,0 +DA:87,0 +DA:90,0 +DA:91,0 +DA:96,0 +DA:97,0 +DA:100,0 +DA:101,0 +LF:36 +LH:0 +BRDA:16,0,0,0 +BRDA:16,0,1,0 +BRDA:50,1,0,0 +BRDA:50,1,1,0 +BRDA:55,2,0,0 +BRDA:55,2,1,0 +BRDA:55,3,0,0 +BRDA:55,3,1,0 +BRDA:60,4,0,0 +BRDA:60,4,1,0 +BRDA:60,5,0,0 +BRDA:60,5,1,0 +BRDA:65,6,0,0 +BRDA:65,6,1,0 +BRDA:70,7,0,0 +BRDA:70,7,1,0 +BRDA:82,8,0,0 +BRDA:82,8,1,0 +BRDA:82,9,0,0 +BRDA:82,9,1,0 +BRDA:86,10,0,0 +BRDA:86,10,1,0 +BRDA:86,11,0,0 +BRDA:86,11,1,0 +BRDA:90,12,0,0 +BRDA:90,12,1,0 +BRDA:96,13,0,0 +BRDA:96,13,1,0 +BRDA:96,14,0,0 +BRDA:96,14,1,0 +BRDA:100,15,0,0 +BRDA:100,15,1,0 +BRDA:100,16,0,0 +BRDA:100,16,1,0 +BRDA:100,16,2,0 +BRF:35 +BRH:0 +end_of_record +TN: +SF:src/framework/migrations/MigrationBuilder.ts +FN:17,(anonymous_0) +FN:30,(anonymous_1) +FN:35,(anonymous_2) +FN:40,(anonymous_3) +FN:45,(anonymous_4) +FN:50,(anonymous_5) +FN:56,(anonymous_6) +FN:75,(anonymous_7) +FN:87,(anonymous_8) +FN:97,(anonymous_9) +FN:123,(anonymous_10) +FN:144,(anonymous_11) +FN:168,(anonymous_12) +FN:192,(anonymous_13) +FN:208,(anonymous_14) +FN:218,(anonymous_15) +FN:223,(anonymous_16) +FN:229,(anonymous_17) +FN:233,(anonymous_18) +FN:237,(anonymous_19) +FN:246,(anonymous_20) +FN:270,(anonymous_21) +FN:280,(anonymous_22) +FN:319,(anonymous_23) +FN:332,(anonymous_24) +FN:336,(anonymous_25) +FN:343,(anonymous_26) +FN:347,(anonymous_27) +FN:355,(anonymous_28) +FN:381,(anonymous_29) +FN:387,(anonymous_30) +FN:388,(anonymous_31) +FN:396,(anonymous_32) +FN:400,(anonymous_33) +FN:413,(anonymous_34) +FN:425,(anonymous_35) +FN:442,(anonymous_36) +FN:458,createMigration +FNF:38 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,createMigration +DA:13,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:31,0 +DA:32,0 +DA:36,0 +DA:37,0 +DA:41,0 +DA:42,0 +DA:46,0 +DA:47,0 +DA:51,0 +DA:52,0 +DA:57,0 +DA:65,0 +DA:71,0 +DA:72,0 +DA:76,0 +DA:82,0 +DA:84,0 +DA:88,0 +DA:93,0 +DA:94,0 +DA:103,0 +DA:110,0 +DA:111,0 +DA:119,0 +DA:120,0 +DA:124,0 +DA:132,0 +DA:139,0 +DA:140,0 +DA:149,0 +DA:155,0 +DA:156,0 +DA:163,0 +DA:164,0 +DA:173,0 +DA:179,0 +DA:180,0 +DA:187,0 +DA:188,0 +DA:193,0 +DA:199,0 +DA:205,0 +DA:209,0 +DA:215,0 +DA:219,0 +DA:223,0 +DA:226,0 +DA:231,0 +DA:234,0 +DA:238,0 +DA:242,0 +DA:247,0 +DA:256,0 +DA:265,0 +DA:266,0 +DA:280,0 +DA:281,0 +DA:283,0 +DA:284,0 +DA:286,0 +DA:287,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:294,0 +DA:297,0 +DA:298,0 +DA:299,0 +DA:304,0 +DA:305,0 +DA:308,0 +DA:313,0 +DA:314,0 +DA:315,0 +DA:324,0 +DA:329,0 +DA:333,0 +DA:338,0 +DA:344,0 +DA:348,0 +DA:349,0 +DA:356,0 +DA:357,0 +DA:360,0 +DA:361,0 +DA:364,0 +DA:382,0 +DA:383,0 +DA:388,0 +DA:389,0 +DA:390,0 +DA:391,0 +DA:397,0 +DA:407,0 +DA:419,0 +DA:432,0 +DA:450,0 +DA:459,0 +LF:102 +LH:0 +BRDA:75,0,0,0 +BRDA:82,1,0,0 +BRDA:82,1,1,0 +BRDA:110,2,0,0 +BRDA:110,2,1,0 +BRDA:155,3,0,0 +BRDA:155,3,1,0 +BRDA:179,4,0,0 +BRDA:179,4,1,0 +BRDA:218,5,0,0 +BRDA:246,6,0,0 +BRDA:274,7,0,0 +BRDA:284,8,0,0 +BRDA:284,8,1,0 +BRDA:290,9,0,0 +BRDA:290,9,1,0 +BRDA:290,10,0,0 +BRDA:290,10,1,0 +BRDA:298,11,0,0 +BRDA:298,11,1,0 +BRDA:304,12,0,0 +BRDA:304,12,1,0 +BRDA:356,13,0,0 +BRDA:356,13,1,0 +BRDA:356,14,0,0 +BRDA:356,14,1,0 +BRDA:360,15,0,0 +BRDA:360,15,1,0 +BRDA:373,16,0,0 +BRDA:373,16,1,0 +BRDA:382,17,0,0 +BRDA:382,17,1,0 +BRDA:390,18,0,0 +BRDA:390,18,1,0 +BRF:34 +BRH:0 +end_of_record +TN: +SF:src/framework/migrations/MigrationManager.ts +FN:119,(anonymous_0) +FN:126,(anonymous_1) +FN:132,(anonymous_2) +FN:149,(anonymous_3) +FN:150,(anonymous_4) +FN:156,(anonymous_5) +FN:161,(anonymous_6) +FN:164,(anonymous_7) +FN:166,(anonymous_8) +FN:175,(anonymous_9) +FN:280,(anonymous_10) +FN:323,(anonymous_11) +FN:330,(anonymous_12) +FN:372,(anonymous_13) +FN:422,(anonymous_14) +FN:453,(anonymous_15) +FN:492,(anonymous_16) +FN:526,(anonymous_17) +FN:568,(anonymous_18) +FN:605,(anonymous_19) +FN:647,(anonymous_20) +FN:669,(anonymous_21) +FN:676,(anonymous_22) +FN:681,(anonymous_23) +FN:698,(anonymous_24) +FN:723,(anonymous_25) +FN:744,(anonymous_26) +FN:748,(anonymous_27) +FN:757,(anonymous_28) +FN:784,(anonymous_29) +FN:790,(anonymous_30) +FN:835,(anonymous_31) +FN:853,(anonymous_32) +FN:882,(anonymous_33) +FN:888,(anonymous_34) +FN:892,(anonymous_35) +FN:898,(anonymous_36) +FN:910,(anonymous_37) +FN:925,(anonymous_38) +FN:928,(anonymous_39) +FN:929,(anonymous_40) +FN:933,(anonymous_41) +FN:935,(anonymous_42) +FN:936,(anonymous_43) +FN:938,(anonymous_44) +FN:940,(anonymous_45) +FN:946,(anonymous_46) +FN:950,(anonymous_47) +FN:954,(anonymous_48) +FN:964,(anonymous_49) +FN:968,(anonymous_50) +FNF:51 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +FNDA:0,(anonymous_44) +FNDA:0,(anonymous_45) +FNDA:0,(anonymous_46) +FNDA:0,(anonymous_47) +FNDA:0,(anonymous_48) +FNDA:0,(anonymous_49) +FNDA:0,(anonymous_50) +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:128,0 +DA:131,0 +DA:132,0 +DA:135,0 +DA:136,0 +DA:139,0 +DA:140,0 +DA:142,0 +DA:150,0 +DA:151,0 +DA:157,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:170,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:190,0 +DA:191,0 +DA:195,0 +DA:197,0 +DA:198,0 +DA:209,0 +DA:211,0 +DA:212,0 +DA:218,0 +DA:219,0 +DA:223,0 +DA:224,0 +DA:228,0 +DA:231,0 +DA:232,0 +DA:236,0 +DA:237,0 +DA:239,0 +DA:241,0 +DA:247,0 +DA:249,0 +DA:250,0 +DA:252,0 +DA:259,0 +DA:261,0 +DA:272,0 +DA:273,0 +DA:275,0 +DA:288,0 +DA:289,0 +DA:291,0 +DA:296,0 +DA:297,0 +DA:298,0 +DA:302,0 +DA:304,0 +DA:305,0 +DA:309,0 +DA:312,0 +DA:313,0 +DA:315,0 +DA:319,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:329,0 +DA:330,0 +DA:332,0 +DA:333,0 +DA:336,0 +DA:337,0 +DA:348,0 +DA:349,0 +DA:351,0 +DA:353,0 +DA:354,0 +DA:356,0 +DA:361,0 +DA:363,0 +DA:367,0 +DA:377,0 +DA:378,0 +DA:379,0 +DA:381,0 +DA:382,0 +DA:383,0 +DA:385,0 +DA:387,0 +DA:392,0 +DA:402,0 +DA:403,0 +DA:404,0 +DA:405,0 +DA:409,0 +DA:426,0 +DA:428,0 +DA:430,0 +DA:433,0 +DA:436,0 +DA:439,0 +DA:442,0 +DA:445,0 +DA:448,0 +DA:457,0 +DA:459,0 +DA:460,0 +DA:464,0 +DA:469,0 +DA:470,0 +DA:473,0 +DA:474,0 +DA:475,0 +DA:477,0 +DA:478,0 +DA:479,0 +DA:480,0 +DA:481,0 +DA:485,0 +DA:488,0 +DA:496,0 +DA:498,0 +DA:499,0 +DA:502,0 +DA:504,0 +DA:505,0 +DA:507,0 +DA:508,0 +DA:509,0 +DA:511,0 +DA:512,0 +DA:513,0 +DA:514,0 +DA:515,0 +DA:519,0 +DA:522,0 +DA:530,0 +DA:532,0 +DA:533,0 +DA:536,0 +DA:540,0 +DA:541,0 +DA:543,0 +DA:544,0 +DA:545,0 +DA:547,0 +DA:548,0 +DA:550,0 +DA:551,0 +DA:553,0 +DA:554,0 +DA:555,0 +DA:556,0 +DA:561,0 +DA:564,0 +DA:572,0 +DA:574,0 +DA:575,0 +DA:578,0 +DA:582,0 +DA:583,0 +DA:585,0 +DA:586,0 +DA:587,0 +DA:589,0 +DA:590,0 +DA:591,0 +DA:592,0 +DA:593,0 +DA:594,0 +DA:598,0 +DA:601,0 +DA:609,0 +DA:611,0 +DA:612,0 +DA:615,0 +DA:617,0 +DA:618,0 +DA:620,0 +DA:621,0 +DA:622,0 +DA:624,0 +DA:625,0 +DA:626,0 +DA:627,0 +DA:629,0 +DA:630,0 +DA:631,0 +DA:632,0 +DA:635,0 +DA:636,0 +DA:640,0 +DA:643,0 +DA:651,0 +DA:653,0 +DA:654,0 +DA:657,0 +DA:659,0 +DA:660,0 +DA:661,0 +DA:663,0 +DA:664,0 +DA:672,0 +DA:673,0 +DA:678,0 +DA:683,0 +DA:685,0 +DA:687,0 +DA:689,0 +DA:691,0 +DA:693,0 +DA:699,0 +DA:700,0 +DA:703,0 +DA:704,0 +DA:707,0 +DA:708,0 +DA:712,0 +DA:713,0 +DA:716,0 +DA:717,0 +DA:718,0 +DA:724,0 +DA:735,0 +DA:736,0 +DA:739,0 +DA:740,0 +DA:745,0 +DA:747,0 +DA:748,0 +DA:750,0 +DA:751,0 +DA:752,0 +DA:758,0 +DA:760,0 +DA:761,0 +DA:763,0 +DA:773,0 +DA:774,0 +DA:775,0 +DA:778,0 +DA:779,0 +DA:786,0 +DA:794,0 +DA:795,0 +DA:798,0 +DA:799,0 +DA:800,0 +DA:803,0 +DA:804,0 +DA:805,0 +DA:807,0 +DA:817,0 +DA:818,0 +DA:819,0 +DA:823,0 +DA:839,0 +DA:840,0 +DA:841,0 +DA:842,0 +DA:843,0 +DA:846,0 +DA:849,0 +DA:854,0 +DA:856,0 +DA:857,0 +DA:860,0 +DA:861,0 +DA:862,0 +DA:866,0 +DA:867,0 +DA:870,0 +DA:884,0 +DA:889,0 +DA:891,0 +DA:892,0 +DA:895,0 +DA:899,0 +DA:900,0 +DA:903,0 +DA:906,0 +DA:911,0 +DA:912,0 +DA:914,0 +DA:915,0 +DA:916,0 +DA:918,0 +DA:919,0 +DA:922,0 +DA:926,0 +DA:927,0 +DA:928,0 +DA:929,0 +DA:934,0 +DA:935,0 +DA:937,0 +DA:939,0 +DA:941,0 +DA:947,0 +DA:951,0 +DA:955,0 +DA:956,0 +DA:959,0 +DA:960,0 +DA:961,0 +DA:964,0 +DA:969,0 +DA:970,0 +LF:315 +LH:0 +BRDA:122,0,0,0 +BRDA:122,0,1,0 +BRDA:135,1,0,0 +BRDA:135,1,1,0 +BRDA:135,2,0,0 +BRDA:135,2,1,0 +BRDA:157,3,0,0 +BRDA:157,3,1,0 +BRDA:167,4,0,0 +BRDA:167,4,1,0 +BRDA:168,5,0,0 +BRDA:168,5,1,0 +BRDA:177,6,0,0 +BRDA:185,7,0,0 +BRDA:185,7,1,0 +BRDA:190,8,0,0 +BRDA:190,8,1,0 +BRDA:218,9,0,0 +BRDA:218,9,1,0 +BRDA:223,10,0,0 +BRDA:223,10,1,0 +BRDA:231,11,0,0 +BRDA:231,11,1,0 +BRDA:281,12,0,0 +BRDA:304,13,0,0 +BRDA:304,13,1,0 +BRDA:304,14,0,0 +BRDA:304,14,1,0 +BRDA:312,15,0,0 +BRDA:312,15,1,0 +BRDA:325,16,0,0 +BRDA:325,16,1,0 +BRDA:330,17,0,0 +BRDA:330,17,1,0 +BRDA:332,18,0,0 +BRDA:332,18,1,0 +BRDA:383,19,0,0 +BRDA:383,19,1,0 +BRDA:385,20,0,0 +BRDA:385,20,1,0 +BRDA:428,21,0,0 +BRDA:428,21,1,0 +BRDA:428,21,2,0 +BRDA:428,21,3,0 +BRDA:428,21,4,0 +BRDA:428,21,5,0 +BRDA:428,21,6,0 +BRDA:459,22,0,0 +BRDA:459,22,1,0 +BRDA:459,23,0,0 +BRDA:459,23,1,0 +BRDA:473,24,0,0 +BRDA:473,24,1,0 +BRDA:478,25,0,0 +BRDA:478,25,1,0 +BRDA:479,26,0,0 +BRDA:479,26,1,0 +BRDA:498,27,0,0 +BRDA:498,27,1,0 +BRDA:507,28,0,0 +BRDA:507,28,1,0 +BRDA:512,29,0,0 +BRDA:512,29,1,0 +BRDA:532,30,0,0 +BRDA:532,30,1,0 +BRDA:532,31,0,0 +BRDA:532,31,1,0 +BRDA:543,32,0,0 +BRDA:543,32,1,0 +BRDA:548,33,0,0 +BRDA:548,33,1,0 +BRDA:553,34,0,0 +BRDA:553,34,1,0 +BRDA:574,35,0,0 +BRDA:574,35,1,0 +BRDA:574,36,0,0 +BRDA:574,36,1,0 +BRDA:585,37,0,0 +BRDA:585,37,1,0 +BRDA:590,38,0,0 +BRDA:590,38,1,0 +BRDA:611,39,0,0 +BRDA:611,39,1,0 +BRDA:620,40,0,0 +BRDA:620,40,1,0 +BRDA:629,41,0,0 +BRDA:629,41,1,0 +BRDA:653,42,0,0 +BRDA:653,42,1,0 +BRDA:683,43,0,0 +BRDA:683,43,1,0 +BRDA:683,43,2,0 +BRDA:683,43,3,0 +BRDA:683,43,4,0 +BRDA:685,44,0,0 +BRDA:685,44,1,0 +BRDA:687,45,0,0 +BRDA:687,45,1,0 +BRDA:689,46,0,0 +BRDA:689,46,1,0 +BRDA:691,47,0,0 +BRDA:691,47,1,0 +BRDA:699,48,0,0 +BRDA:699,48,1,0 +BRDA:699,49,0,0 +BRDA:699,49,1,0 +BRDA:699,49,2,0 +BRDA:703,50,0,0 +BRDA:703,50,1,0 +BRDA:703,51,0,0 +BRDA:703,51,1,0 +BRDA:707,52,0,0 +BRDA:707,52,1,0 +BRDA:707,53,0,0 +BRDA:707,53,1,0 +BRDA:716,54,0,0 +BRDA:716,54,1,0 +BRDA:735,55,0,0 +BRDA:735,55,1,0 +BRDA:739,56,0,0 +BRDA:739,56,1,0 +BRDA:745,57,0,0 +BRDA:745,57,1,0 +BRDA:751,58,0,0 +BRDA:751,58,1,0 +BRDA:758,59,0,0 +BRDA:758,59,1,0 +BRDA:774,60,0,0 +BRDA:774,60,1,0 +BRDA:778,61,0,0 +BRDA:778,61,1,0 +BRDA:794,62,0,0 +BRDA:794,62,1,0 +BRDA:794,63,0,0 +BRDA:794,63,1,0 +BRDA:805,64,0,0 +BRDA:805,64,1,0 +BRDA:840,65,0,0 +BRDA:840,65,1,0 +BRDA:840,66,0,0 +BRDA:840,66,1,0 +BRDA:899,67,0,0 +BRDA:899,67,1,0 +BRDA:915,68,0,0 +BRDA:915,68,1,0 +BRDA:916,69,0,0 +BRDA:916,69,1,0 +BRDA:918,70,0,0 +BRDA:918,70,1,0 +BRDA:919,71,0,0 +BRDA:919,71,1,0 +BRDA:935,72,0,0 +BRDA:935,72,1,0 +BRDA:937,73,0,0 +BRDA:937,73,1,0 +BRDA:939,74,0,0 +BRDA:939,74,1,0 +BRDA:941,75,0,0 +BRDA:941,75,1,0 +BRDA:947,76,0,0 +BRDA:947,76,1,0 +BRDA:955,77,0,0 +BRDA:955,77,1,0 +BRDA:956,78,0,0 +BRDA:956,78,1,0 +BRF:165 +BRH:0 +end_of_record +TN: +SF:src/framework/models/BaseModel.ts +FN:24,(anonymous_0) +FN:29,(anonymous_1) +FN:66,(anonymous_2) +FN:71,(anonymous_3) +FN:79,(anonymous_4) +FN:90,(anonymous_5) +FN:96,(anonymous_6) +FN:110,(anonymous_7) +FN:119,(anonymous_8) +FN:127,(anonymous_9) +FN:135,(anonymous_10) +FN:142,(anonymous_11) +FN:149,(anonymous_12) +FN:160,(anonymous_13) +FN:176,(anonymous_14) +FN:191,(anonymous_15) +FN:203,(anonymous_16) +FN:207,(anonymous_17) +FN:211,(anonymous_18) +FN:215,(anonymous_19) +FN:219,(anonymous_20) +FN:224,(anonymous_21) +FN:235,(anonymous_22) +FN:242,(anonymous_23) +FN:246,(anonymous_24) +FN:261,(anonymous_25) +FN:281,(anonymous_26) +FN:313,(anonymous_27) +FN:333,(anonymous_28) +FN:337,(anonymous_29) +FN:341,(anonymous_30) +FN:345,(anonymous_31) +FN:349,(anonymous_32) +FN:353,(anonymous_33) +FN:357,(anonymous_34) +FN:367,(anonymous_35) +FN:372,(anonymous_36) +FN:416,(anonymous_37) +FN:467,(anonymous_38) +FN:508,(anonymous_39) +FN:514,(anonymous_40) +FN:518,(anonymous_41) +FN:522,(anonymous_42) +FN:526,(anonymous_43) +FNF:44 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +DA:7,0 +DA:8,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:16,0 +DA:17,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:25,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:41,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:56,0 +DA:58,0 +DA:60,0 +DA:63,0 +DA:67,0 +DA:68,0 +DA:76,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:87,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:97,0 +DA:100,0 +DA:102,0 +DA:103,0 +DA:106,0 +DA:116,0 +DA:124,0 +DA:132,0 +DA:139,0 +DA:145,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:156,0 +DA:157,0 +DA:162,0 +DA:163,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:172,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:186,0 +DA:193,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:200,0 +DA:204,0 +DA:208,0 +DA:212,0 +DA:216,0 +DA:220,0 +DA:225,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:235,0 +DA:236,0 +DA:239,0 +DA:243,0 +DA:246,0 +DA:247,0 +DA:248,0 +DA:253,0 +DA:254,0 +DA:257,0 +DA:262,0 +DA:263,0 +DA:266,0 +DA:267,0 +DA:268,0 +DA:269,0 +DA:272,0 +DA:274,0 +DA:275,0 +DA:278,0 +DA:282,0 +DA:285,0 +DA:286,0 +DA:287,0 +DA:291,0 +DA:292,0 +DA:296,0 +DA:297,0 +DA:301,0 +DA:302,0 +DA:303,0 +DA:304,0 +DA:305,0 +DA:306,0 +DA:310,0 +DA:314,0 +DA:316,0 +DA:318,0 +DA:320,0 +DA:322,0 +DA:324,0 +DA:326,0 +DA:328,0 +DA:334,0 +DA:338,0 +DA:342,0 +DA:346,0 +DA:350,0 +DA:354,0 +DA:358,0 +DA:359,0 +DA:361,0 +DA:362,0 +DA:368,0 +DA:373,0 +DA:374,0 +DA:375,0 +DA:376,0 +DA:379,0 +DA:381,0 +DA:382,0 +DA:384,0 +DA:385,0 +DA:386,0 +DA:389,0 +DA:393,0 +DA:396,0 +DA:398,0 +DA:399,0 +DA:406,0 +DA:407,0 +DA:411,0 +DA:412,0 +DA:417,0 +DA:418,0 +DA:419,0 +DA:420,0 +DA:423,0 +DA:425,0 +DA:426,0 +DA:427,0 +DA:428,0 +DA:429,0 +DA:432,0 +DA:436,0 +DA:443,0 +DA:444,0 +DA:445,0 +DA:452,0 +DA:453,0 +DA:462,0 +DA:463,0 +DA:468,0 +DA:469,0 +DA:470,0 +DA:471,0 +DA:474,0 +DA:476,0 +DA:477,0 +DA:478,0 +DA:479,0 +DA:480,0 +DA:483,0 +DA:487,0 +DA:489,0 +DA:490,0 +DA:491,0 +DA:497,0 +DA:498,0 +DA:501,0 +DA:503,0 +DA:504,0 +DA:510,0 +DA:515,0 +DA:519,0 +DA:523,0 +DA:527,0 +LF:199 +LH:0 +BRDA:24,0,0,0 +BRDA:32,1,0,0 +BRDA:32,1,1,0 +BRDA:36,2,0,0 +BRDA:36,2,1,0 +BRDA:50,3,0,0 +BRDA:50,3,1,0 +BRDA:84,4,0,0 +BRDA:84,4,1,0 +BRDA:102,5,0,0 +BRDA:102,5,1,0 +BRDA:130,6,0,0 +BRDA:151,7,0,0 +BRDA:151,7,1,0 +BRDA:162,8,0,0 +BRDA:162,8,1,0 +BRDA:167,9,0,0 +BRDA:167,9,1,0 +BRDA:181,10,0,0 +BRDA:181,10,1,0 +BRDA:196,11,0,0 +BRDA:196,11,1,0 +BRDA:229,12,0,0 +BRDA:229,12,1,0 +BRDA:229,13,0,0 +BRDA:229,13,1,0 +BRDA:243,14,0,0 +BRDA:243,14,1,0 +BRDA:247,15,0,0 +BRDA:247,15,1,0 +BRDA:247,16,0,0 +BRDA:247,16,1,0 +BRDA:247,16,2,0 +BRDA:253,17,0,0 +BRDA:253,17,1,0 +BRDA:274,18,0,0 +BRDA:274,18,1,0 +BRDA:285,19,0,0 +BRDA:285,19,1,0 +BRDA:285,20,0,0 +BRDA:285,20,1,0 +BRDA:285,20,2,0 +BRDA:285,20,3,0 +BRDA:291,21,0,0 +BRDA:291,21,1,0 +BRDA:291,22,0,0 +BRDA:291,22,1,0 +BRDA:296,23,0,0 +BRDA:296,23,1,0 +BRDA:301,24,0,0 +BRDA:301,24,1,0 +BRDA:303,25,0,0 +BRDA:303,25,1,0 +BRDA:305,26,0,0 +BRDA:305,26,1,0 +BRDA:314,27,0,0 +BRDA:314,27,1,0 +BRDA:314,27,2,0 +BRDA:314,27,3,0 +BRDA:314,27,4,0 +BRDA:314,27,5,0 +BRDA:314,27,6,0 +BRDA:318,28,0,0 +BRDA:318,28,1,0 +BRDA:324,29,0,0 +BRDA:324,29,1,0 +BRDA:326,30,0,0 +BRDA:326,30,1,0 +BRDA:326,30,2,0 +BRDA:359,31,0,0 +BRDA:359,31,1,0 +BRDA:374,32,0,0 +BRDA:374,32,1,0 +BRDA:382,33,0,0 +BRDA:382,33,1,0 +BRDA:385,34,0,0 +BRDA:385,34,1,0 +BRDA:396,35,0,0 +BRDA:396,35,1,0 +BRDA:418,36,0,0 +BRDA:418,36,1,0 +BRDA:426,37,0,0 +BRDA:426,37,1,0 +BRDA:428,38,0,0 +BRDA:428,38,1,0 +BRDA:443,39,0,0 +BRDA:443,39,1,0 +BRDA:469,40,0,0 +BRDA:469,40,1,0 +BRDA:477,41,0,0 +BRDA:477,41,1,0 +BRDA:479,42,0,0 +BRDA:479,42,1,0 +BRDA:489,43,0,0 +BRDA:489,43,1,0 +BRDA:527,44,0,0 +BRDA:527,44,1,0 +BRF:97 +BRH:0 +end_of_record +TN: +SF:src/framework/models/decorators/Field.ts +FN:3,Field +FN:4,(anonymous_1) +FN:20,(anonymous_2) +FN:23,(anonymous_3) +FN:55,validateFieldValue +FN:91,isValidType +FN:111,getFieldConfig +FNF:7 +FNH:0 +FNDA:0,Field +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,validateFieldValue +FNDA:0,isValidType +FNDA:0,getFieldConfig +DA:4,0 +DA:6,0 +DA:7,0 +DA:11,0 +DA:14,0 +DA:17,0 +DA:19,0 +DA:21,0 +DA:25,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:44,0 +DA:45,0 +DA:60,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:69,0 +DA:70,0 +DA:74,0 +DA:75,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:88,0 +DA:92,0 +DA:94,0 +DA:96,0 +DA:98,0 +DA:100,0 +DA:102,0 +DA:104,0 +DA:106,0 +DA:112,0 +DA:113,0 +DA:115,0 +LF:43 +LH:0 +BRDA:6,0,0,0 +BRDA:6,0,1,0 +BRDA:25,1,0,0 +BRDA:25,1,1,0 +BRDA:29,2,0,0 +BRDA:29,2,1,0 +BRDA:35,3,0,0 +BRDA:35,3,1,0 +BRDA:44,4,0,0 +BRDA:44,4,1,0 +BRDA:63,5,0,0 +BRDA:63,5,1,0 +BRDA:63,6,0,0 +BRDA:63,6,1,0 +BRDA:63,6,2,0 +BRDA:63,6,3,0 +BRDA:69,7,0,0 +BRDA:69,7,1,0 +BRDA:69,8,0,0 +BRDA:69,8,1,0 +BRDA:74,9,0,0 +BRDA:74,9,1,0 +BRDA:79,10,0,0 +BRDA:79,10,1,0 +BRDA:81,11,0,0 +BRDA:81,11,1,0 +BRDA:83,12,0,0 +BRDA:83,12,1,0 +BRDA:92,13,0,0 +BRDA:92,13,1,0 +BRDA:92,13,2,0 +BRDA:92,13,3,0 +BRDA:92,13,4,0 +BRDA:92,13,5,0 +BRDA:92,13,6,0 +BRDA:96,14,0,0 +BRDA:96,14,1,0 +BRDA:102,15,0,0 +BRDA:102,15,1,0 +BRDA:104,16,0,0 +BRDA:104,16,1,0 +BRDA:104,16,2,0 +BRDA:112,17,0,0 +BRDA:112,17,1,0 +BRF:44 +BRH:0 +end_of_record +TN: +SF:src/framework/models/decorators/Model.ts +FN:6,Model +FN:7,(anonymous_1) +FN:25,autoDetectType +FNF:3 +FNH:0 +FNDA:0,Model +FNDA:0,(anonymous_1) +FNDA:0,autoDetectType +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:16,0 +DA:21,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:45,0 +DA:46,0 +DA:51,0 +LF:20 +LH:0 +BRDA:6,0,0,0 +BRDA:9,1,0,0 +BRDA:9,1,1,0 +BRDA:10,2,0,0 +BRDA:10,2,1,0 +BRDA:11,3,0,0 +BRDA:11,3,1,0 +BRDA:29,4,0,0 +BRDA:29,4,1,0 +BRDA:29,5,0,0 +BRDA:29,5,1,0 +BRDA:37,6,0,0 +BRDA:37,6,1,0 +BRDA:37,7,0,0 +BRDA:37,7,1,0 +BRDA:45,8,0,0 +BRDA:45,8,1,0 +BRF:17 +BRH:0 +end_of_record +TN: +SF:src/framework/models/decorators/hooks.ts +FN:1,BeforeCreate +FN:5,AfterCreate +FN:9,BeforeUpdate +FN:13,AfterUpdate +FN:17,BeforeDelete +FN:21,AfterDelete +FN:25,BeforeSave +FN:29,AfterSave +FN:33,registerHook +FN:52,getHooks +FNF:10 +FNH:0 +FNDA:0,BeforeCreate +FNDA:0,AfterCreate +FNDA:0,BeforeUpdate +FNDA:0,AfterUpdate +FNDA:0,BeforeDelete +FNDA:0,AfterDelete +FNDA:0,BeforeSave +FNDA:0,AfterSave +FNDA:0,registerHook +FNDA:0,getHooks +DA:2,0 +DA:6,0 +DA:10,0 +DA:14,0 +DA:18,0 +DA:22,0 +DA:26,0 +DA:30,0 +DA:35,0 +DA:36,0 +DA:40,0 +DA:43,0 +DA:46,0 +DA:48,0 +DA:53,0 +DA:54,0 +DA:56,0 +LF:17 +LH:0 +BRDA:35,0,0,0 +BRDA:35,0,1,0 +BRDA:40,1,0,0 +BRDA:40,1,1,0 +BRDA:53,2,0,0 +BRDA:53,2,1,0 +BRDA:56,3,0,0 +BRDA:56,3,1,0 +BRF:8 +BRH:0 +end_of_record +TN: +SF:src/framework/models/decorators/relationships.ts +FN:4,BelongsTo +FN:9,(anonymous_1) +FN:23,HasMany +FN:28,(anonymous_3) +FN:43,HasOne +FN:48,(anonymous_5) +FN:62,ManyToMany +FN:68,(anonymous_7) +FN:83,registerRelationship +FN:97,createRelationshipProperty +FN:105,(anonymous_10) +FN:120,(anonymous_11) +FN:133,getRelationshipConfig +FNF:13 +FNH:0 +FNDA:0,BelongsTo +FNDA:0,(anonymous_1) +FNDA:0,HasMany +FNDA:0,(anonymous_3) +FNDA:0,HasOne +FNDA:0,(anonymous_5) +FNDA:0,ManyToMany +FNDA:0,(anonymous_7) +FNDA:0,registerRelationship +FNDA:0,createRelationshipProperty +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,getRelationshipConfig +DA:9,0 +DA:10,0 +DA:18,0 +DA:19,0 +DA:28,0 +DA:29,0 +DA:38,0 +DA:39,0 +DA:48,0 +DA:49,0 +DA:57,0 +DA:58,0 +DA:68,0 +DA:69,0 +DA:78,0 +DA:79,0 +DA:85,0 +DA:86,0 +DA:90,0 +DA:92,0 +DA:102,0 +DA:104,0 +DA:107,0 +DA:108,0 +DA:111,0 +DA:113,0 +DA:115,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:137,0 +DA:138,0 +DA:140,0 +LF:33 +LH:0 +BRDA:7,0,0,0 +BRDA:14,1,0,0 +BRDA:14,1,1,0 +BRDA:26,2,0,0 +BRDA:33,3,0,0 +BRDA:33,3,1,0 +BRDA:46,4,0,0 +BRDA:53,5,0,0 +BRDA:53,5,1,0 +BRDA:66,6,0,0 +BRDA:73,7,0,0 +BRDA:73,7,1,0 +BRDA:85,8,0,0 +BRDA:85,8,1,0 +BRDA:107,9,0,0 +BRDA:107,9,1,0 +BRDA:107,10,0,0 +BRDA:107,10,1,0 +BRDA:111,11,0,0 +BRDA:111,11,1,0 +BRDA:122,12,0,0 +BRDA:122,12,1,0 +BRDA:137,13,0,0 +BRDA:137,13,1,0 +BRF:24 +BRH:0 +end_of_record +TN: +SF:src/framework/pinning/PinningManager.ts +FN:64,(anonymous_0) +FN:82,(anonymous_1) +FN:99,(anonymous_2) +FN:168,(anonymous_3) +FN:195,(anonymous_4) +FN:210,(anonymous_5) +FN:265,(anonymous_6) +FN:273,(anonymous_7) +FN:279,(anonymous_8) +FN:280,(anonymous_9) +FN:297,(anonymous_10) +FN:317,(anonymous_11) +FN:342,(anonymous_12) +FN:347,(anonymous_13) +FN:348,(anonymous_14) +FN:361,(anonymous_15) +FN:368,(anonymous_16) +FN:384,(anonymous_17) +FN:431,(anonymous_18) +FN:432,(anonymous_19) +FN:433,(anonymous_20) +FN:440,(anonymous_21) +FN:448,(anonymous_22) +FN:459,(anonymous_23) +FN:461,(anonymous_24) +FN:475,(anonymous_25) +FN:481,(anonymous_26) +FN:482,(anonymous_27) +FN:490,(anonymous_28) +FN:503,(anonymous_29) +FN:507,(anonymous_30) +FN:508,(anonymous_31) +FN:510,(anonymous_32) +FN:520,(anonymous_33) +FN:526,(anonymous_34) +FN:533,(anonymous_35) +FN:552,(anonymous_36) +FN:554,(anonymous_37) +FN:554,(anonymous_38) +FN:570,(anonymous_39) +FN:571,(anonymous_40) +FN:575,(anonymous_41) +FN:580,(anonymous_42) +FN:594,(anonymous_43) +FNF:44 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:78,0 +DA:83,0 +DA:84,0 +DA:92,0 +DA:93,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:119,0 +DA:122,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:131,0 +DA:135,0 +DA:138,0 +DA:150,0 +DA:151,0 +DA:153,0 +DA:158,0 +DA:160,0 +DA:162,0 +DA:163,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:186,0 +DA:187,0 +DA:189,0 +DA:190,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:211,0 +DA:212,0 +DA:214,0 +DA:217,0 +DA:218,0 +DA:222,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:230,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:245,0 +DA:246,0 +DA:253,0 +DA:255,0 +DA:258,0 +DA:261,0 +DA:271,0 +DA:272,0 +DA:273,0 +DA:276,0 +DA:278,0 +DA:279,0 +DA:280,0 +DA:282,0 +DA:283,0 +DA:287,0 +DA:292,0 +DA:294,0 +DA:296,0 +DA:297,0 +DA:300,0 +DA:301,0 +DA:304,0 +DA:307,0 +DA:309,0 +DA:310,0 +DA:313,0 +DA:318,0 +DA:319,0 +DA:322,0 +DA:323,0 +DA:325,0 +DA:329,0 +DA:330,0 +DA:334,0 +DA:335,0 +DA:338,0 +DA:343,0 +DA:346,0 +DA:347,0 +DA:348,0 +DA:350,0 +DA:351,0 +DA:353,0 +DA:354,0 +DA:357,0 +DA:362,0 +DA:365,0 +DA:366,0 +DA:367,0 +DA:368,0 +DA:371,0 +DA:372,0 +DA:377,0 +DA:378,0 +DA:379,0 +DA:385,0 +DA:386,0 +DA:388,0 +DA:389,0 +DA:390,0 +DA:392,0 +DA:395,0 +DA:396,0 +DA:397,0 +DA:398,0 +DA:403,0 +DA:404,0 +DA:405,0 +DA:410,0 +DA:411,0 +DA:412,0 +DA:415,0 +DA:416,0 +DA:421,0 +DA:422,0 +DA:425,0 +DA:426,0 +DA:432,0 +DA:433,0 +DA:434,0 +DA:441,0 +DA:442,0 +DA:443,0 +DA:449,0 +DA:450,0 +DA:451,0 +DA:453,0 +DA:454,0 +DA:460,0 +DA:461,0 +DA:462,0 +DA:465,0 +DA:466,0 +DA:467,0 +DA:468,0 +DA:469,0 +DA:470,0 +DA:475,0 +DA:477,0 +DA:481,0 +DA:482,0 +DA:491,0 +DA:492,0 +DA:506,0 +DA:507,0 +DA:508,0 +DA:510,0 +DA:516,0 +DA:521,0 +DA:522,0 +DA:525,0 +DA:526,0 +DA:529,0 +DA:533,0 +DA:538,0 +DA:540,0 +DA:553,0 +DA:554,0 +DA:556,0 +DA:557,0 +DA:560,0 +DA:561,0 +DA:562,0 +DA:563,0 +DA:564,0 +DA:566,0 +DA:571,0 +DA:576,0 +DA:581,0 +DA:583,0 +DA:584,0 +DA:587,0 +DA:588,0 +DA:590,0 +DA:595,0 +DA:596,0 +LF:218 +LH:0 +BRDA:66,0,0,0 +BRDA:73,1,0,0 +BRDA:73,1,1,0 +BRDA:74,2,0,0 +BRDA:74,2,1,0 +BRDA:75,3,0,0 +BRDA:75,3,1,0 +BRDA:103,4,0,0 +BRDA:107,5,0,0 +BRDA:107,5,1,0 +BRDA:113,6,0,0 +BRDA:113,6,1,0 +BRDA:127,7,0,0 +BRDA:127,7,1,0 +BRDA:168,8,0,0 +BRDA:171,9,0,0 +BRDA:171,9,1,0 +BRDA:177,10,0,0 +BRDA:177,10,1,0 +BRDA:177,11,0,0 +BRDA:177,11,1,0 +BRDA:197,12,0,0 +BRDA:197,12,1,0 +BRDA:203,13,0,0 +BRDA:203,13,1,0 +BRDA:214,14,0,0 +BRDA:214,14,1,0 +BRDA:214,14,2,0 +BRDA:214,14,3,0 +BRDA:214,14,4,0 +BRDA:214,14,5,0 +BRDA:214,15,0,0 +BRDA:214,15,1,0 +BRDA:217,16,0,0 +BRDA:217,16,1,0 +BRDA:222,17,0,0 +BRDA:222,17,1,0 +BRDA:225,18,0,0 +BRDA:225,18,1,0 +BRDA:232,19,0,0 +BRDA:232,19,1,0 +BRDA:238,20,0,0 +BRDA:238,20,1,0 +BRDA:240,21,0,0 +BRDA:240,21,1,0 +BRDA:245,22,0,0 +BRDA:245,22,1,0 +BRDA:251,23,0,0 +BRDA:251,23,1,0 +BRDA:253,24,0,0 +BRDA:253,24,1,0 +BRDA:258,25,0,0 +BRDA:258,25,1,0 +BRDA:271,26,0,0 +BRDA:271,26,1,0 +BRDA:276,27,0,0 +BRDA:276,27,1,0 +BRDA:282,28,0,0 +BRDA:282,28,1,0 +BRDA:282,29,0,0 +BRDA:282,29,1,0 +BRDA:294,30,0,0 +BRDA:294,30,1,0 +BRDA:300,31,0,0 +BRDA:300,31,1,0 +BRDA:300,32,0,0 +BRDA:300,32,1,0 +BRDA:307,33,0,0 +BRDA:307,33,1,0 +BRDA:319,34,0,0 +BRDA:319,34,1,0 +BRDA:323,35,0,0 +BRDA:323,35,1,0 +BRDA:329,36,0,0 +BRDA:329,36,1,0 +BRDA:334,37,0,0 +BRDA:334,37,1,0 +BRDA:351,38,0,0 +BRDA:351,38,1,0 +BRDA:365,39,0,0 +BRDA:365,39,1,0 +BRDA:377,40,0,0 +BRDA:377,40,1,0 +BRDA:390,41,0,0 +BRDA:390,41,1,0 +BRDA:395,42,0,0 +BRDA:395,42,1,0 +BRDA:397,43,0,0 +BRDA:397,43,1,0 +BRDA:403,44,0,0 +BRDA:403,44,1,0 +BRDA:404,45,0,0 +BRDA:404,45,1,0 +BRDA:411,46,0,0 +BRDA:411,46,1,0 +BRDA:411,47,0,0 +BRDA:411,47,1,0 +BRDA:415,48,0,0 +BRDA:415,48,1,0 +BRDA:415,49,0,0 +BRDA:415,49,1,0 +BRDA:425,50,0,0 +BRDA:425,50,1,0 +BRDA:441,51,0,0 +BRDA:441,51,1,0 +BRDA:451,52,0,0 +BRDA:451,52,1,0 +BRDA:451,52,2,0 +BRDA:467,53,0,0 +BRDA:467,53,1,0 +BRDA:468,54,0,0 +BRDA:468,54,1,0 +BRDA:469,55,0,0 +BRDA:469,55,1,0 +BRDA:480,56,0,0 +BRDA:480,56,1,0 +BRDA:481,57,0,0 +BRDA:481,57,1,0 +BRDA:482,58,0,0 +BRDA:482,58,1,0 +BRDA:483,59,0,0 +BRDA:483,59,1,0 +BRDA:484,60,0,0 +BRDA:484,60,1,0 +BRDA:529,61,0,0 +BRDA:529,61,1,0 +BRDA:533,62,0,0 +BRDA:533,62,1,0 +BRDA:538,63,0,0 +BRDA:538,63,1,0 +BRDA:556,64,0,0 +BRDA:556,64,1,0 +BRF:132 +BRH:0 +end_of_record +TN: +SF:src/framework/pubsub/PubSubManager.ts +FN:96,(anonymous_0) +FN:134,(anonymous_1) +FN:136,(anonymous_2) +FN:146,(anonymous_3) +FN:154,(anonymous_4) +FN:168,(anonymous_5) +FN:201,(anonymous_6) +FN:245,(anonymous_7) +FN:278,(anonymous_8) +FN:301,(anonymous_9) +FN:312,(anonymous_10) +FN:341,(anonymous_11) +FN:350,(anonymous_12) +FN:373,(anonymous_13) +FN:382,(anonymous_14) +FN:397,(anonymous_15) +FN:416,(anonymous_16) +FN:455,(anonymous_17) +FN:467,(anonymous_18) +FN:475,(anonymous_19) +FN:502,(anonymous_20) +FN:539,(anonymous_21) +FN:558,(anonymous_22) +FN:566,(anonymous_23) +FN:577,(anonymous_24) +FN:578,(anonymous_25) +FN:584,(anonymous_26) +FN:616,(anonymous_27) +FN:636,(anonymous_28) +FN:640,(anonymous_29) +FN:645,(anonymous_30) +FN:650,(anonymous_31) +FN:655,(anonymous_32) +FN:660,(anonymous_33) +FN:666,(anonymous_34) +FN:672,(anonymous_35) +FN:691,(anonymous_36) +FNF:37 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +DA:87,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:93,0 +DA:94,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:122,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:140,0 +DA:143,0 +DA:147,0 +DA:148,0 +DA:150,0 +DA:151,0 +DA:155,0 +DA:156,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:164,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:174,0 +DA:175,0 +DA:178,0 +DA:179,0 +DA:183,0 +DA:184,0 +DA:188,0 +DA:189,0 +DA:192,0 +DA:193,0 +DA:195,0 +DA:196,0 +DA:212,0 +DA:213,0 +DA:216,0 +DA:226,0 +DA:228,0 +DA:231,0 +DA:232,0 +DA:234,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:255,0 +DA:256,0 +DA:259,0 +DA:261,0 +DA:262,0 +DA:274,0 +DA:275,0 +DA:278,0 +DA:279,0 +DA:283,0 +DA:284,0 +DA:287,0 +DA:289,0 +DA:290,0 +DA:292,0 +DA:294,0 +DA:295,0 +DA:296,0 +DA:302,0 +DA:303,0 +DA:305,0 +DA:306,0 +DA:309,0 +DA:310,0 +DA:312,0 +DA:313,0 +DA:314,0 +DA:315,0 +DA:319,0 +DA:320,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:327,0 +DA:330,0 +DA:331,0 +DA:333,0 +DA:335,0 +DA:336,0 +DA:342,0 +DA:350,0 +DA:351,0 +DA:352,0 +DA:354,0 +DA:362,0 +DA:374,0 +DA:382,0 +DA:383,0 +DA:384,0 +DA:386,0 +DA:398,0 +DA:399,0 +DA:402,0 +DA:403,0 +DA:406,0 +DA:407,0 +DA:410,0 +DA:411,0 +DA:412,0 +DA:413,0 +DA:415,0 +DA:416,0 +DA:419,0 +DA:421,0 +DA:422,0 +DA:424,0 +DA:425,0 +DA:429,0 +DA:431,0 +DA:432,0 +DA:435,0 +DA:436,0 +DA:446,0 +DA:448,0 +DA:449,0 +DA:450,0 +DA:460,0 +DA:461,0 +DA:463,0 +DA:464,0 +DA:467,0 +DA:468,0 +DA:470,0 +DA:476,0 +DA:479,0 +DA:489,0 +DA:498,0 +DA:503,0 +DA:504,0 +DA:507,0 +DA:513,0 +DA:518,0 +DA:521,0 +DA:522,0 +DA:523,0 +DA:527,0 +DA:528,0 +DA:531,0 +DA:533,0 +DA:534,0 +DA:544,0 +DA:545,0 +DA:546,0 +DA:548,0 +DA:549,0 +DA:551,0 +DA:553,0 +DA:554,0 +DA:557,0 +DA:558,0 +DA:562,0 +DA:567,0 +DA:569,0 +DA:572,0 +DA:573,0 +DA:578,0 +DA:579,0 +DA:585,0 +DA:587,0 +DA:588,0 +DA:590,0 +DA:593,0 +DA:594,0 +DA:595,0 +DA:596,0 +DA:598,0 +DA:602,0 +DA:603,0 +DA:604,0 +DA:605,0 +DA:606,0 +DA:609,0 +DA:610,0 +DA:621,0 +DA:622,0 +DA:630,0 +DA:631,0 +DA:632,0 +DA:637,0 +DA:641,0 +DA:642,0 +DA:646,0 +DA:651,0 +DA:656,0 +DA:661,0 +DA:662,0 +DA:667,0 +DA:668,0 +DA:673,0 +DA:675,0 +DA:676,0 +DA:677,0 +DA:679,0 +DA:683,0 +DA:684,0 +DA:685,0 +DA:687,0 +DA:692,0 +DA:695,0 +DA:696,0 +DA:697,0 +DA:701,0 +DA:704,0 +DA:707,0 +DA:709,0 +DA:710,0 +LF:220 +LH:0 +BRDA:96,0,0,0 +BRDA:135,1,0,0 +BRDA:135,1,1,0 +BRDA:147,2,0,0 +BRDA:147,2,1,0 +BRDA:155,3,0,0 +BRDA:155,3,1,0 +BRDA:158,4,0,0 +BRDA:158,4,1,0 +BRDA:160,5,0,0 +BRDA:160,5,1,0 +BRDA:169,6,0,0 +BRDA:169,6,1,0 +BRDA:178,7,0,0 +BRDA:178,7,1,0 +BRDA:183,8,0,0 +BRDA:183,8,1,0 +BRDA:188,9,0,0 +BRDA:188,9,1,0 +BRDA:204,10,0,0 +BRDA:212,11,0,0 +BRDA:212,11,1,0 +BRDA:212,12,0,0 +BRDA:212,12,1,0 +BRDA:231,13,0,0 +BRDA:231,13,1,0 +BRDA:231,14,0,0 +BRDA:231,14,1,0 +BRDA:248,15,0,0 +BRDA:255,16,0,0 +BRDA:255,16,1,0 +BRDA:255,17,0,0 +BRDA:255,17,1,0 +BRDA:268,18,0,0 +BRDA:268,18,1,0 +BRDA:274,19,0,0 +BRDA:274,19,1,0 +BRDA:305,20,0,0 +BRDA:305,20,1,0 +BRDA:310,21,0,0 +BRDA:310,21,1,0 +BRDA:313,22,0,0 +BRDA:313,22,1,0 +BRDA:324,23,0,0 +BRDA:324,23,1,0 +BRDA:352,24,0,0 +BRDA:352,24,1,0 +BRDA:363,25,0,0 +BRDA:363,25,1,0 +BRDA:384,26,0,0 +BRDA:384,26,1,0 +BRDA:387,27,0,0 +BRDA:387,27,1,0 +BRDA:403,28,0,0 +BRDA:403,28,1,0 +BRDA:412,29,0,0 +BRDA:412,29,1,0 +BRDA:419,30,0,0 +BRDA:419,30,1,0 +BRDA:424,31,0,0 +BRDA:424,31,1,0 +BRDA:424,32,0,0 +BRDA:424,32,1,0 +BRDA:435,33,0,0 +BRDA:435,33,1,0 +BRDA:439,34,0,0 +BRDA:439,34,1,0 +BRDA:458,35,0,0 +BRDA:463,36,0,0 +BRDA:463,36,1,0 +BRDA:479,37,0,0 +BRDA:479,37,1,0 +BRDA:480,38,0,0 +BRDA:480,38,1,0 +BRDA:480,38,2,0 +BRDA:489,39,0,0 +BRDA:489,39,1,0 +BRDA:490,40,0,0 +BRDA:490,40,1,0 +BRDA:490,40,2,0 +BRDA:507,41,0,0 +BRDA:507,41,1,0 +BRDA:507,42,0,0 +BRDA:507,42,1,0 +BRDA:513,43,0,0 +BRDA:513,43,1,0 +BRDA:521,44,0,0 +BRDA:521,44,1,0 +BRDA:521,45,0,0 +BRDA:521,45,1,0 +BRDA:521,45,2,0 +BRDA:527,46,0,0 +BRDA:527,46,1,0 +BRDA:542,47,0,0 +BRDA:553,48,0,0 +BRDA:553,48,1,0 +BRDA:567,49,0,0 +BRDA:567,49,1,0 +BRDA:585,50,0,0 +BRDA:585,50,1,0 +BRDA:595,51,0,0 +BRDA:595,51,1,0 +BRDA:621,52,0,0 +BRDA:621,52,1,0 +BRDA:662,53,0,0 +BRDA:662,53,1,0 +BRDA:668,54,0,0 +BRDA:668,54,1,0 +BRDA:695,55,0,0 +BRDA:695,55,1,0 +BRF:110 +BRH:0 +end_of_record +TN: +SF:src/framework/query/QueryBuilder.ts +FN:16,(anonymous_0) +FN:21,(anonymous_1) +FN:26,(anonymous_2) +FN:30,(anonymous_3) +FN:34,(anonymous_4) +FN:38,(anonymous_5) +FN:42,(anonymous_6) +FN:46,(anonymous_7) +FN:50,(anonymous_8) +FN:54,(anonymous_9) +FN:59,(anonymous_10) +FN:63,(anonymous_11) +FN:71,(anonymous_12) +FN:75,(anonymous_13) +FN:79,(anonymous_14) +FN:84,(anonymous_15) +FN:88,(anonymous_16) +FN:98,(anonymous_17) +FN:112,(anonymous_18) +FN:116,(anonymous_19) +FN:120,(anonymous_20) +FN:124,(anonymous_21) +FN:129,(anonymous_22) +FN:134,(anonymous_23) +FN:138,(anonymous_24) +FN:144,(anonymous_25) +FN:145,(anonymous_26) +FN:150,(anonymous_27) +FN:155,(anonymous_28) +FN:160,(anonymous_29) +FN:164,(anonymous_30) +FN:169,(anonymous_31) +FN:176,(anonymous_32) +FN:181,(anonymous_33) +FN:185,(anonymous_34) +FN:193,(anonymous_35) +FN:198,(anonymous_36) +FN:204,(anonymous_37) +FN:210,(anonymous_38) +FN:215,(anonymous_39) +FN:219,(anonymous_40) +FN:224,(anonymous_41) +FN:232,(anonymous_42) +FN:236,(anonymous_43) +FN:244,(anonymous_44) +FN:249,(anonymous_45) +FN:254,(anonymous_46) +FN:259,(anonymous_47) +FN:264,(anonymous_48) +FN:269,(anonymous_49) +FN:275,(anonymous_50) +FN:304,(anonymous_51) +FN:331,(anonymous_52) +FN:337,(anonymous_53) +FN:344,(anonymous_54) +FN:354,(anonymous_55) +FN:358,(anonymous_56) +FN:362,(anonymous_57) +FN:366,(anonymous_58) +FN:370,(anonymous_59) +FN:374,(anonymous_60) +FN:378,(anonymous_61) +FN:382,(anonymous_62) +FN:386,(anonymous_63) +FN:391,(anonymous_64) +FN:406,(anonymous_65) +FN:412,(anonymous_66) +FN:419,(anonymous_67) +FN:435,(anonymous_68) +FNF:69 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +FNDA:0,(anonymous_44) +FNDA:0,(anonymous_45) +FNDA:0,(anonymous_46) +FNDA:0,(anonymous_47) +FNDA:0,(anonymous_48) +FNDA:0,(anonymous_49) +FNDA:0,(anonymous_50) +FNDA:0,(anonymous_51) +FNDA:0,(anonymous_52) +FNDA:0,(anonymous_53) +FNDA:0,(anonymous_54) +FNDA:0,(anonymous_55) +FNDA:0,(anonymous_56) +FNDA:0,(anonymous_57) +FNDA:0,(anonymous_58) +FNDA:0,(anonymous_59) +FNDA:0,(anonymous_60) +FNDA:0,(anonymous_61) +FNDA:0,(anonymous_62) +FNDA:0,(anonymous_63) +FNDA:0,(anonymous_64) +FNDA:0,(anonymous_65) +FNDA:0,(anonymous_66) +FNDA:0,(anonymous_67) +FNDA:0,(anonymous_68) +DA:7,0 +DA:8,0 +DA:9,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:17,0 +DA:22,0 +DA:23,0 +DA:27,0 +DA:31,0 +DA:35,0 +DA:39,0 +DA:43,0 +DA:47,0 +DA:51,0 +DA:55,0 +DA:60,0 +DA:68,0 +DA:72,0 +DA:76,0 +DA:80,0 +DA:85,0 +DA:89,0 +DA:94,0 +DA:99,0 +DA:100,0 +DA:102,0 +DA:108,0 +DA:113,0 +DA:117,0 +DA:121,0 +DA:125,0 +DA:130,0 +DA:131,0 +DA:135,0 +DA:139,0 +DA:140,0 +DA:145,0 +DA:146,0 +DA:151,0 +DA:152,0 +DA:156,0 +DA:157,0 +DA:161,0 +DA:165,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:177,0 +DA:178,0 +DA:182,0 +DA:187,0 +DA:189,0 +DA:194,0 +DA:195,0 +DA:199,0 +DA:200,0 +DA:205,0 +DA:206,0 +DA:211,0 +DA:212,0 +DA:216,0 +DA:220,0 +DA:221,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:229,0 +DA:233,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:241,0 +DA:245,0 +DA:246,0 +DA:250,0 +DA:251,0 +DA:255,0 +DA:256,0 +DA:260,0 +DA:261,0 +DA:265,0 +DA:266,0 +DA:270,0 +DA:271,0 +DA:287,0 +DA:288,0 +DA:290,0 +DA:292,0 +DA:308,0 +DA:309,0 +DA:311,0 +DA:312,0 +DA:314,0 +DA:315,0 +DA:318,0 +DA:321,0 +DA:322,0 +DA:325,0 +DA:326,0 +DA:333,0 +DA:334,0 +DA:339,0 +DA:340,0 +DA:345,0 +DA:350,0 +DA:355,0 +DA:359,0 +DA:363,0 +DA:367,0 +DA:371,0 +DA:375,0 +DA:379,0 +DA:383,0 +DA:387,0 +DA:392,0 +DA:393,0 +DA:394,0 +DA:395,0 +DA:396,0 +DA:397,0 +DA:398,0 +DA:399,0 +DA:400,0 +DA:402,0 +DA:408,0 +DA:410,0 +DA:411,0 +DA:412,0 +DA:414,0 +DA:417,0 +DA:418,0 +DA:419,0 +DA:421,0 +DA:424,0 +DA:425,0 +DA:428,0 +DA:429,0 +DA:432,0 +DA:436,0 +LF:141 +LH:0 +BRDA:129,0,0,0 +BRDA:221,1,0,0 +BRDA:221,1,1,0 +BRDA:226,2,0,0 +BRDA:226,2,1,0 +BRDA:238,3,0,0 +BRDA:238,3,1,0 +BRDA:276,4,0,0 +BRDA:277,5,0,0 +BRDA:314,6,0,0 +BRDA:314,6,1,0 +BRDA:321,7,0,0 +BRDA:321,7,1,0 +BRDA:344,8,0,0 +BRDA:410,9,0,0 +BRDA:410,9,1,0 +BRDA:417,10,0,0 +BRDA:417,10,1,0 +BRDA:424,11,0,0 +BRDA:424,11,1,0 +BRDA:428,12,0,0 +BRDA:428,12,1,0 +BRF:22 +BRH:0 +end_of_record +TN: +SF:src/framework/query/QueryCache.ts +FN:27,(anonymous_0) +FN:41,(anonymous_1) +FN:53,(anonymous_2) +FN:64,(anonymous_3) +FN:91,(anonymous_4) +FN:94,(anonymous_5) +FN:99,(anonymous_6) +FN:118,(anonymous_7) +FN:125,(anonymous_8) +FN:139,(anonymous_9) +FN:154,(anonymous_10) +FN:163,(anonymous_11) +FN:168,(anonymous_12) +FN:171,(anonymous_13) +FN:186,(anonymous_14) +FN:188,(anonymous_15) +FN:193,(anonymous_16) +FN:197,(anonymous_17) +FN:211,(anonymous_18) +FN:223,(anonymous_19) +FN:233,(anonymous_20) +FN:238,(anonymous_21) +FN:247,(anonymous_22) +FN:248,(anonymous_23) +FN:251,(anonymous_24) +FN:263,(anonymous_25) +FN:287,(anonymous_26) +FN:297,(anonymous_27) +FN:303,(anonymous_28) +FNF:29 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +DA:22,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:50,0 +DA:53,0 +DA:61,0 +DA:65,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:90,0 +DA:91,0 +DA:95,0 +DA:96,0 +DA:99,0 +DA:101,0 +DA:110,0 +DA:111,0 +DA:114,0 +DA:115,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:126,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:135,0 +DA:136,0 +DA:140,0 +DA:142,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:150,0 +DA:151,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:164,0 +DA:169,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:177,0 +DA:181,0 +DA:182,0 +DA:187,0 +DA:188,0 +DA:193,0 +DA:198,0 +DA:199,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:207,0 +DA:212,0 +DA:214,0 +DA:215,0 +DA:218,0 +DA:219,0 +DA:224,0 +DA:225,0 +DA:228,0 +DA:229,0 +DA:234,0 +DA:244,0 +DA:245,0 +DA:247,0 +DA:248,0 +DA:251,0 +DA:252,0 +DA:255,0 +DA:264,0 +DA:267,0 +DA:268,0 +DA:270,0 +DA:272,0 +DA:273,0 +DA:275,0 +DA:276,0 +DA:277,0 +DA:281,0 +DA:282,0 +DA:283,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:293,0 +DA:298,0 +DA:299,0 +DA:304,0 +DA:305,0 +DA:307,0 +DA:308,0 +DA:309,0 +DA:310,0 +DA:313,0 +LF:123 +LH:0 +BRDA:27,0,0,0 +BRDA:27,1,0,0 +BRDA:56,2,0,0 +BRDA:56,2,1,0 +BRDA:57,3,0,0 +BRDA:57,3,1,0 +BRDA:70,4,0,0 +BRDA:70,4,1,0 +BRDA:77,5,0,0 +BRDA:77,5,1,0 +BRDA:96,6,0,0 +BRDA:96,6,1,0 +BRDA:110,7,0,0 +BRDA:110,7,1,0 +BRDA:129,8,0,0 +BRDA:129,8,1,0 +BRDA:144,9,0,0 +BRDA:144,9,1,0 +BRDA:186,10,0,0 +BRDA:202,11,0,0 +BRDA:202,11,1,0 +BRDA:257,12,0,0 +BRDA:257,12,1,0 +BRDA:258,13,0,0 +BRDA:258,13,1,0 +BRDA:264,14,0,0 +BRDA:264,14,1,0 +BRDA:275,15,0,0 +BRDA:275,15,1,0 +BRDA:281,16,0,0 +BRDA:281,16,1,0 +BRDA:298,17,0,0 +BRDA:298,17,1,0 +BRDA:305,18,0,0 +BRDA:305,18,1,0 +BRF:35 +BRH:0 +end_of_record +TN: +SF:src/framework/query/QueryExecutor.ts +FN:14,(anonymous_0) +FN:20,(anonymous_1) +FN:58,(anonymous_2) +FN:63,(anonymous_3) +FN:65,(anonymous_4) +FN:71,(anonymous_5) +FN:79,(anonymous_6) +FN:83,(anonymous_7) +FN:89,(anonymous_8) +FN:93,(anonymous_9) +FN:99,(anonymous_10) +FN:103,(anonymous_11) +FN:113,(anonymous_12) +FN:121,(anonymous_13) +FN:145,(anonymous_14) +FN:160,(anonymous_15) +FN:173,(anonymous_16) +FN:184,(anonymous_17) +FN:194,(anonymous_18) +FN:201,(anonymous_19) +FN:215,(anonymous_20) +FN:231,(anonymous_21) +FN:244,(anonymous_22) +FN:266,(anonymous_23) +FN:269,(anonymous_24) +FN:286,(anonymous_25) +FN:325,(anonymous_26) +FN:340,(anonymous_27) +FN:352,(anonymous_28) +FN:355,(anonymous_29) +FN:356,(anonymous_30) +FN:362,(anonymous_31) +FN:367,(anonymous_32) +FN:471,(anonymous_33) +FN:493,(anonymous_34) +FN:500,(anonymous_35) +FN:516,(anonymous_36) +FN:523,(anonymous_37) +FN:542,(anonymous_38) +FN:559,(anonymous_39) +FN:569,(anonymous_40) +FN:586,(anonymous_41) +FN:591,(anonymous_42) +FN:596,(anonymous_43) +FN:600,(anonymous_44) +FN:612,(anonymous_45) +FNF:46 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +FNDA:0,(anonymous_44) +FNDA:0,(anonymous_45) +DA:12,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:21,0 +DA:22,0 +DA:25,0 +DA:26,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:59,0 +DA:60,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:72,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:80,0 +DA:81,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:90,0 +DA:91,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:100,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:109,0 +DA:114,0 +DA:116,0 +DA:118,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:128,0 +DA:130,0 +DA:131,0 +DA:135,0 +DA:138,0 +DA:139,0 +DA:142,0 +DA:146,0 +DA:149,0 +DA:150,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:161,0 +DA:163,0 +DA:165,0 +DA:166,0 +DA:170,0 +DA:176,0 +DA:180,0 +DA:181,0 +DA:186,0 +DA:187,0 +DA:189,0 +DA:190,0 +DA:195,0 +DA:197,0 +DA:198,0 +DA:201,0 +DA:203,0 +DA:205,0 +DA:209,0 +DA:210,0 +DA:212,0 +DA:213,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:225,0 +DA:228,0 +DA:229,0 +DA:231,0 +DA:232,0 +DA:234,0 +DA:236,0 +DA:237,0 +DA:240,0 +DA:248,0 +DA:249,0 +DA:251,0 +DA:252,0 +DA:256,0 +DA:259,0 +DA:262,0 +DA:265,0 +DA:266,0 +DA:270,0 +DA:272,0 +DA:275,0 +DA:277,0 +DA:278,0 +DA:279,0 +DA:280,0 +DA:282,0 +DA:286,0 +DA:287,0 +DA:288,0 +DA:293,0 +DA:296,0 +DA:297,0 +DA:298,0 +DA:299,0 +DA:300,0 +DA:301,0 +DA:304,0 +DA:308,0 +DA:310,0 +DA:311,0 +DA:315,0 +DA:318,0 +DA:319,0 +DA:322,0 +DA:326,0 +DA:327,0 +DA:329,0 +DA:332,0 +DA:337,0 +DA:338,0 +DA:339,0 +DA:340,0 +DA:344,0 +DA:347,0 +DA:348,0 +DA:353,0 +DA:355,0 +DA:356,0 +DA:357,0 +DA:363,0 +DA:366,0 +DA:367,0 +DA:368,0 +DA:372,0 +DA:374,0 +DA:375,0 +DA:378,0 +DA:380,0 +DA:383,0 +DA:387,0 +DA:390,0 +DA:394,0 +DA:397,0 +DA:401,0 +DA:404,0 +DA:407,0 +DA:410,0 +DA:413,0 +DA:416,0 +DA:419,0 +DA:422,0 +DA:425,0 +DA:428,0 +DA:431,0 +DA:434,0 +DA:437,0 +DA:440,0 +DA:443,0 +DA:446,0 +DA:449,0 +DA:452,0 +DA:457,0 +DA:460,0 +DA:463,0 +DA:466,0 +DA:467,0 +DA:472,0 +DA:473,0 +DA:475,0 +DA:477,0 +DA:479,0 +DA:481,0 +DA:483,0 +DA:485,0 +DA:487,0 +DA:489,0 +DA:494,0 +DA:495,0 +DA:496,0 +DA:497,0 +DA:501,0 +DA:502,0 +DA:504,0 +DA:506,0 +DA:508,0 +DA:510,0 +DA:512,0 +DA:517,0 +DA:519,0 +DA:520,0 +DA:523,0 +DA:524,0 +DA:525,0 +DA:526,0 +DA:528,0 +DA:530,0 +DA:531,0 +DA:533,0 +DA:534,0 +DA:538,0 +DA:543,0 +DA:544,0 +DA:546,0 +DA:548,0 +DA:549,0 +DA:552,0 +DA:553,0 +DA:556,0 +DA:561,0 +DA:564,0 +DA:566,0 +DA:570,0 +DA:572,0 +DA:573,0 +DA:575,0 +DA:576,0 +DA:577,0 +DA:579,0 +DA:582,0 +DA:587,0 +DA:588,0 +DA:592,0 +DA:593,0 +DA:597,0 +DA:601,0 +DA:602,0 +DA:604,0 +DA:613,0 +DA:614,0 +DA:615,0 +DA:617,0 +LF:256 +LH:0 +BRDA:31,0,0,0 +BRDA:31,0,1,0 +BRDA:31,1,0,0 +BRDA:31,1,1,0 +BRDA:33,2,0,0 +BRDA:33,2,1,0 +BRDA:41,3,0,0 +BRDA:41,3,1,0 +BRDA:48,4,0,0 +BRDA:48,4,1,0 +BRDA:48,5,0,0 +BRDA:48,5,1,0 +BRDA:48,5,2,0 +BRDA:67,6,0,0 +BRDA:67,6,1,0 +BRDA:73,7,0,0 +BRDA:73,7,1,0 +BRDA:81,8,0,0 +BRDA:81,8,1,0 +BRDA:85,9,0,0 +BRDA:85,9,1,0 +BRDA:85,10,0,0 +BRDA:85,10,1,0 +BRDA:91,11,0,0 +BRDA:91,11,1,0 +BRDA:95,12,0,0 +BRDA:95,12,1,0 +BRDA:95,13,0,0 +BRDA:95,13,1,0 +BRDA:103,14,0,0 +BRDA:103,14,1,0 +BRDA:105,15,0,0 +BRDA:105,15,1,0 +BRDA:114,16,0,0 +BRDA:114,16,1,0 +BRDA:152,17,0,0 +BRDA:152,17,1,0 +BRDA:152,18,0,0 +BRDA:152,18,1,0 +BRDA:186,19,0,0 +BRDA:186,19,1,0 +BRDA:203,20,0,0 +BRDA:203,20,1,0 +BRDA:203,21,0,0 +BRDA:203,21,1,0 +BRDA:210,22,0,0 +BRDA:210,22,1,0 +BRDA:210,23,0,0 +BRDA:210,23,1,0 +BRDA:279,24,0,0 +BRDA:279,24,1,0 +BRDA:299,25,0,0 +BRDA:299,25,1,0 +BRDA:327,26,0,0 +BRDA:327,26,1,0 +BRDA:327,26,2,0 +BRDA:327,26,3,0 +BRDA:327,26,4,0 +BRDA:340,27,0,0 +BRDA:340,27,1,0 +BRDA:366,28,0,0 +BRDA:366,28,1,0 +BRDA:372,29,0,0 +BRDA:372,29,1,0 +BRDA:380,30,0,0 +BRDA:380,30,1,0 +BRDA:380,30,2,0 +BRDA:380,30,3,0 +BRDA:380,30,4,0 +BRDA:380,30,5,0 +BRDA:380,30,6,0 +BRDA:380,30,7,0 +BRDA:380,30,8,0 +BRDA:380,30,9,0 +BRDA:380,30,10,0 +BRDA:380,30,11,0 +BRDA:380,30,12,0 +BRDA:380,30,13,0 +BRDA:380,30,14,0 +BRDA:380,30,15,0 +BRDA:380,30,16,0 +BRDA:380,30,17,0 +BRDA:380,30,18,0 +BRDA:380,30,19,0 +BRDA:380,30,20,0 +BRDA:380,30,21,0 +BRDA:380,30,22,0 +BRDA:380,30,23,0 +BRDA:380,30,24,0 +BRDA:380,30,25,0 +BRDA:380,30,26,0 +BRDA:380,30,27,0 +BRDA:380,30,28,0 +BRDA:380,30,29,0 +BRDA:380,30,30,0 +BRDA:404,31,0,0 +BRDA:404,31,1,0 +BRDA:407,32,0,0 +BRDA:407,32,1,0 +BRDA:410,33,0,0 +BRDA:410,33,1,0 +BRDA:419,34,0,0 +BRDA:419,34,1,0 +BRDA:422,35,0,0 +BRDA:422,35,1,0 +BRDA:425,36,0,0 +BRDA:425,36,1,0 +BRDA:425,36,2,0 +BRDA:428,37,0,0 +BRDA:428,37,1,0 +BRDA:431,38,0,0 +BRDA:431,38,1,0 +BRDA:434,39,0,0 +BRDA:434,39,1,0 +BRDA:437,40,0,0 +BRDA:437,40,1,0 +BRDA:440,41,0,0 +BRDA:440,41,1,0 +BRDA:440,41,2,0 +BRDA:453,42,0,0 +BRDA:453,42,1,0 +BRDA:475,43,0,0 +BRDA:475,43,1,0 +BRDA:475,44,0,0 +BRDA:475,44,1,0 +BRDA:477,45,0,0 +BRDA:477,45,1,0 +BRDA:477,45,2,0 +BRDA:477,45,3,0 +BRDA:477,45,4,0 +BRDA:477,45,5,0 +BRDA:494,46,0,0 +BRDA:494,46,1,0 +BRDA:495,47,0,0 +BRDA:495,47,1,0 +BRDA:496,48,0,0 +BRDA:496,48,1,0 +BRDA:502,49,0,0 +BRDA:502,49,1,0 +BRDA:504,50,0,0 +BRDA:504,50,1,0 +BRDA:504,50,2,0 +BRDA:504,50,3,0 +BRDA:519,51,0,0 +BRDA:519,51,1,0 +BRDA:530,52,0,0 +BRDA:530,52,1,0 +BRDA:531,53,0,0 +BRDA:531,53,1,0 +BRDA:533,54,0,0 +BRDA:533,54,1,0 +BRDA:534,55,0,0 +BRDA:534,55,1,0 +BRDA:548,56,0,0 +BRDA:548,56,1,0 +BRDA:548,57,0,0 +BRDA:548,57,1,0 +BRDA:552,58,0,0 +BRDA:552,58,1,0 +BRDA:552,59,0,0 +BRDA:552,59,1,0 +BRDA:570,60,0,0 +BRDA:570,60,1,0 +BRDA:576,61,0,0 +BRDA:576,61,1,0 +BRDA:576,62,0,0 +BRDA:576,62,1,0 +BRDA:601,63,0,0 +BRDA:601,63,1,0 +BRDA:614,64,0,0 +BRDA:614,64,1,0 +BRF:171 +BRH:0 +end_of_record +TN: +SF:src/framework/query/QueryOptimizer.ts +FN:14,(anonymous_0) +FN:29,(anonymous_1) +FN:38,(anonymous_2) +FN:58,(anonymous_3) +FN:69,(anonymous_4) +FN:100,(anonymous_5) +FN:101,(anonymous_6) +FN:104,(anonymous_7) +FN:116,(anonymous_8) +FN:121,(anonymous_9) +FN:131,(anonymous_10) +FN:138,(anonymous_11) +FN:160,(anonymous_12) +FN:172,(anonymous_13) +FN:209,(anonymous_14) +FN:217,(anonymous_15) +FN:239,(anonymous_16) +FN:247,(anonymous_17) +FNF:18 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:71,0 +DA:72,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:103,0 +DA:104,0 +DA:107,0 +DA:117,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:126,0 +DA:127,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:140,0 +DA:142,0 +DA:144,0 +DA:149,0 +DA:152,0 +DA:154,0 +DA:156,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:168,0 +DA:169,0 +DA:173,0 +DA:174,0 +DA:177,0 +DA:178,0 +DA:182,0 +DA:184,0 +DA:185,0 +DA:187,0 +DA:188,0 +DA:190,0 +DA:191,0 +DA:196,0 +DA:197,0 +DA:199,0 +DA:200,0 +DA:202,0 +DA:206,0 +DA:210,0 +DA:211,0 +DA:212,0 +DA:213,0 +DA:216,0 +DA:217,0 +DA:218,0 +DA:219,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:232,0 +DA:233,0 +DA:234,0 +DA:239,0 +DA:240,0 +DA:242,0 +DA:243,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:252,0 +LF:126 +LH:0 +BRDA:27,0,0,0 +BRDA:27,0,1,0 +BRDA:29,1,0,0 +BRDA:29,1,1,0 +BRDA:32,2,0,0 +BRDA:32,2,1,0 +BRDA:35,3,0,0 +BRDA:35,3,1,0 +BRDA:57,4,0,0 +BRDA:57,4,1,0 +BRDA:60,5,0,0 +BRDA:60,5,1,0 +BRDA:61,6,0,0 +BRDA:61,6,1,0 +BRDA:66,7,0,0 +BRDA:66,7,1,0 +BRDA:76,8,0,0 +BRDA:76,8,1,0 +BRDA:88,9,0,0 +BRDA:88,9,1,0 +BRDA:88,10,0,0 +BRDA:88,10,1,0 +BRDA:93,11,0,0 +BRDA:93,11,1,0 +BRDA:100,12,0,0 +BRDA:100,12,1,0 +BRDA:100,12,2,0 +BRDA:103,13,0,0 +BRDA:103,13,1,0 +BRDA:123,14,0,0 +BRDA:123,14,1,0 +BRDA:140,15,0,0 +BRDA:140,15,1,0 +BRDA:140,15,2,0 +BRDA:140,15,3,0 +BRDA:140,15,4,0 +BRDA:140,15,5,0 +BRDA:140,15,6,0 +BRDA:140,15,7,0 +BRDA:140,15,8,0 +BRDA:140,15,9,0 +BRDA:144,16,0,0 +BRDA:144,16,1,0 +BRDA:163,17,0,0 +BRDA:163,17,1,0 +BRDA:177,18,0,0 +BRDA:177,18,1,0 +BRDA:185,19,0,0 +BRDA:185,19,1,0 +BRDA:185,19,2,0 +BRDA:185,19,3,0 +BRDA:185,19,4,0 +BRDA:185,19,5,0 +BRDA:185,19,6,0 +BRDA:185,19,7,0 +BRDA:190,20,0,0 +BRDA:190,20,1,0 +BRDA:216,21,0,0 +BRDA:216,21,1,0 +BRDA:217,22,0,0 +BRDA:217,22,1,0 +BRDA:218,23,0,0 +BRDA:218,23,1,0 +BRDA:224,24,0,0 +BRDA:224,24,1,0 +BRDA:226,25,0,0 +BRDA:226,25,1,0 +BRDA:233,26,0,0 +BRDA:233,26,1,0 +BRDA:242,27,0,0 +BRDA:242,27,1,0 +BRDA:248,28,0,0 +BRDA:248,28,1,0 +BRF:73 +BRH:0 +end_of_record +TN: +SF:src/framework/relationships/LazyLoader.ts +FN:14,(anonymous_0) +FN:18,(anonymous_1) +FN:28,(anonymous_2) +FN:35,(anonymous_3) +FN:40,(anonymous_4) +FN:48,(anonymous_5) +FN:66,(anonymous_6) +FN:67,(anonymous_7) +FN:73,(anonymous_8) +FN:84,(anonymous_9) +FN:109,(anonymous_10) +FN:113,(anonymous_11) +FN:121,(anonymous_12) +FN:134,(anonymous_13) +FN:177,(anonymous_14) +FN:190,(anonymous_15) +FN:203,(anonymous_16) +FN:211,(anonymous_17) +FN:226,(anonymous_18) +FN:237,(anonymous_19) +FN:243,(anonymous_20) +FN:252,(anonymous_21) +FN:266,(anonymous_22) +FN:285,(anonymous_23) +FN:294,(anonymous_24) +FN:322,(anonymous_25) +FN:336,(anonymous_26) +FN:341,(anonymous_27) +FN:369,(anonymous_28) +FN:373,(anonymous_29) +FN:390,(anonymous_30) +FN:394,(anonymous_31) +FN:398,(anonymous_32) +FN:402,(anonymous_33) +FN:409,(anonymous_34) +FN:425,(anonymous_35) +FN:436,(anonymous_36) +FNF:37 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +DA:15,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:50,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:59,0 +DA:63,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:79,0 +DA:82,0 +DA:83,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:95,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:109,0 +DA:110,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:118,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:126,0 +DA:132,0 +DA:133,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:141,0 +DA:145,0 +DA:146,0 +DA:148,0 +DA:149,0 +DA:151,0 +DA:152,0 +DA:154,0 +DA:155,0 +DA:157,0 +DA:158,0 +DA:162,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:190,0 +DA:191,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:200,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:208,0 +DA:212,0 +DA:213,0 +DA:214,0 +DA:216,0 +DA:222,0 +DA:227,0 +DA:238,0 +DA:239,0 +DA:242,0 +DA:243,0 +DA:246,0 +DA:247,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:253,0 +DA:257,0 +DA:261,0 +DA:262,0 +DA:267,0 +DA:269,0 +DA:270,0 +DA:271,0 +DA:272,0 +DA:273,0 +DA:274,0 +DA:277,0 +DA:281,0 +DA:290,0 +DA:300,0 +DA:317,0 +DA:318,0 +DA:319,0 +DA:320,0 +DA:329,0 +DA:330,0 +DA:331,0 +DA:332,0 +DA:333,0 +DA:337,0 +DA:339,0 +DA:342,0 +DA:343,0 +DA:344,0 +DA:346,0 +DA:350,0 +DA:357,0 +DA:358,0 +DA:359,0 +DA:361,0 +DA:362,0 +DA:366,0 +DA:370,0 +DA:374,0 +DA:375,0 +DA:378,0 +DA:384,0 +DA:385,0 +DA:387,0 +DA:391,0 +DA:395,0 +DA:399,0 +DA:403,0 +DA:404,0 +DA:406,0 +DA:411,0 +DA:412,0 +DA:413,0 +DA:417,0 +DA:418,0 +DA:419,0 +DA:422,0 +DA:426,0 +DA:427,0 +DA:432,0 +DA:433,0 +DA:437,0 +DA:438,0 +DA:439,0 +LF:166 +LH:0 +BRDA:22,0,0,0 +BRDA:29,1,0,0 +BRDA:29,1,1,0 +BRDA:58,2,0,0 +BRDA:58,2,1,0 +BRDA:77,3,0,0 +BRDA:82,4,0,0 +BRDA:82,4,1,0 +BRDA:82,5,0,0 +BRDA:82,5,1,0 +BRDA:86,6,0,0 +BRDA:86,6,1,0 +BRDA:89,7,0,0 +BRDA:89,7,1,0 +BRDA:92,8,0,0 +BRDA:92,8,1,0 +BRDA:95,9,0,0 +BRDA:95,9,1,0 +BRDA:98,10,0,0 +BRDA:98,10,1,0 +BRDA:103,11,0,0 +BRDA:103,11,1,0 +BRDA:105,12,0,0 +BRDA:105,12,1,0 +BRDA:114,13,0,0 +BRDA:114,13,1,0 +BRDA:116,14,0,0 +BRDA:116,14,1,0 +BRDA:122,15,0,0 +BRDA:122,15,1,0 +BRDA:124,16,0,0 +BRDA:124,16,1,0 +BRDA:132,17,0,0 +BRDA:132,17,1,0 +BRDA:132,18,0,0 +BRDA:132,18,1,0 +BRDA:136,19,0,0 +BRDA:136,19,1,0 +BRDA:137,20,0,0 +BRDA:137,20,1,0 +BRDA:139,21,0,0 +BRDA:139,21,1,0 +BRDA:145,22,0,0 +BRDA:145,22,1,0 +BRDA:148,23,0,0 +BRDA:148,23,1,0 +BRDA:151,24,0,0 +BRDA:151,24,1,0 +BRDA:154,25,0,0 +BRDA:154,25,1,0 +BRDA:157,26,0,0 +BRDA:157,26,1,0 +BRDA:162,27,0,0 +BRDA:162,27,1,0 +BRDA:163,28,0,0 +BRDA:163,28,1,0 +BRDA:184,29,0,0 +BRDA:184,29,1,0 +BRDA:184,30,0,0 +BRDA:184,30,1,0 +BRDA:185,31,0,0 +BRDA:185,31,1,0 +BRDA:187,32,0,0 +BRDA:187,32,1,0 +BRDA:195,33,0,0 +BRDA:195,33,1,0 +BRDA:197,34,0,0 +BRDA:197,34,1,0 +BRDA:204,35,0,0 +BRDA:204,35,1,0 +BRDA:206,36,0,0 +BRDA:206,36,1,0 +BRDA:212,37,0,0 +BRDA:212,37,1,0 +BRDA:214,38,0,0 +BRDA:214,38,1,0 +BRDA:228,39,0,0 +BRDA:228,39,1,0 +BRDA:228,39,2,0 +BRDA:228,39,3,0 +BRDA:228,39,4,0 +BRDA:238,40,0,0 +BRDA:238,40,1,0 +BRDA:238,41,0,0 +BRDA:238,41,1,0 +BRDA:242,42,0,0 +BRDA:242,42,1,0 +BRDA:250,43,0,0 +BRDA:250,43,1,0 +BRDA:272,44,0,0 +BRDA:272,44,1,0 +BRDA:273,45,0,0 +BRDA:273,45,1,0 +BRDA:298,46,0,0 +BRDA:336,47,0,0 +BRDA:336,48,0,0 +BRDA:343,49,0,0 +BRDA:343,49,1,0 +BRDA:357,50,0,0 +BRDA:357,50,1,0 +BRDA:361,51,0,0 +BRDA:361,51,1,0 +BRDA:369,52,0,0 +BRDA:374,53,0,0 +BRDA:374,53,1,0 +BRDA:403,54,0,0 +BRDA:403,54,1,0 +BRDA:412,55,0,0 +BRDA:412,55,1,0 +BRDA:417,56,0,0 +BRDA:417,56,1,0 +BRDA:426,57,0,0 +BRDA:426,57,1,0 +BRF:113 +BRH:0 +end_of_record +TN: +SF:src/framework/relationships/RelationshipCache.ts +FN:26,(anonymous_0) +FN:39,(anonymous_1) +FN:50,(anonymous_2) +FN:73,(anonymous_3) +FN:101,(anonymous_4) +FN:108,(anonymous_5) +FN:124,(anonymous_6) +FN:139,(anonymous_7) +FN:154,(anonymous_8) +FN:165,(anonymous_9) +FN:170,(anonymous_10) +FN:183,(anonymous_11) +FN:188,(anonymous_12) +FN:203,(anonymous_13) +FN:205,(anonymous_14) +FN:210,(anonymous_15) +FN:224,(anonymous_16) +FN:237,(anonymous_17) +FN:268,(anonymous_18) +FN:270,(anonymous_19) +FN:276,(anonymous_20) +FN:286,(anonymous_21) +FN:288,(anonymous_22) +FN:294,(anonymous_23) +FN:303,(anonymous_24) +FN:321,(anonymous_25) +FN:326,(anonymous_26) +FN:335,(anonymous_27) +FNF:28 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +DA:21,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:47,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:80,0 +DA:83,0 +DA:84,0 +DA:87,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:140,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:155,0 +DA:156,0 +DA:166,0 +DA:175,0 +DA:177,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:189,0 +DA:198,0 +DA:199,0 +DA:204,0 +DA:205,0 +DA:211,0 +DA:212,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:220,0 +DA:225,0 +DA:227,0 +DA:228,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:253,0 +DA:254,0 +DA:256,0 +DA:257,0 +DA:260,0 +DA:269,0 +DA:270,0 +DA:272,0 +DA:277,0 +DA:278,0 +DA:283,0 +DA:287,0 +DA:288,0 +DA:290,0 +DA:295,0 +DA:298,0 +DA:300,0 +DA:304,0 +DA:306,0 +DA:307,0 +DA:309,0 +DA:310,0 +DA:311,0 +DA:312,0 +DA:316,0 +DA:317,0 +DA:322,0 +DA:323,0 +DA:328,0 +DA:329,0 +DA:330,0 +DA:332,0 +DA:336,0 +DA:337,0 +DA:339,0 +DA:340,0 +DA:341,0 +DA:342,0 +DA:345,0 +LF:133 +LH:0 +BRDA:26,0,0,0 +BRDA:26,1,0,0 +BRDA:42,2,0,0 +BRDA:42,2,1,0 +BRDA:53,3,0,0 +BRDA:53,3,1,0 +BRDA:60,4,0,0 +BRDA:60,4,1,0 +BRDA:80,5,0,0 +BRDA:80,5,1,0 +BRDA:83,6,0,0 +BRDA:83,6,1,0 +BRDA:113,7,0,0 +BRDA:113,7,1,0 +BRDA:128,8,0,0 +BRDA:128,8,1,0 +BRDA:128,9,0,0 +BRDA:128,9,1,0 +BRDA:143,10,0,0 +BRDA:143,10,1,0 +BRDA:185,11,0,0 +BRDA:185,11,1,0 +BRDA:215,12,0,0 +BRDA:215,12,1,0 +BRDA:253,13,0,0 +BRDA:253,13,1,0 +BRDA:254,14,0,0 +BRDA:254,14,1,0 +BRDA:256,15,0,0 +BRDA:256,15,1,0 +BRDA:261,16,0,0 +BRDA:261,16,1,0 +BRDA:263,17,0,0 +BRDA:263,17,1,0 +BRDA:269,18,0,0 +BRDA:269,18,1,0 +BRDA:277,19,0,0 +BRDA:277,19,1,0 +BRDA:277,20,0,0 +BRDA:277,20,1,0 +BRDA:287,21,0,0 +BRDA:287,21,1,0 +BRDA:295,22,0,0 +BRDA:295,22,1,0 +BRDA:295,23,0,0 +BRDA:295,23,1,0 +BRDA:295,23,2,0 +BRDA:304,24,0,0 +BRDA:304,24,1,0 +BRDA:310,25,0,0 +BRDA:310,25,1,0 +BRDA:316,26,0,0 +BRDA:316,26,1,0 +BRDA:323,27,0,0 +BRDA:323,27,1,0 +BRDA:337,28,0,0 +BRDA:337,28,1,0 +BRF:57 +BRH:0 +end_of_record +TN: +SF:src/framework/relationships/RelationshipManager.ts +FN:24,(anonymous_0) +FN:29,(anonymous_1) +FN:94,(anonymous_2) +FN:117,(anonymous_3) +FN:152,(anonymous_4) +FN:169,(anonymous_5) +FN:200,(anonymous_6) +FN:223,(anonymous_7) +FN:249,(anonymous_8) +FN:287,(anonymous_9) +FN:295,(anonymous_10) +FN:296,(anonymous_11) +FN:300,(anonymous_12) +FN:320,(anonymous_13) +FN:323,(anonymous_14) +FN:337,(anonymous_15) +FN:349,(anonymous_16) +FN:350,(anonymous_17) +FN:353,(anonymous_18) +FN:374,(anonymous_19) +FN:384,(anonymous_20) +FN:392,(anonymous_21) +FN:406,(anonymous_22) +FN:419,(anonymous_23) +FN:426,(anonymous_24) +FN:438,(anonymous_25) +FN:439,(anonymous_26) +FN:442,(anonymous_27) +FN:454,(anonymous_28) +FN:462,(anonymous_29) +FN:471,(anonymous_30) +FN:489,(anonymous_31) +FN:492,(anonymous_32) +FN:497,(anonymous_33) +FN:501,(anonymous_34) +FN:519,(anonymous_35) +FN:522,(anonymous_36) +FN:534,(anonymous_37) +FN:543,(anonymous_38) +FN:547,(anonymous_39) +FN:555,(anonymous_40) +FN:556,(anonymous_41) +FN:562,(anonymous_42) +FN:566,(anonymous_43) +FNF:44 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +DA:25,0 +DA:26,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:70,0 +DA:72,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:82,0 +DA:86,0 +DA:88,0 +DA:91,0 +DA:99,0 +DA:101,0 +DA:102,0 +DA:106,0 +DA:109,0 +DA:110,0 +DA:113,0 +DA:114,0 +DA:122,0 +DA:123,0 +DA:126,0 +DA:128,0 +DA:129,0 +DA:133,0 +DA:136,0 +DA:137,0 +DA:141,0 +DA:142,0 +DA:145,0 +DA:146,0 +DA:149,0 +DA:157,0 +DA:166,0 +DA:174,0 +DA:175,0 +DA:178,0 +DA:180,0 +DA:181,0 +DA:185,0 +DA:188,0 +DA:193,0 +DA:195,0 +DA:196,0 +DA:200,0 +DA:203,0 +DA:206,0 +DA:207,0 +DA:211,0 +DA:212,0 +DA:215,0 +DA:216,0 +DA:219,0 +DA:228,0 +DA:230,0 +DA:235,0 +DA:238,0 +DA:239,0 +DA:246,0 +DA:254,0 +DA:255,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:261,0 +DA:262,0 +DA:263,0 +DA:266,0 +DA:270,0 +DA:272,0 +DA:273,0 +DA:275,0 +DA:276,0 +DA:278,0 +DA:279,0 +DA:281,0 +DA:282,0 +DA:294,0 +DA:295,0 +DA:296,0 +DA:298,0 +DA:300,0 +DA:301,0 +DA:303,0 +DA:307,0 +DA:310,0 +DA:312,0 +DA:313,0 +DA:316,0 +DA:319,0 +DA:320,0 +DA:323,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:329,0 +DA:330,0 +DA:331,0 +DA:332,0 +DA:343,0 +DA:344,0 +DA:348,0 +DA:349,0 +DA:350,0 +DA:352,0 +DA:353,0 +DA:354,0 +DA:356,0 +DA:360,0 +DA:362,0 +DA:363,0 +DA:366,0 +DA:367,0 +DA:370,0 +DA:373,0 +DA:374,0 +DA:375,0 +DA:376,0 +DA:377,0 +DA:379,0 +DA:383,0 +DA:384,0 +DA:385,0 +DA:386,0 +DA:392,0 +DA:393,0 +DA:394,0 +DA:395,0 +DA:398,0 +DA:399,0 +DA:400,0 +DA:401,0 +DA:413,0 +DA:419,0 +DA:420,0 +DA:421,0 +DA:422,0 +DA:432,0 +DA:433,0 +DA:437,0 +DA:438,0 +DA:439,0 +DA:441,0 +DA:442,0 +DA:443,0 +DA:445,0 +DA:449,0 +DA:453,0 +DA:454,0 +DA:455,0 +DA:457,0 +DA:461,0 +DA:462,0 +DA:463,0 +DA:464,0 +DA:465,0 +DA:467,0 +DA:471,0 +DA:472,0 +DA:475,0 +DA:477,0 +DA:478,0 +DA:481,0 +DA:482,0 +DA:485,0 +DA:488,0 +DA:489,0 +DA:492,0 +DA:493,0 +DA:494,0 +DA:496,0 +DA:498,0 +DA:499,0 +DA:501,0 +DA:504,0 +DA:508,0 +DA:511,0 +DA:512,0 +DA:513,0 +DA:514,0 +DA:520,0 +DA:522,0 +DA:523,0 +DA:524,0 +DA:525,0 +DA:527,0 +DA:530,0 +DA:535,0 +DA:536,0 +DA:537,0 +DA:539,0 +DA:544,0 +DA:548,0 +DA:556,0 +DA:557,0 +DA:563,0 +DA:567,0 +LF:217 +LH:0 +BRDA:32,0,0,0 +BRDA:37,1,0,0 +BRDA:37,1,1,0 +BRDA:46,2,0,0 +BRDA:46,2,1,0 +BRDA:49,3,0,0 +BRDA:49,3,1,0 +BRDA:58,4,0,0 +BRDA:58,4,1,0 +BRDA:58,4,2,0 +BRDA:58,4,3,0 +BRDA:58,4,4,0 +BRDA:76,5,0,0 +BRDA:76,5,1,0 +BRDA:76,6,0,0 +BRDA:76,6,1,0 +BRDA:78,7,0,0 +BRDA:78,7,1,0 +BRDA:79,8,0,0 +BRDA:79,8,1,0 +BRDA:80,9,0,0 +BRDA:80,9,1,0 +BRDA:89,10,0,0 +BRDA:89,10,1,0 +BRDA:101,11,0,0 +BRDA:101,11,1,0 +BRDA:109,12,0,0 +BRDA:109,12,1,0 +BRDA:122,13,0,0 +BRDA:122,13,1,0 +BRDA:126,14,0,0 +BRDA:126,14,1,0 +BRDA:128,15,0,0 +BRDA:128,15,1,0 +BRDA:136,16,0,0 +BRDA:136,16,1,0 +BRDA:141,17,0,0 +BRDA:141,17,1,0 +BRDA:145,18,0,0 +BRDA:145,18,1,0 +BRDA:166,19,0,0 +BRDA:166,19,1,0 +BRDA:174,20,0,0 +BRDA:174,20,1,0 +BRDA:178,21,0,0 +BRDA:178,21,1,0 +BRDA:180,22,0,0 +BRDA:180,22,1,0 +BRDA:185,23,0,0 +BRDA:185,23,1,0 +BRDA:188,24,0,0 +BRDA:188,24,1,0 +BRDA:195,25,0,0 +BRDA:195,25,1,0 +BRDA:206,26,0,0 +BRDA:206,26,1,0 +BRDA:211,27,0,0 +BRDA:211,27,1,0 +BRDA:215,28,0,0 +BRDA:215,28,1,0 +BRDA:226,29,0,0 +BRDA:228,30,0,0 +BRDA:228,30,1,0 +BRDA:242,31,0,0 +BRDA:242,31,1,0 +BRDA:255,32,0,0 +BRDA:255,32,1,0 +BRDA:261,33,0,0 +BRDA:261,33,1,0 +BRDA:270,34,0,0 +BRDA:270,34,1,0 +BRDA:270,34,2,0 +BRDA:270,34,3,0 +BRDA:298,35,0,0 +BRDA:298,35,1,0 +BRDA:312,36,0,0 +BRDA:312,36,1,0 +BRDA:325,37,0,0 +BRDA:325,37,1,0 +BRDA:329,38,0,0 +BRDA:329,38,1,0 +BRDA:331,39,0,0 +BRDA:331,39,1,0 +BRDA:343,40,0,0 +BRDA:343,40,1,0 +BRDA:349,41,0,0 +BRDA:349,41,1,0 +BRDA:352,42,0,0 +BRDA:352,42,1,0 +BRDA:362,43,0,0 +BRDA:362,43,1,0 +BRDA:366,44,0,0 +BRDA:366,44,1,0 +BRDA:376,45,0,0 +BRDA:376,45,1,0 +BRDA:383,46,0,0 +BRDA:383,46,1,0 +BRDA:385,47,0,0 +BRDA:385,47,1,0 +BRDA:393,48,0,0 +BRDA:393,48,1,0 +BRDA:394,49,0,0 +BRDA:394,49,1,0 +BRDA:398,50,0,0 +BRDA:398,50,1,0 +BRDA:400,51,0,0 +BRDA:400,51,1,0 +BRDA:420,52,0,0 +BRDA:420,52,1,0 +BRDA:421,53,0,0 +BRDA:421,53,1,0 +BRDA:432,54,0,0 +BRDA:432,54,1,0 +BRDA:438,55,0,0 +BRDA:438,55,1,0 +BRDA:441,56,0,0 +BRDA:441,56,1,0 +BRDA:450,57,0,0 +BRDA:450,57,1,0 +BRDA:453,58,0,0 +BRDA:453,58,1,0 +BRDA:463,59,0,0 +BRDA:463,59,1,0 +BRDA:464,60,0,0 +BRDA:464,60,1,0 +BRDA:477,61,0,0 +BRDA:477,61,1,0 +BRDA:481,62,0,0 +BRDA:481,62,1,0 +BRDA:493,63,0,0 +BRDA:493,63,1,0 +BRDA:494,64,0,0 +BRDA:494,64,1,0 +BRDA:504,65,0,0 +BRDA:504,65,1,0 +BRDA:511,66,0,0 +BRDA:511,66,1,0 +BRDA:513,67,0,0 +BRDA:513,67,1,0 +BRDA:524,68,0,0 +BRDA:524,68,1,0 +BRDA:535,69,0,0 +BRDA:535,69,1,0 +BRDA:537,70,0,0 +BRDA:537,70,1,0 +BRF:145 +BRH:0 +end_of_record +TN: +SF:src/framework/services/OrbitDBService.ts +FN:25,(anonymous_0) +FN:29,(anonymous_1) +FN:33,(anonymous_2) +FN:37,(anonymous_3) +FN:43,(anonymous_4) +FN:51,(anonymous_5) +FN:55,(anonymous_6) +FN:59,(anonymous_7) +FN:65,(anonymous_8) +FN:69,(anonymous_9) +FN:73,(anonymous_10) +FN:89,(anonymous_11) +FN:95,(anonymous_12) +FNF:13 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +DA:26,0 +DA:30,0 +DA:34,0 +DA:38,0 +DA:39,0 +DA:44,0 +DA:52,0 +DA:56,0 +DA:60,0 +DA:61,0 +DA:66,0 +DA:70,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:86,0 +DA:92,0 +DA:96,0 +LF:22 +LH:0 +BRDA:38,0,0,0 +BRDA:38,0,1,0 +BRDA:60,1,0,0 +BRDA:60,1,1,0 +BRDA:75,2,0,0 +BRDA:75,2,1,0 +BRF:6 +BRH:0 +end_of_record +TN: +SF:src/framework/sharding/ShardManager.ts +FN:16,(anonymous_0) +FN:20,(anonymous_1) +FN:52,(anonymous_2) +FN:67,(anonymous_3) +FN:71,(anonymous_4) +FN:79,(anonymous_5) +FN:84,(anonymous_6) +FN:104,(anonymous_7) +FN:113,(anonymous_8) +FN:125,(anonymous_9) +FN:130,(anonymous_10) +FN:150,(anonymous_11) +FN:181,(anonymous_12) +FN:200,(anonymous_13) +FN:218,(anonymous_14) +FN:237,(anonymous_15) +FN:249,(anonymous_16) +FN:269,(anonymous_17) +FN:278,(anonymous_18) +FN:286,(anonymous_19) +FN:291,(anonymous_20) +FNF:21 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +DA:13,0 +DA:14,0 +DA:17,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:48,0 +DA:49,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:63,0 +DA:64,0 +DA:68,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:80,0 +DA:81,0 +DA:89,0 +DA:91,0 +DA:94,0 +DA:97,0 +DA:100,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:115,0 +DA:116,0 +DA:119,0 +DA:120,0 +DA:122,0 +DA:127,0 +DA:135,0 +DA:136,0 +DA:139,0 +DA:141,0 +DA:151,0 +DA:152,0 +DA:155,0 +DA:158,0 +DA:159,0 +DA:161,0 +DA:162,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:168,0 +DA:170,0 +DA:171,0 +DA:176,0 +DA:178,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:188,0 +DA:189,0 +DA:191,0 +DA:193,0 +DA:195,0 +DA:196,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:207,0 +DA:208,0 +DA:210,0 +DA:211,0 +DA:213,0 +DA:214,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:225,0 +DA:226,0 +DA:228,0 +DA:229,0 +DA:231,0 +DA:232,0 +DA:241,0 +DA:242,0 +DA:243,0 +DA:246,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:253,0 +DA:254,0 +DA:258,0 +DA:261,0 +DA:262,0 +DA:265,0 +DA:270,0 +DA:271,0 +DA:272,0 +DA:275,0 +DA:278,0 +DA:287,0 +DA:292,0 +DA:294,0 +DA:295,0 +DA:297,0 +LF:117 +LH:0 +BRDA:23,0,0,0 +BRDA:25,1,0,0 +BRDA:25,1,1,0 +BRDA:54,2,0,0 +BRDA:54,2,1,0 +BRDA:54,3,0,0 +BRDA:54,3,1,0 +BRDA:59,4,0,0 +BRDA:59,4,1,0 +BRDA:68,5,0,0 +BRDA:68,5,1,0 +BRDA:73,6,0,0 +BRDA:73,6,1,0 +BRDA:73,7,0,0 +BRDA:73,7,1,0 +BRDA:73,7,2,0 +BRDA:81,8,0,0 +BRDA:81,8,1,0 +BRDA:89,9,0,0 +BRDA:89,9,1,0 +BRDA:89,9,2,0 +BRDA:89,9,3,0 +BRDA:135,10,0,0 +BRDA:135,10,1,0 +BRDA:151,11,0,0 +BRDA:151,11,1,0 +BRDA:183,12,0,0 +BRDA:183,12,1,0 +BRDA:202,13,0,0 +BRDA:202,13,1,0 +BRDA:220,14,0,0 +BRDA:220,14,1,0 +BRDA:242,15,0,0 +BRDA:242,15,1,0 +BRDA:271,16,0,0 +BRDA:271,16,1,0 +BRF:36 +BRH:0 +end_of_record +TN: +SF:src/framework/types/models.ts +FN:40,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:41,0 +DA:42,0 +DA:43,0 +LF:3 +LH:0 +BRF:0 +BRH:0 +end_of_record diff --git a/coverage/prettify.css b/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/prettify.js b/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/sort-arrow-sprite.png b/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/sorter.js b/coverage/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/coverage/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting);