diff --git a/.gitignore b/.gitignore index b62507c..11cf3d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ network.txt node_modules/ dist/ -system.txt -.DS_Store \ No newline at end of file +.DS_Store +coverage/ \ No newline at end of file diff --git a/.lintstagedrc b/.lintstagedrc index dae4adf..970fbfb 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,5 +1,5 @@ { - "*.{js,ts}": [ + "src/**/*.{js,ts}": [ "prettier --write", "eslint --fix", "npm run build" diff --git a/coverage/base.css b/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/favicon.png b/coverage/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/favicon.png differ diff --git a/coverage/framework/DebrosFramework.ts.html b/coverage/framework/DebrosFramework.ts.html new file mode 100644 index 0000000..26507d0 --- /dev/null +++ b/coverage/framework/DebrosFramework.ts.html @@ -0,0 +1,2386 @@ + + + + + + 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 0000000..c1525b8 Binary files /dev/null and b/coverage/lcov-report/favicon.png differ diff --git a/coverage/lcov-report/framework/DebrosFramework.ts.html b/coverage/lcov-report/framework/DebrosFramework.ts.html new file mode 100644 index 0000000..3cf0246 --- /dev/null +++ b/coverage/lcov-report/framework/DebrosFramework.ts.html @@ -0,0 +1,2386 @@ + + + + + + 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 0000000..6ed6831 Binary files /dev/null and b/coverage/lcov-report/sort-arrow-sprite.png differ 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 0000000..6ed6831 Binary files /dev/null and b/coverage/sort-arrow-sprite.png differ 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); diff --git a/debug_fields.js b/debug_fields.js new file mode 100644 index 0000000..f92f1b1 --- /dev/null +++ b/debug_fields.js @@ -0,0 +1,44 @@ +// Simple debug script to test field defaults +const { execSync } = require('child_process'); + +// Run a small test using jest directly +const testCode = ` +import { BaseModel } from './src/framework/models/BaseModel'; +import { Model, Field } from './src/framework/models/decorators'; + +@Model({ + scope: 'global', + type: 'docstore' +}) +class TestUser extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + @Field({ type: 'number', required: false, default: 0 }) + score: number; + + @Field({ type: 'boolean', required: false, default: true }) + isActive: boolean; +} + +// Debug the fields +console.log('TestUser.fields:', TestUser.fields); +console.log('TestUser.fields size:', TestUser.fields?.size); + +if (TestUser.fields) { + for (const [fieldName, fieldConfig] of TestUser.fields) { + console.log(\`Field: \${fieldName}, Config:\`, fieldConfig); + } +} + +// Test instance creation +const user = new TestUser(); +console.log('User instance score:', user.score); +console.log('User instance isActive:', user.isActive); + +// Check private fields +console.log('User _score:', (user as any)._score); +console.log('User _isActive:', (user as any)._isActive); +`; + +console.log('Test code created for debugging...'); \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..b2d6de3 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b28211a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,41 @@ +# Website + +This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. + +## Installation + +```bash +yarn +``` + +## Local Development + +```bash +yarn start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +## Build + +```bash +yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +## Deployment + +Using SSH: + +```bash +USE_SSH=true yarn deploy +``` + +Not using SSH: + +```bash +GIT_USER= yarn deploy +``` + +If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/docs/blog/2019-05-28-first-blog-post.md b/docs/blog/2019-05-28-first-blog-post.md new file mode 100644 index 0000000..d3032ef --- /dev/null +++ b/docs/blog/2019-05-28-first-blog-post.md @@ -0,0 +1,12 @@ +--- +slug: first-blog-post +title: First Blog Post +authors: [slorber, yangshun] +tags: [hola, docusaurus] +--- + +Lorem ipsum dolor sit amet... + + + +...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/docs/blog/2019-05-29-long-blog-post.md b/docs/blog/2019-05-29-long-blog-post.md new file mode 100644 index 0000000..eb4435d --- /dev/null +++ b/docs/blog/2019-05-29-long-blog-post.md @@ -0,0 +1,44 @@ +--- +slug: long-blog-post +title: Long Blog Post +authors: yangshun +tags: [hello, docusaurus] +--- + +This is the summary of a very long blog post, + +Use a `` comment to limit blog post size in the list view. + + + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/docs/blog/2021-08-01-mdx-blog-post.mdx b/docs/blog/2021-08-01-mdx-blog-post.mdx new file mode 100644 index 0000000..0c4b4a4 --- /dev/null +++ b/docs/blog/2021-08-01-mdx-blog-post.mdx @@ -0,0 +1,24 @@ +--- +slug: mdx-blog-post +title: MDX Blog Post +authors: [slorber] +tags: [docusaurus] +--- + +Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/). + +:::tip + +Use the power of React to create interactive blog posts. + +::: + +{/* truncate */} + +For example, use JSX to create an interactive button: + +```js + +``` + + diff --git a/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg b/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg new file mode 100644 index 0000000..11bda09 Binary files /dev/null and b/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg differ diff --git a/docs/blog/2021-08-26-welcome/index.md b/docs/blog/2021-08-26-welcome/index.md new file mode 100644 index 0000000..349ea07 --- /dev/null +++ b/docs/blog/2021-08-26-welcome/index.md @@ -0,0 +1,29 @@ +--- +slug: welcome +title: Welcome +authors: [slorber, yangshun] +tags: [facebook, hello, docusaurus] +--- + +[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). + +Here are a few tips you might find useful. + + + +Simply add Markdown files (or folders) to the `blog` directory. + +Regular blog authors can be added to `authors.yml`. + +The blog post date can be extracted from filenames, such as: + +- `2019-05-30-welcome.md` +- `2019-05-30-welcome/index.md` + +A blog post folder can be convenient to co-locate blog post images: + +![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) + +The blog supports tags as well! + +**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml new file mode 100644 index 0000000..0fd3987 --- /dev/null +++ b/docs/blog/authors.yml @@ -0,0 +1,25 @@ +yangshun: + name: Yangshun Tay + title: Ex-Meta Staff Engineer, Co-founder GreatFrontEnd + url: https://linkedin.com/in/yangshun + image_url: https://github.com/yangshun.png + page: true + socials: + x: yangshunz + linkedin: yangshun + github: yangshun + newsletter: https://www.greatfrontend.com + +slorber: + name: Sébastien Lorber + title: Docusaurus maintainer + url: https://sebastienlorber.com + image_url: https://github.com/slorber.png + page: + # customize the url of the author page at /blog/authors/ + permalink: '/all-sebastien-lorber-articles' + socials: + x: sebastienlorber + linkedin: sebastienlorber + github: slorber + newsletter: https://thisweekinreact.com diff --git a/docs/blog/tags.yml b/docs/blog/tags.yml new file mode 100644 index 0000000..bfaa778 --- /dev/null +++ b/docs/blog/tags.yml @@ -0,0 +1,19 @@ +facebook: + label: Facebook + permalink: /facebook + description: Facebook tag description + +hello: + label: Hello + permalink: /hello + description: Hello tag description + +docusaurus: + label: Docusaurus + permalink: /docusaurus + description: Docusaurus tag description + +hola: + label: Hola + permalink: /hola + description: Hola tag description diff --git a/docs/docs/api/debros-framework.md b/docs/docs/api/debros-framework.md new file mode 100644 index 0000000..8ab5f50 --- /dev/null +++ b/docs/docs/api/debros-framework.md @@ -0,0 +1,629 @@ +--- +sidebar_position: 2 +--- + +# DebrosFramework Class + +The `DebrosFramework` class is the main entry point for the framework. It handles initialization, configuration, and lifecycle management of all framework components. + +## Class Definition + +```typescript +class DebrosFramework { + constructor(config?: Partial); + + // Initialization + async initialize( + orbitDBService?: any, + ipfsService?: any, + overrideConfig?: Partial, + ): Promise; + + // Lifecycle management + async start(): Promise; + async stop(): Promise; + + // Component access + getDatabaseManager(): DatabaseManager; + getShardManager(): ShardManager; + getQueryExecutor(): QueryExecutor; + getRelationshipManager(): RelationshipManager; + getMigrationManager(): MigrationManager; + + // Configuration + getConfig(): DebrosFrameworkConfig; + updateConfig(config: Partial): void; + + // Status + isInitialized(): boolean; + isRunning(): boolean; + getStatus(): FrameworkStatus; +} +``` + +## Constructor + +### new DebrosFramework(config?) + +Creates a new instance of the DebrosFramework. + +**Parameters:** + +- `config` (optional): Partial framework configuration + +**Example:** + +```typescript +import { DebrosFramework } from 'debros-framework'; + +// Default configuration +const framework = new DebrosFramework(); + +// Custom configuration +const framework = new DebrosFramework({ + cache: { + enabled: true, + maxSize: 5000, + ttl: 600000, // 10 minutes + }, + queryOptimization: { + enabled: true, + cacheQueries: true, + parallelExecution: true, + }, + development: { + logLevel: 'debug', + enableMetrics: true, + }, +}); +``` + +## Initialization Methods + +### initialize(orbitDBService?, ipfsService?, overrideConfig?) + +Initializes the framework with OrbitDB and IPFS services. + +**Parameters:** + +- `orbitDBService` (optional): OrbitDB service instance +- `ipfsService` (optional): IPFS service instance +- `overrideConfig` (optional): Configuration overrides + +**Returns:** `Promise` + +**Throws:** `DebrosFrameworkError` if initialization fails + +**Example:** + +```typescript +import { DebrosFramework } from 'debros-framework'; +import { setupOrbitDB, setupIPFS } from './services'; + +async function initializeFramework() { + const framework = new DebrosFramework(); + + // Setup external services + const orbitDBService = await setupOrbitDB(); + const ipfsService = await setupIPFS(); + + // Initialize framework + await framework.initialize(orbitDBService, ipfsService, { + cache: { enabled: true }, + development: { logLevel: 'info' }, + }); + + console.log('Framework initialized successfully'); + return framework; +} +``` + +**With existing services:** + +```typescript +async function initializeWithExistingServices() { + const framework = new DebrosFramework(); + + // Use existing services from your application + const existingOrbitDB = app.orbitDB; + const existingIPFS = app.ipfs; + + await framework.initialize(existingOrbitDB, existingIPFS); + + return framework; +} +``` + +**Environment-specific configuration:** + +```typescript +async function initializeForEnvironment(env: 'development' | 'production' | 'test') { + const framework = new DebrosFramework(); + + const configs = { + development: { + development: { logLevel: 'debug', enableMetrics: true }, + cache: { enabled: true, maxSize: 1000 }, + }, + production: { + development: { logLevel: 'error', enableMetrics: false }, + cache: { enabled: true, maxSize: 10000 }, + queryOptimization: { enabled: true, parallelExecution: true }, + }, + test: { + development: { logLevel: 'warn', enableMetrics: false }, + cache: { enabled: false }, + }, + }; + + await framework.initialize(await setupOrbitDB(env), await setupIPFS(env), configs[env]); + + return framework; +} +``` + +## Lifecycle Methods + +### start() + +Starts the framework and all its components. + +**Returns:** `Promise` + +**Example:** + +```typescript +const framework = new DebrosFramework(); +await framework.initialize(orbitDBService, ipfsService); +await framework.start(); + +console.log('Framework is now running'); +``` + +### stop() + +Gracefully stops the framework and cleans up resources. + +**Returns:** `Promise` + +**Example:** + +```typescript +// Graceful shutdown +process.on('SIGINT', async () => { + console.log('Shutting down framework...'); + await framework.stop(); + console.log('Framework stopped'); + process.exit(0); +}); + +// Manual stop +await framework.stop(); +``` + +## Component Access Methods + +### getDatabaseManager() + +Returns the database manager instance. + +**Returns:** `DatabaseManager` + +**Example:** + +```typescript +const databaseManager = framework.getDatabaseManager(); + +// Get user database +const userDB = await databaseManager.getUserDatabase('user123', 'Post'); + +// Get global database +const globalDB = await databaseManager.getGlobalDatabase('User'); +``` + +### getShardManager() + +Returns the shard manager instance. + +**Returns:** `ShardManager` + +**Example:** + +```typescript +const shardManager = framework.getShardManager(); + +// Get shard for data +const shard = shardManager.getShardForData('Post', { userId: 'user123' }); + +// Get all shards for model +const shards = shardManager.getShardsForModel('Post'); +``` + +### getQueryExecutor() + +Returns the query executor instance. + +**Returns:** `QueryExecutor` + +**Example:** + +```typescript +const queryExecutor = framework.getQueryExecutor(); + +// Execute custom query +const results = await queryExecutor.execute(queryBuilder, { + timeout: 10000, + useCache: true, +}); +``` + +### getRelationshipManager() + +Returns the relationship manager instance. + +**Returns:** `RelationshipManager` + +**Example:** + +```typescript +const relationshipManager = framework.getRelationshipManager(); + +// Load relationship +const posts = await relationshipManager.loadRelationship(user, 'posts', { eager: true }); +``` + +### getMigrationManager() + +Returns the migration manager instance. + +**Returns:** `MigrationManager` + +**Example:** + +```typescript +const migrationManager = framework.getMigrationManager(); + +// Run pending migrations +await migrationManager.runPendingMigrations(); + +// Get migration status +const status = migrationManager.getActiveMigrations(); +``` + +## Configuration Methods + +### getConfig() + +Returns the current framework configuration. + +**Returns:** `DebrosFrameworkConfig` + +**Example:** + +```typescript +const config = framework.getConfig(); +console.log('Cache enabled:', config.cache?.enabled); +console.log('Log level:', config.development?.logLevel); +``` + +### updateConfig(config) + +Updates the framework configuration. + +**Parameters:** + +- `config`: Partial configuration to merge + +**Example:** + +```typescript +// Update cache settings +framework.updateConfig({ + cache: { + enabled: true, + maxSize: 2000, + ttl: 300000, + }, +}); + +// Update development settings +framework.updateConfig({ + development: { + logLevel: 'debug', + }, +}); +``` + +## Status Methods + +### isInitialized() + +Checks if the framework has been initialized. + +**Returns:** `boolean` + +**Example:** + +```typescript +if (!framework.isInitialized()) { + await framework.initialize(orbitDBService, ipfsService); +} +``` + +### isRunning() + +Checks if the framework is currently running. + +**Returns:** `boolean` + +**Example:** + +```typescript +if (framework.isRunning()) { + console.log('Framework is active'); +} else { + await framework.start(); +} +``` + +### getStatus() + +Returns detailed framework status information. + +**Returns:** `FrameworkStatus` + +**Example:** + +```typescript +const status = framework.getStatus(); + +console.log('Framework Status:', { + initialized: status.initialized, + running: status.running, + uptime: status.uptime, + version: status.version, + components: status.components, + metrics: status.metrics, +}); +``` + +## Configuration Interface + +### DebrosFrameworkConfig + +```typescript +interface DebrosFrameworkConfig { + // Cache configuration + cache?: { + enabled?: boolean; // Enable/disable caching + maxSize?: number; // Maximum cache entries + ttl?: number; // Time to live in milliseconds + cleanupInterval?: number; // Cleanup interval in milliseconds + }; + + // Query optimization + queryOptimization?: { + enabled?: boolean; // Enable query optimization + cacheQueries?: boolean; // Cache query results + parallelExecution?: boolean; // Execute queries in parallel + maxConcurrent?: number; // Max concurrent queries + timeout?: number; // Query timeout in milliseconds + }; + + // Automatic pinning + automaticPinning?: { + enabled?: boolean; // Enable automatic pinning + strategy?: 'popularity' | 'fixed' | 'tiered'; // Pinning strategy + factor?: number; // Pinning factor + maxPins?: number; // Maximum pins + evaluationInterval?: number; // Evaluation interval in milliseconds + }; + + // PubSub configuration + pubsub?: { + enabled?: boolean; // Enable PubSub + bufferSize?: number; // Event buffer size + maxRetries?: number; // Max retry attempts + retryDelay?: number; // Retry delay in milliseconds + }; + + // Sharding configuration + sharding?: { + defaultStrategy?: 'hash' | 'range' | 'user'; // Default sharding strategy + defaultCount?: number; // Default shard count + maxShards?: number; // Maximum shards per model + }; + + // Development settings + development?: { + logLevel?: 'debug' | 'info' | 'warn' | 'error'; // Logging level + enableMetrics?: boolean; // Enable metrics collection + enableProfiling?: boolean; // Enable performance profiling + mockMode?: boolean; // Enable mock mode for testing + }; + + // Network configuration + network?: { + connectionTimeout?: number; // Connection timeout in milliseconds + retryAttempts?: number; // Number of retry attempts + retryDelay?: number; // Delay between retries in milliseconds + }; +} +``` + +### FrameworkStatus + +```typescript +interface FrameworkStatus { + initialized: boolean; + running: boolean; + version: string; + uptime: number; + + components: { + databaseManager: ComponentStatus; + shardManager: ComponentStatus; + queryExecutor: ComponentStatus; + relationshipManager: ComponentStatus; + migrationManager: ComponentStatus; + pinningManager: ComponentStatus; + pubsubManager: ComponentStatus; + }; + + metrics?: { + queriesExecuted: number; + cacheHits: number; + cacheMisses: number; + averageQueryTime: number; + activeConnections: number; + memoryUsage: number; + }; +} + +interface ComponentStatus { + status: 'active' | 'inactive' | 'error'; + lastActivity?: number; + errorCount?: number; + lastError?: string; +} +``` + +## Error Handling + +### Common Errors + +```typescript +try { + await framework.initialize(orbitDBService, ipfsService); +} catch (error) { + if (error instanceof DebrosFrameworkError) { + switch (error.code) { + case 'INITIALIZATION_FAILED': + console.error('Framework initialization failed:', error.message); + break; + case 'SERVICE_NOT_AVAILABLE': + console.error('Required service not available:', error.details); + break; + case 'CONFIGURATION_ERROR': + console.error('Configuration error:', error.message); + break; + default: + console.error('Unknown framework error:', error); + } + } else { + console.error('Unexpected error:', error); + } +} +``` + +## Complete Example + +### Production Application Setup + +```typescript +import { DebrosFramework } from 'debros-framework'; +import { setupOrbitDB, setupIPFS } from './services'; +import { User, Post, Comment } from './models'; + +class Application { + private framework: DebrosFramework; + + async initialize() { + // Create framework with production configuration + this.framework = new DebrosFramework({ + cache: { + enabled: true, + maxSize: 10000, + ttl: 600000, // 10 minutes + }, + queryOptimization: { + enabled: true, + cacheQueries: true, + parallelExecution: true, + maxConcurrent: 20, + timeout: 30000, + }, + automaticPinning: { + enabled: true, + strategy: 'popularity', + factor: 3, + maxPins: 1000, + }, + development: { + logLevel: 'info', + enableMetrics: true, + }, + }); + + // Setup services + const orbitDBService = await setupOrbitDB(); + const ipfsService = await setupIPFS(); + + // Initialize framework + await this.framework.initialize(orbitDBService, ipfsService); + await this.framework.start(); + + console.log('Application initialized successfully'); + + // Log status + const status = this.framework.getStatus(); + console.log('Framework status:', status); + } + + async shutdown() { + if (this.framework) { + await this.framework.stop(); + console.log('Application shutdown complete'); + } + } + + getFramework(): DebrosFramework { + return this.framework; + } +} + +// Usage +const app = new Application(); + +async function main() { + try { + await app.initialize(); + + // Your application logic here + const framework = app.getFramework(); + + // Create some data + const user = await User.create({ + username: 'alice', + email: 'alice@example.com', + }); + + const post = await Post.create({ + title: 'Hello DebrosFramework', + content: 'This is my first post!', + authorId: user.id, + }); + + console.log('Created user and post successfully'); + } catch (error) { + console.error('Application failed:', error); + } +} + +// Graceful shutdown +process.on('SIGINT', async () => { + console.log('Received SIGINT, shutting down gracefully...'); + await app.shutdown(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('Received SIGTERM, shutting down gracefully...'); + await app.shutdown(); + process.exit(0); +}); + +main().catch(console.error); +``` + +This comprehensive API reference for the DebrosFramework class covers all public methods, configuration options, and usage patterns for initializing and managing the framework in your applications. diff --git a/docs/docs/api/network-api.md b/docs/docs/api/network-api.md new file mode 100644 index 0000000..1f18ffc --- /dev/null +++ b/docs/docs/api/network-api.md @@ -0,0 +1,269 @@ +--- +sidebar_position: 1 +--- + +# API Reference Overview + +The @debros/network API provides a comprehensive set of functions for building decentralized applications with familiar database operations built on OrbitDB and IPFS. + +## Core Database Functions + +### Primary Operations + +| Function | Description | Parameters | Returns | +| ------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------- | ----------------------------- | +| `initDB(connectionId?)` | Initialize database connection | `connectionId?: string` | `Promise` | +| `create(collection, id, data, options?)` | Create a new document | `collection: string, id: string, data: T, options?: CreateOptions` | `Promise` | +| `get(collection, id, options?)` | Get document by ID | `collection: string, id: string, options?: GetOptions` | `Promise` | +| `update(collection, id, data, options?)` | Update existing document | `collection: string, id: string, data: Partial, options?: UpdateOptions` | `Promise` | +| `remove(collection, id, options?)` | Delete document | `collection: string, id: string, options?: RemoveOptions` | `Promise` | +| `list(collection, options?)` | List documents with pagination | `collection: string, options?: ListOptions` | `Promise>` | +| `query(collection, filter, options?)` | Query documents with filtering | `collection: string, filter: FilterFunction, options?: QueryOptions` | `Promise>` | +| `stopDB()` | Stop database service | None | `Promise` | + +## File Operations + +| Function | Description | Parameters | Returns | +| ---------------------------- | ----------------------- | ----------------------------------------------------- | --------------------------- | +| `uploadFile(data, options?)` | Upload file to IPFS | `data: Buffer \| Uint8Array, options?: UploadOptions` | `Promise` | +| `getFile(cid, options?)` | Retrieve file from IPFS | `cid: string, options?: FileGetOptions` | `Promise` | +| `deleteFile(cid, options?)` | Delete file from IPFS | `cid: string, options?: FileDeleteOptions` | `Promise` | + +## Schema and Validation + +| Function | Description | Parameters | Returns | +| ---------------------------------- | ------------------------ | ---------------------------------------------- | ------- | +| `defineSchema(collection, schema)` | Define validation schema | `collection: string, schema: SchemaDefinition` | `void` | + +## Transaction System + +| Function | Description | Parameters | Returns | +| ---------------------------------- | ---------------------- | -------------------------- | ---------------------------- | +| `createTransaction(connectionId?)` | Create new transaction | `connectionId?: string` | `Transaction` | +| `commitTransaction(transaction)` | Execute transaction | `transaction: Transaction` | `Promise` | + +## Event System + +| Function | Description | Parameters | Returns | +| ---------------------------- | ------------------- | ------------------------------------------- | --------------------- | +| `subscribe(event, callback)` | Subscribe to events | `event: EventType, callback: EventCallback` | `UnsubscribeFunction` | + +## Connection Management + +| Function | Description | Parameters | Returns | +| ------------------------------- | ------------------------- | ---------------------- | ------------------ | +| `closeConnection(connectionId)` | Close specific connection | `connectionId: string` | `Promise` | + +## Performance and Indexing + +| Function | Description | Parameters | Returns | +| ------------------------------------------ | --------------------- | ----------------------------------------------------------- | ------------------ | +| `createIndex(collection, field, options?)` | Create database index | `collection: string, field: string, options?: IndexOptions` | `Promise` | + +## Type Definitions + +### Core Types + +```typescript +interface CreateResult { + id: string; + success: boolean; + timestamp: number; +} + +interface UpdateResult { + id: string; + success: boolean; + modified: boolean; + timestamp: number; +} + +interface PaginatedResult { + documents: T[]; + total: number; + hasMore: boolean; + offset: number; + limit: number; +} + +interface FileUploadResult { + cid: string; + size: number; + filename?: string; + metadata?: Record; +} + +interface FileResult { + data: Buffer; + metadata?: Record; + size: number; +} +``` + +### Options Types + +```typescript +interface CreateOptions { + connectionId?: string; + validate?: boolean; + overwrite?: boolean; +} + +interface GetOptions { + connectionId?: string; + includeMetadata?: boolean; +} + +interface UpdateOptions { + connectionId?: string; + validate?: boolean; + upsert?: boolean; +} + +interface ListOptions { + connectionId?: string; + limit?: number; + offset?: number; + sort?: { + field: string; + order: 'asc' | 'desc'; + }; +} + +interface QueryOptions extends ListOptions { + includeScore?: boolean; +} + +interface UploadOptions { + filename?: string; + metadata?: Record; + connectionId?: string; +} +``` + +### Store Types + +```typescript +enum StoreType { + KEYVALUE = 'keyvalue', + DOCSTORE = 'docstore', + FEED = 'feed', + EVENTLOG = 'eventlog', + COUNTER = 'counter', +} +``` + +### Event Types + +```typescript +type EventType = + | 'document:created' + | 'document:updated' + | 'document:deleted' + | 'connection:established' + | 'connection:lost'; + +type EventCallback = (data: EventData) => void; + +interface EventData { + collection: string; + id: string; + document?: any; + timestamp: number; +} +``` + +## Configuration + +### Environment Configuration + +```typescript +import { config } from '@debros/network'; + +// Available configuration options +config.env.fingerprint = 'my-app-id'; +config.env.port = 9000; +config.ipfs.blockstorePath = './blockstore'; +config.orbitdb.directory = './orbitdb'; +``` + +## Error Handling + +### Common Error Types + +```typescript +// Network errors +class NetworkError extends Error { + code: 'NETWORK_ERROR'; + details: string; +} + +// Validation errors +class ValidationError extends Error { + code: 'VALIDATION_ERROR'; + field: string; + value: any; +} + +// Not found errors +class NotFoundError extends Error { + code: 'NOT_FOUND'; + collection: string; + id: string; +} +``` + +## Usage Examples + +### Basic CRUD Operations + +```typescript +import { initDB, create, get, update, remove } from '@debros/network'; + +// Initialize +await initDB(); + +// Create +const user = await create('users', 'user123', { + username: 'alice', + email: 'alice@example.com', +}); + +// Read +const retrieved = await get('users', 'user123'); + +// Update +await update('users', 'user123', { email: 'newemail@example.com' }); + +// Delete +await remove('users', 'user123'); +``` + +### File Operations + +```typescript +import { uploadFile, getFile } from '@debros/network'; + +// Upload file +const fileData = Buffer.from('Hello World'); +const result = await uploadFile(fileData, { filename: 'hello.txt' }); + +// Get file +const file = await getFile(result.cid); +console.log(file.data.toString()); // "Hello World" +``` + +### Transaction Example + +```typescript +import { createTransaction, commitTransaction } from '@debros/network'; + +const tx = createTransaction(); +tx.create('users', 'user1', { name: 'Alice' }) + .create('posts', 'post1', { title: 'Hello', authorId: 'user1' }) + .update('users', 'user1', { postCount: 1 }); + +const result = await commitTransaction(tx); +``` + +This API reference covers all the actual functionality available in the @debros/network package. For detailed examples and guides, see the other documentation sections. diff --git a/docs/docs/api/overview.md b/docs/docs/api/overview.md new file mode 100644 index 0000000..9e89482 --- /dev/null +++ b/docs/docs/api/overview.md @@ -0,0 +1,453 @@ +--- +sidebar_position: 1 +--- + +# API Reference Overview + +The DebrosFramework API provides a comprehensive set of classes, methods, and interfaces for building decentralized applications. This reference covers all public APIs, their parameters, return types, and usage examples. + +## Core Framework Classes + +### Primary Classes + +| Class | Description | Key Features | +| ----------------------------------------------- | ---------------------------- | ------------------------------------ | +| [`DebrosFramework`](./debros-framework) | Main framework class | Initialization, lifecycle management | +| [`BaseModel`](./base-model) | Abstract base for all models | CRUD operations, validation, hooks | +| [`DatabaseManager`](./database-manager) | Database management | User/global databases, lifecycle | +| [`ShardManager`](./shard-manager) | Data sharding | Distribution strategies, routing | +| [`QueryBuilder`](./query-builder) | Query construction | Fluent API, type safety | +| [`QueryExecutor`](./query-executor) | Query execution | Optimization, caching | +| [`RelationshipManager`](./relationship-manager) | Relationship handling | Lazy/eager loading, caching | +| [`MigrationManager`](./migration-manager) | Schema migrations | Version control, rollbacks | +| [`MigrationBuilder`](./migration-builder) | Migration creation | Fluent API, validation | + +### Utility Classes + +| Class | Description | Use Case | +| ------------------- | ------------------------ | ------------------------------ | +| `ModelRegistry` | Model registration | Framework initialization | +| `ConfigManager` | Configuration management | Environment-specific settings | +| `PinningManager` | Automatic pinning | Data availability optimization | +| `PubSubManager` | Event publishing | Real-time features | +| `QueryCache` | Query result caching | Performance optimization | +| `QueryOptimizer` | Query optimization | Automatic performance tuning | +| `RelationshipCache` | Relationship caching | Relationship performance | +| `LazyLoader` | Lazy loading | On-demand data loading | + +## Decorators + +### Model Decorators + +| Decorator | Purpose | Usage | +| ------------------------------ | -------------------------- | ------------------ | +| [`@Model`](./decorators/model) | Define model configuration | Class decorator | +| [`@Field`](./decorators/field) | Define field properties | Property decorator | + +### Relationship Decorators + +| Decorator | Relationship Type | Usage | +| ------------------------------------------------------ | ----------------- | ------------------ | +| [`@BelongsTo`](./decorators/relationships#belongsto) | Many-to-one | Property decorator | +| [`@HasMany`](./decorators/relationships#hasmany) | One-to-many | Property decorator | +| [`@HasOne`](./decorators/relationships#hasone) | One-to-one | Property decorator | +| [`@ManyToMany`](./decorators/relationships#manytomany) | Many-to-many | Property decorator | + +### Hook Decorators + +| Decorator | Trigger | Usage | +| -------------------------------------------------- | --------------------------- | ---------------- | +| [`@BeforeCreate`](./decorators/hooks#beforecreate) | Before creating record | Method decorator | +| [`@AfterCreate`](./decorators/hooks#aftercreate) | After creating record | Method decorator | +| [`@BeforeUpdate`](./decorators/hooks#beforeupdate) | Before updating record | Method decorator | +| [`@AfterUpdate`](./decorators/hooks#afterupdate) | After updating record | Method decorator | +| [`@BeforeDelete`](./decorators/hooks#beforedelete) | Before deleting record | Method decorator | +| [`@AfterDelete`](./decorators/hooks#afterdelete) | After deleting record | Method decorator | +| [`@BeforeSave`](./decorators/hooks#beforesave) | Before save (create/update) | Method decorator | +| [`@AfterSave`](./decorators/hooks#aftersave) | After save (create/update) | Method decorator | + +## Type Definitions + +### Core Types + +```typescript +// Model configuration +interface ModelConfig { + scope: 'user' | 'global'; + type: StoreType; + sharding?: ShardingConfig; + pinning?: PinningConfig; + pubsub?: PubSubConfig; + validation?: ValidationConfig; +} + +// Field configuration +interface FieldConfig { + type: FieldType; + required?: boolean; + unique?: boolean; + default?: any | (() => any); + validate?: (value: any) => boolean; + transform?: (value: any) => any; + serialize?: boolean; + index?: boolean; + virtual?: boolean; +} + +// Query types +interface QueryOptions { + where?: WhereClause[]; + orderBy?: OrderByClause[]; + limit?: number; + offset?: number; + with?: string[]; + cache?: boolean | number; +} +``` + +### Enum Types + +```typescript +// Store types +type StoreType = 'docstore' | 'eventlog' | 'keyvalue' | 'counter' | 'feed'; + +// Field types +type FieldType = 'string' | 'number' | 'boolean' | 'array' | 'object' | 'date'; + +// Sharding strategies +type ShardingStrategy = 'hash' | 'range' | 'user'; + +// Query operators +type QueryOperator = + | 'eq' + | 'ne' + | 'gt' + | 'gte' + | 'lt' + | 'lte' + | 'in' + | 'not in' + | 'like' + | 'regex' + | 'is null' + | 'is not null' + | 'includes' + | 'includes any' + | 'includes all'; +``` + +## Configuration Interfaces + +### Framework Configuration + +```typescript +interface DebrosFrameworkConfig { + // Cache configuration + cache?: { + enabled?: boolean; + maxSize?: number; + ttl?: number; + }; + + // Query optimization + queryOptimization?: { + enabled?: boolean; + cacheQueries?: boolean; + parallelExecution?: boolean; + }; + + // Automatic features + automaticPinning?: { + enabled?: boolean; + strategy?: 'popularity' | 'fixed' | 'tiered'; + }; + + // PubSub configuration + pubsub?: { + enabled?: boolean; + bufferSize?: number; + }; + + // Development settings + development?: { + logLevel?: 'debug' | 'info' | 'warn' | 'error'; + enableMetrics?: boolean; + }; +} +``` + +### Sharding Configuration + +```typescript +interface ShardingConfig { + strategy: ShardingStrategy; + count: number; + key: string; + ranges?: ShardRange[]; +} + +interface ShardRange { + min: any; + max: any; + shard: number; +} +``` + +### Pinning Configuration + +```typescript +interface PinningConfig { + strategy: 'fixed' | 'popularity' | 'tiered'; + factor?: number; + maxPins?: number; + ttl?: number; +} +``` + +## Error Types + +### Framework Errors + +```typescript +class DebrosFrameworkError extends Error { + code: string; + details?: any; +} + +class ValidationError extends DebrosFrameworkError { + field: string; + value: any; + constraint: string; +} + +class QueryError extends DebrosFrameworkError { + query: string; + parameters?: any[]; +} + +class RelationshipError extends DebrosFrameworkError { + modelName: string; + relationshipName: string; + relatedModel: string; +} +``` + +## Response Types + +### Query Results + +```typescript +interface QueryResult { + data: T[]; + total: number; + page?: number; + perPage?: number; + totalPages?: number; + hasMore?: boolean; +} + +interface PaginationInfo { + page: number; + perPage: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; +} +``` + +### Operation Results + +```typescript +interface CreateResult { + model: T; + created: boolean; + errors?: ValidationError[]; +} + +interface UpdateResult { + model: T; + updated: boolean; + changes: string[]; + errors?: ValidationError[]; +} + +interface DeleteResult { + deleted: boolean; + id: string; +} +``` + +## Event Types + +### Model Events + +```typescript +interface ModelEvent { + type: 'create' | 'update' | 'delete'; + modelName: string; + modelId: string; + data?: any; + changes?: string[]; + timestamp: number; + userId?: string; +} + +interface RelationshipEvent { + type: 'attach' | 'detach'; + modelName: string; + modelId: string; + relationshipName: string; + relatedModelName: string; + relatedModelId: string; + timestamp: number; +} +``` + +### Framework Events + +```typescript +interface FrameworkEvent { + type: 'initialized' | 'stopped' | 'error'; + message?: string; + error?: Error; + timestamp: number; +} + +interface DatabaseEvent { + type: 'created' | 'opened' | 'closed'; + databaseName: string; + scope: 'user' | 'global'; + userId?: string; + timestamp: number; +} +``` + +## Migration Types + +### Migration Configuration + +```typescript +interface Migration { + id: string; + version: string; + name: string; + description: string; + targetModels: string[]; + up: MigrationOperation[]; + down: MigrationOperation[]; + dependencies?: string[]; + validators?: MigrationValidator[]; + createdAt: number; +} + +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; + transformer?: (data: any) => any; + customOperation?: (context: MigrationContext) => Promise; +} +``` + +## Constants + +### Default Values + +```typescript +const DEFAULT_CONFIG = { + CACHE_SIZE: 1000, + CACHE_TTL: 300000, // 5 minutes + QUERY_TIMEOUT: 30000, // 30 seconds + MAX_CONCURRENT_QUERIES: 10, + DEFAULT_PAGE_SIZE: 20, + MAX_PAGE_SIZE: 1000, + SHARD_COUNT: 4, + PIN_FACTOR: 2, +}; +``` + +### Status Codes + +```typescript +enum StatusCodes { + SUCCESS = 200, + CREATED = 201, + NO_CONTENT = 204, + BAD_REQUEST = 400, + NOT_FOUND = 404, + VALIDATION_ERROR = 422, + INTERNAL_ERROR = 500, +} +``` + +## Utility Functions + +### Helper Functions + +```typescript +// Model utilities +function getModelConfig(modelClass: typeof BaseModel): ModelConfig; +function getFieldConfig(modelClass: typeof BaseModel, fieldName: string): FieldConfig; +function getRelationshipConfig(modelClass: typeof BaseModel): RelationshipConfig[]; + +// Query utilities +function buildWhereClause(field: string, operator: QueryOperator, value: any): WhereClause; +function optimizeQuery(query: QueryBuilder): QueryBuilder; +function cacheKey(query: QueryBuilder): string; + +// Validation utilities +function validateFieldValue(value: any, config: FieldConfig): ValidationResult; +function sanitizeInput(value: any): any; +function normalizeEmail(email: string): string; +``` + +## Version Information + +Current API version: **1.0.0** + +### Version Compatibility + +| Framework Version | API Version | Breaking Changes | +| ----------------- | ----------- | --------------------------- | +| 1.0.x | 1.0.0 | None | +| 1.1.x | 1.0.0 | None (backwards compatible) | + +### Deprecation Policy + +- Deprecated APIs are marked with `@deprecated` tags +- Deprecated features are supported for at least 2 minor versions +- Breaking changes only occur in major version updates +- Migration guides are provided for breaking changes + +## Getting Help + +### Documentation + +- **[Getting Started Guide](../getting-started)** - Basic setup and usage +- **[Core Concepts](../core-concepts/architecture)** - Framework architecture +- **[Examples](../examples/basic-usage)** - Practical examples + +### Support + +- **GitHub Issues** - Bug reports and feature requests +- **Discord Community** - Real-time help and discussion +- **Stack Overflow** - Tagged questions with `debros-framework` + +### Contributing + +- **[Contributing Guide](https://github.com/debros/network/blob/main/CONTRIBUTING.md)** - How to contribute +- **[API Design Guidelines](https://github.com/debros/network/blob/main/API_DESIGN.md)** - API design principles +- **[Development Setup](https://github.com/debros/network/blob/main/DEVELOPMENT.md)** - Local development setup + +This API reference provides comprehensive documentation for all public interfaces in DebrosFramework. For detailed information about specific classes and methods, explore the individual API documentation pages. diff --git a/docs/docs/core-concepts/architecture.md b/docs/docs/core-concepts/architecture.md new file mode 100644 index 0000000..3ab793e --- /dev/null +++ b/docs/docs/core-concepts/architecture.md @@ -0,0 +1,340 @@ +--- +sidebar_position: 1 +--- + +# Architecture Overview + +DebrosFramework is designed with a modular architecture that provides powerful abstractions over OrbitDB and IPFS while maintaining scalability and performance. This guide explains how the framework components work together. + +## High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your Application │ +├─────────────────────────────────────────────────────────────┤ +│ DebrosFramework │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Models │ │ Queries │ │ Migrations │ │ +│ │ & Decorators│ │ System │ │ System │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Database │ │ Shard │ │ Relationship│ │ +│ │ Manager │ │ Manager │ │ Manager │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Pinning │ │ PubSub │ │ Cache │ │ +│ │ Manager │ │ Manager │ │ System │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ OrbitDB Layer │ +├─────────────────────────────────────────────────────────────┤ +│ IPFS Layer │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Core Components + +### 1. Models & Decorators Layer + +The foundation of DebrosFramework is the model layer, which provides: + +- **BaseModel**: Abstract base class with CRUD operations +- **Decorators**: Type-safe decorators for defining models, fields, and relationships +- **Model Registry**: Central registry for model management +- **Validation System**: Built-in validation with custom validators + +```typescript +// Example: Model definition with decorators +@Model({ + scope: 'user', + type: 'docstore', + sharding: { strategy: 'hash', count: 4, key: 'userId' }, +}) +class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @BelongsTo(() => User, 'userId') + user: User; +} +``` + +### 2. Database Management Layer + +Handles the complexity of distributed database operations: + +#### Database Manager + +- **User-Scoped Databases**: Each user gets their own database instance +- **Global Databases**: Shared databases for global data +- **Automatic Creation**: Databases are created on-demand +- **Lifecycle Management**: Handles database initialization and cleanup + +#### Shard Manager + +- **Distribution Strategy**: Distributes data across multiple databases +- **Hash-based Sharding**: Uses consistent hashing for data distribution +- **Range-based Sharding**: Distributes data based on value ranges +- **User-based Sharding**: Dedicated shards per user or user group + +```typescript +// Example: Database scoping +@Model({ scope: 'user' }) // Each user gets their own database +class UserPost extends BaseModel {} + +@Model({ scope: 'global' }) // Shared across all users +class GlobalNews extends BaseModel {} +``` + +### 3. Query System + +Provides powerful querying capabilities with optimization: + +#### Query Builder + +- **Chainable API**: Fluent interface for building queries +- **Type Safety**: Full TypeScript support with auto-completion +- **Complex Conditions**: Support for complex where clauses +- **Relationship Loading**: Eager and lazy loading of relationships + +#### Query Executor + +- **Smart Routing**: Routes queries to appropriate databases/shards +- **Optimization**: Automatically optimizes query execution +- **Parallel Execution**: Executes queries across shards in parallel +- **Result Aggregation**: Combines results from multiple sources + +#### Query Cache + +- **Intelligent Caching**: Caches frequently accessed data +- **Cache Invalidation**: Automatic cache invalidation on updates +- **Memory Management**: Efficient memory usage with LRU eviction + +```typescript +// Example: Complex query with optimization +const posts = await Post.query() + .where('userId', currentUser.id) + .where('isPublic', true) + .where('createdAt', '>', Date.now() - 30 * 24 * 60 * 60 * 1000) + .with(['user', 'comments.user']) + .orderBy('popularity', 'desc') + .limit(20) + .cache(300) // Cache for 5 minutes + .find(); +``` + +### 4. Relationship Management + +Handles complex relationships between distributed models: + +#### Relationship Manager + +- **Lazy Loading**: Load relationships on-demand +- **Eager Loading**: Pre-load relationships to reduce queries +- **Cross-Database**: Handle relationships across different databases +- **Performance Optimization**: Batch loading and caching + +#### Relationship Cache + +- **Intelligent Caching**: Cache relationship data based on access patterns +- **Consistency**: Maintain consistency across cached relationships +- **Memory Efficiency**: Optimize memory usage for large datasets + +```typescript +// Example: Complex relationships +@Model({ scope: 'global' }) +class User extends BaseModel { + @HasMany(() => Post, 'userId') + posts: Post[]; + + @ManyToMany(() => User, 'followers', 'following') + followers: User[]; +} + +// Load user with all relationships +const user = await User.findById(userId, { + with: ['posts.comments', 'followers.posts'], +}); +``` + +### 5. Automatic Features + +Provides built-in optimization and convenience features: + +#### Pinning Manager + +- **Automatic Pinning**: Intelligently pin important data +- **Popularity-based**: Pin data based on access frequency +- **Tiered Pinning**: Different pinning strategies for different data types +- **Resource Management**: Optimize pinning resources across the network + +#### PubSub Manager + +- **Event Publishing**: Automatically publish model events +- **Real-time Updates**: Enable real-time application features +- **Event Filtering**: Intelligent event routing and filtering +- **Performance**: Efficient event handling with batching + +```typescript +// Example: Automatic features in action +@Model({ + pinning: { strategy: 'popularity', factor: 2 }, + pubsub: { publishEvents: ['create', 'update'] }, +}) +class ImportantData extends BaseModel { + // Data is automatically pinned based on popularity + // Events are published on create/update +} +``` + +### 6. Migration System + +Handles schema evolution and data transformation: + +#### Migration Manager + +- **Version Management**: Track schema versions across databases +- **Safe Migrations**: Rollback capabilities for failed migrations +- **Data Transformation**: Transform existing data during migrations +- **Conflict Resolution**: Handle migration conflicts in distributed systems + +#### Migration Builder + +- **Fluent API**: Easy-to-use migration definition +- **Validation**: Pre and post migration validation +- **Batch Processing**: Handle large datasets efficiently +- **Progress Tracking**: Monitor migration progress + +```typescript +// Example: Schema migration +const migration = createMigration('add_user_profiles', '1.1.0') + .addField('User', 'profilePicture', { + type: 'string', + required: false, + }) + .transformData('User', (user) => ({ + ...user, + displayName: user.username || 'Anonymous', + })) + .addValidator('check_profile_data', async (context) => { + // Validate migration + return { valid: true, errors: [], warnings: [] }; + }) + .build(); + +await migrationManager.runMigration(migration.id); +``` + +## Data Flow + +### 1. Model Operation Flow + +``` +User Code → Model Method → Database Manager → Shard Manager → OrbitDB → IPFS + ↑ ↓ + └─── Query Cache ← Query Optimizer ← Query Executor ←────────────────── +``` + +### 2. Query Execution Flow + +1. **Query Building**: User builds query using Query Builder +2. **Optimization**: Query Optimizer analyzes and optimizes the query +3. **Routing**: Query Executor determines which databases/shards to query +4. **Execution**: Parallel execution across relevant databases +5. **Aggregation**: Results are combined and returned +6. **Caching**: Results are cached for future queries + +### 3. Relationship Loading Flow + +1. **Detection**: Framework detects relationship access +2. **Strategy**: Determines lazy vs eager loading strategy +3. **Batching**: Batches multiple relationship loads +4. **Caching**: Caches loaded relationships +5. **Resolution**: Returns resolved relationship data + +## Scalability Features + +### Horizontal Scaling + +- **Automatic Sharding**: Data is automatically distributed across shards +- **Dynamic Scaling**: Add new shards without downtime +- **Load Balancing**: Distribute queries across available resources +- **Peer Distribution**: Leverage IPFS network for data distribution + +### Performance Optimization + +- **Query Optimization**: Automatic query optimization and caching +- **Lazy Loading**: Load data only when needed +- **Batch Operations**: Combine multiple operations for efficiency +- **Memory Management**: Efficient memory usage with automatic cleanup + +### Data Consistency + +- **Eventual Consistency**: Handle distributed system consistency challenges +- **Conflict Resolution**: Automatic conflict resolution strategies +- **Version Management**: Track data versions across the network +- **Validation**: Ensure data integrity with comprehensive validation + +## Security Considerations + +### Access Control + +- **User-Scoped Data**: Automatic isolation of user data +- **Permission System**: Built-in permission checking +- **Validation**: Input validation and sanitization +- **Audit Logging**: Track all data operations + +### Data Protection + +- **Encryption**: Support for data encryption at rest and in transit +- **Privacy**: User-scoped databases ensure data privacy +- **Network Security**: Leverage IPFS and OrbitDB security features +- **Key Management**: Secure key storage and rotation + +## Framework Lifecycle + +### Initialization + +1. **Service Setup**: Initialize IPFS and OrbitDB services +2. **Framework Init**: Initialize DebrosFramework with services +3. **Model Registration**: Register application models +4. **Database Creation**: Create necessary databases on-demand + +### Operation + +1. **Request Processing**: Handle user requests through models +2. **Query Execution**: Execute optimized queries across shards +3. **Data Management**: Manage data lifecycle and cleanup +4. **Event Publishing**: Publish relevant events through PubSub + +### Shutdown + +1. **Graceful Shutdown**: Complete ongoing operations +2. **Data Persistence**: Ensure all data is persisted +3. **Resource Cleanup**: Clean up resources and connections +4. **Service Shutdown**: Stop underlying services + +## Best Practices + +### Model Design + +- Use appropriate scoping (user vs global) based on data access patterns +- Design efficient sharding strategies for your data distribution +- Implement proper validation to ensure data integrity +- Use relationships judiciously to avoid performance issues + +### Query Optimization + +- Use indexes for frequently queried fields +- Implement proper caching strategies +- Use eager loading for predictable relationship access +- Monitor query performance and optimize accordingly + +### Data Management + +- Implement proper migration strategies for schema evolution +- Use appropriate pinning strategies for data availability +- Monitor and manage resource usage +- Implement proper error handling and recovery + +This architecture enables DebrosFramework to provide a powerful, scalable, and easy-to-use abstraction over the complexities of distributed systems while maintaining the benefits of decentralization. diff --git a/docs/docs/core-concepts/decorators.md b/docs/docs/core-concepts/decorators.md new file mode 100644 index 0000000..14425bb --- /dev/null +++ b/docs/docs/core-concepts/decorators.md @@ -0,0 +1,696 @@ +--- +sidebar_position: 3 +--- + +# Decorators Reference + +DebrosFramework uses TypeScript decorators to provide a clean, declarative way to define models, fields, relationships, and hooks. This guide covers all available decorators and their usage patterns. + +## Model Decorators + +### @Model + +The `@Model` decorator is used to mark a class as a DebrosFramework model and configure its behavior. + +```typescript +import { BaseModel, Model } from 'debros-framework'; + +@Model({ + scope: 'global', + type: 'docstore', + sharding: { + strategy: 'hash', + count: 4, + key: 'id', + }, + pinning: { + strategy: 'popularity', + factor: 2, + }, + pubsub: { + publishEvents: ['create', 'update'], + }, +}) +export class User extends BaseModel { + // Model definition +} +``` + +#### Configuration Options + +| Option | Type | Description | Default | +| ------------ | -------------------- | ------------------------ | ------------ | +| `scope` | `'user' \| 'global'` | Database scope | `'global'` | +| `type` | `StoreType` | OrbitDB store type | `'docstore'` | +| `sharding` | `ShardingConfig` | Sharding configuration | `undefined` | +| `pinning` | `PinningConfig` | Pinning configuration | `undefined` | +| `pubsub` | `PubSubConfig` | PubSub configuration | `undefined` | +| `validation` | `ValidationConfig` | Validation configuration | `undefined` | + +#### Store Types + +```typescript +type StoreType = 'docstore' | 'eventlog' | 'keyvalue' | 'counter' | 'feed'; + +// Examples of different store types +@Model({ type: 'docstore' }) // Document storage (most common) +class Document extends BaseModel {} + +@Model({ type: 'eventlog' }) // Event log storage +class Event extends BaseModel {} + +@Model({ type: 'keyvalue' }) // Key-value storage +class Setting extends BaseModel {} + +@Model({ type: 'counter' }) // Counter storage +class Counter extends BaseModel {} + +@Model({ type: 'feed' }) // Feed storage +class FeedItem extends BaseModel {} +``` + +#### Sharding Configuration + +```typescript +interface ShardingConfig { + strategy: 'hash' | 'range' | 'user'; + count: number; + key: string; + ranges?: Array<{ min: any; max: any; shard: number }>; +} + +// Hash-based sharding +@Model({ + sharding: { + strategy: 'hash', + count: 8, + key: 'userId', // Shard based on userId hash + }, +}) +class UserPost extends BaseModel {} + +// Range-based sharding +@Model({ + sharding: { + strategy: 'range', + count: 4, + key: 'createdAt', + ranges: [ + { min: 0, max: Date.now() - 365 * 24 * 60 * 60 * 1000, shard: 0 }, // Old data + { + min: Date.now() - 365 * 24 * 60 * 60 * 1000, + max: Date.now() - 30 * 24 * 60 * 60 * 1000, + shard: 1, + }, // Medium data + { min: Date.now() - 30 * 24 * 60 * 60 * 1000, max: Date.now(), shard: 2 }, // Recent data + { min: Date.now(), max: Infinity, shard: 3 }, // Future data + ], + }, +}) +class TimeBasedData extends BaseModel {} + +// User-based sharding +@Model({ + sharding: { + strategy: 'user', + count: 1, // One shard per user + key: 'userId', + }, +}) +class PrivateUserData extends BaseModel {} +``` + +## Field Decorators + +### @Field + +The `@Field` decorator defines model fields with validation, transformation, and serialization options. + +```typescript +import { Field } from 'debros-framework'; + +export class User extends BaseModel { + @Field({ + type: 'string', + required: true, + unique: true, + minLength: 3, + maxLength: 20, + pattern: /^[a-zA-Z0-9_]+$/, + validate: (value: string) => value.toLowerCase() !== 'admin', + transform: (value: string) => value.toLowerCase(), + index: true, + }) + username: string; +} +``` + +#### Field Types + +```typescript +// String fields +@Field({ type: 'string', required: true }) +name: string; + +@Field({ + type: 'string', + required: false, + default: 'default-value', + minLength: 3, + maxLength: 100, + pattern: /^[a-zA-Z\s]+$/ +}) +description?: string; + +// Number fields +@Field({ + type: 'number', + required: true, + min: 0, + max: 100 +}) +score: number; + +@Field({ + type: 'number', + required: false, + default: () => Date.now() +}) +timestamp?: number; + +// Boolean fields +@Field({ + type: 'boolean', + required: false, + default: false +}) +isActive: boolean; + +// Array fields +@Field({ + type: 'array', + required: false, + default: [], + maxLength: 10 +}) +tags: string[]; + +// Object fields +@Field({ + type: 'object', + required: false, + default: {} +}) +metadata: Record; + +// Date fields +@Field({ + type: 'date', + required: false, + default: () => new Date() +}) +createdAt: Date; +``` + +#### Field Configuration Options + +| Option | Type | Description | Applies To | +| ----------- | ----------------- | ----------------------------- | --------------- | +| `type` | `FieldType` | Field data type | All | +| `required` | `boolean` | Whether field is required | All | +| `unique` | `boolean` | Whether field must be unique | All | +| `default` | `any \| Function` | Default value or function | All | +| `min` | `number` | Minimum value | Numbers | +| `max` | `number` | Maximum value | Numbers | +| `minLength` | `number` | Minimum length | Strings, Arrays | +| `maxLength` | `number` | Maximum length | Strings, Arrays | +| `pattern` | `RegExp` | Validation pattern | Strings | +| `validate` | `Function` | Custom validation function | All | +| `transform` | `Function` | Value transformation function | All | +| `serialize` | `boolean` | Include in serialization | All | +| `index` | `boolean` | Create index for queries | All | +| `virtual` | `boolean` | Virtual field (computed) | All | + +#### Advanced Field Examples + +```typescript +export class User extends BaseModel { + // Email with validation + @Field({ + type: 'string', + required: true, + unique: true, + validate: (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + throw new Error('Invalid email format'); + } + return true; + }, + transform: (email: string) => email.toLowerCase(), + index: true, + }) + email: string; + + // Password with hashing + @Field({ + type: 'string', + required: true, + serialize: false, // Don't include in JSON + validate: (password: string) => { + if (password.length < 8) { + throw new Error('Password must be at least 8 characters'); + } + return true; + }, + }) + passwordHash: string; + + // Tags with normalization + @Field({ + type: 'array', + required: false, + default: [], + maxLength: 10, + transform: (tags: string[]) => { + // Normalize and deduplicate tags + return [...new Set(tags.map((tag) => tag.toLowerCase().trim()))].filter( + (tag) => tag.length > 0, + ); + }, + }) + tags: string[]; + + // Virtual computed field + @Field({ + type: 'string', + virtual: true, + }) + get emailDomain(): string { + return this.email.split('@')[1]; + } + + // Score with validation + @Field({ + type: 'number', + required: false, + default: 0, + min: 0, + max: 1000, + validate: (score: number) => { + if (score % 1 !== 0) { + throw new Error('Score must be a whole number'); + } + return true; + }, + }) + score: number; +} +``` + +## Relationship Decorators + +### @BelongsTo + +Defines a many-to-one relationship where this model belongs to another model. + +```typescript +import { BelongsTo } from 'debros-framework'; + +@Model({ scope: 'user' }) +export class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true }) + userId: string; + + // Post belongs to a User + @BelongsTo(() => User, 'userId') + user: User; + + // Post belongs to a Category (with options) + @BelongsTo(() => Category, 'categoryId', { + cache: true, + eager: false, + }) + category: Category; +} +``` + +### @HasMany + +Defines a one-to-many relationship where this model has many of another model. + +```typescript +import { HasMany } from 'debros-framework'; + +@Model({ scope: 'global' }) +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + // User has many Posts + @HasMany(() => Post, 'userId') + posts: Post[]; + + // User has many Comments (with options) + @HasMany(() => Comment, 'userId', { + cache: true, + eager: false, + orderBy: 'createdAt', + limit: 100, + }) + comments: Comment[]; +} +``` + +### @HasOne + +Defines a one-to-one relationship where this model has one of another model. + +```typescript +import { HasOne } from 'debros-framework'; + +@Model({ scope: 'global' }) +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + // User has one Profile + @HasOne(() => UserProfile, 'userId') + profile: UserProfile; + + // User has one Setting (with options) + @HasOne(() => UserSetting, 'userId', { + cache: true, + eager: true, + }) + settings: UserSetting; +} +``` + +### @ManyToMany + +Defines a many-to-many relationship through a join table or field. + +```typescript +import { ManyToMany } from 'debros-framework'; + +@Model({ scope: 'global' }) +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + // Many-to-many through join table + @ManyToMany(() => Role, 'user_roles', 'userId', 'roleId') + roles: Role[]; + + // Many-to-many through array field + @ManyToMany(() => Tag, 'userTags', { + through: 'tagIds', // Array field in this model + cache: true, + }) + tags: Tag[]; + + @Field({ type: 'array', required: false, default: [] }) + tagIds: string[]; // Array of tag IDs +} +``` + +#### Relationship Configuration Options + +| Option | Type | Description | Default | +| --------- | --------- | ------------------------------- | ----------- | +| `cache` | `boolean` | Cache relationship data | `false` | +| `eager` | `boolean` | Load relationship eagerly | `false` | +| `orderBy` | `string` | Order related records by field | `undefined` | +| `limit` | `number` | Limit number of related records | `undefined` | +| `where` | `object` | Additional where conditions | `undefined` | + +#### Advanced Relationship Examples + +```typescript +@Model({ scope: 'global' }) +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + // Posts with caching and ordering + @HasMany(() => Post, 'userId', { + cache: true, + orderBy: 'createdAt', + limit: 50, + where: { isPublished: true }, + }) + publishedPosts: Post[]; + + // Recent posts + @HasMany(() => Post, 'userId', { + orderBy: 'createdAt', + limit: 10, + where: { + createdAt: { $gt: Date.now() - 30 * 24 * 60 * 60 * 1000 }, + }, + }) + recentPosts: Post[]; + + // Followers (many-to-many) + @ManyToMany(() => User, 'user_follows', 'followingId', 'followerId') + followers: User[]; + + // Following (many-to-many) + @ManyToMany(() => User, 'user_follows', 'followerId', 'followingId') + following: User[]; +} +``` + +## Hook Decorators + +Hook decorators allow you to execute code at specific points in the model lifecycle. + +### Lifecycle Hooks + +```typescript +import { + BeforeCreate, + AfterCreate, + BeforeUpdate, + AfterUpdate, + BeforeDelete, + AfterDelete, + BeforeSave, + AfterSave, +} from 'debros-framework'; + +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + @Field({ type: 'string', required: true }) + email: string; + + @Field({ type: 'number', required: false }) + createdAt: number; + + @Field({ type: 'number', required: false }) + updatedAt: number; + + // Before creating a new record + @BeforeCreate() + async beforeCreate() { + this.createdAt = Date.now(); + this.updatedAt = Date.now(); + + // Validate uniqueness + const existing = await User.findOne({ email: this.email }); + if (existing) { + throw new Error('Email already exists'); + } + } + + // After creating a new record + @AfterCreate() + async afterCreate() { + // Send welcome email + await this.sendWelcomeEmail(); + + // Create default settings + await this.createDefaultSettings(); + } + + // Before updating a record + @BeforeUpdate() + async beforeUpdate() { + this.updatedAt = Date.now(); + + // Log the change + console.log(`Updating user ${this.username}`); + } + + // After updating a record + @AfterUpdate() + async afterUpdate() { + // Invalidate cache + await this.invalidateCache(); + } + + // Before deleting a record + @BeforeDelete() + async beforeDelete() { + // Clean up related data + await this.cleanupRelatedData(); + } + + // After deleting a record + @AfterDelete() + async afterDelete() { + // Log deletion + console.log(`User ${this.username} deleted`); + } + + // Before any save operation (create or update) + @BeforeSave() + async beforeSave() { + // Validate data + await this.validateData(); + } + + // After any save operation (create or update) + @AfterSave() + async afterSave() { + // Update search index + await this.updateSearchIndex(); + } + + private async sendWelcomeEmail(): Promise { + // Implementation + } + + private async createDefaultSettings(): Promise { + // Implementation + } + + private async invalidateCache(): Promise { + // Implementation + } + + private async cleanupRelatedData(): Promise { + // Implementation + } + + private async validateData(): Promise { + // Implementation + } + + private async updateSearchIndex(): Promise { + // Implementation + } +} +``` + +### Hook Parameters + +Some hooks can receive parameters with information about the operation: + +```typescript +export class AuditedModel extends BaseModel { + @BeforeUpdate() + async beforeUpdate(changes: Record) { + // Log what fields are changing + console.log('Fields changing:', Object.keys(changes)); + + // Audit specific changes + if (changes.status) { + await this.auditStatusChange(changes.status); + } + } + + @AfterCreate() + async afterCreate(model: this) { + // The model instance is passed to after hooks + console.log(`Created ${model.constructor.name} with ID ${model.id}`); + } + + private async auditStatusChange(newStatus: string): Promise { + // Implementation + } +} +``` + +## Decorator Best Practices + +### TypeScript Configuration + +Ensure your `tsconfig.json` has the required decorator settings: + +```json +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "target": "ES2020", + "module": "commonjs" + } +} +``` + +### Performance Considerations + +1. **Use caching wisely**: Only cache relationships that are accessed frequently +2. **Limit eager loading**: Eager loading can impact performance with large datasets +3. **Optimize hooks**: Keep hook operations lightweight and fast +4. **Index frequently queried fields**: Add indexes to improve query performance + +### Code Organization + +1. **Group related decorators**: Keep related decorators together +2. **Document complex logic**: Add comments for complex validation or transformation logic +3. **Use consistent naming**: Follow consistent naming conventions for fields and relationships +4. **Separate concerns**: Keep business logic in separate methods, not in decorators + +### Error Handling + +```typescript +export class User extends BaseModel { + @Field({ + type: 'string', + required: true, + validate: (value: string) => { + if (!value || value.trim().length === 0) { + throw new Error('Username cannot be empty'); + } + + if (value.length < 3) { + throw new Error('Username must be at least 3 characters long'); + } + + if (!/^[a-zA-Z0-9_]+$/.test(value)) { + throw new Error('Username can only contain letters, numbers, and underscores'); + } + + return true; + }, + }) + username: string; + + @BeforeCreate() + async beforeCreate() { + try { + // Expensive validation + await this.validateUniqueEmail(); + } catch (error) { + throw new Error(`Validation failed: ${error.message}`); + } + } + + private async validateUniqueEmail(): Promise { + const existing = await User.findOne({ email: this.email }); + if (existing) { + throw new Error('Email address is already in use'); + } + } +} +``` + +This comprehensive decorator system provides a powerful, type-safe way to define your data models and their behavior in DebrosFramework applications. diff --git a/docs/docs/core-concepts/models.md b/docs/docs/core-concepts/models.md new file mode 100644 index 0000000..f7c9e4d --- /dev/null +++ b/docs/docs/core-concepts/models.md @@ -0,0 +1,566 @@ +--- +sidebar_position: 2 +--- + +# Models and Fields + +Models are the foundation of DebrosFramework applications. They define your data structure, validation rules, and behavior using TypeScript classes and decorators. This guide covers everything you need to know about creating and working with models. + +## Basic Model Structure + +### Creating a Model + +Every model in DebrosFramework extends the `BaseModel` class and uses the `@Model` decorator: + +```typescript +import { BaseModel, Model, Field } from 'debros-framework'; + +@Model({ + scope: 'global', + type: 'docstore', +}) +export class User extends BaseModel { + @Field({ type: 'string', required: true, unique: true }) + username: string; + + @Field({ type: 'string', required: true }) + email: string; +} +``` + +### Model Configuration Options + +The `@Model` decorator accepts several configuration options: + +```typescript +@Model({ + // Database scope: 'user' or 'global' + scope: 'user', + + // OrbitDB store type + type: 'docstore', // 'docstore' | 'eventlog' | 'keyvalue' | 'counter' | 'feed' + + // Sharding configuration + sharding: { + strategy: 'hash', // 'hash' | 'range' | 'user' + count: 4, // Number of shards + key: 'userId', // Field to use for sharding + }, + + // Pinning configuration + pinning: { + strategy: 'popularity', // 'fixed' | 'popularity' | 'tiered' + factor: 2, // Pinning factor + }, + + // PubSub configuration + pubsub: { + publishEvents: ['create', 'update', 'delete'], + }, + + // Validation configuration + validation: { + strict: true, // Strict validation mode + allowExtraFields: false, + }, +}) +export class Post extends BaseModel { + // Model fields go here +} +``` + +## Field Types and Validation + +### Basic Field Types + +DebrosFramework supports several field types with built-in validation: + +```typescript +export class ExampleModel extends BaseModel { + @Field({ type: 'string', required: true }) + name: string; + + @Field({ type: 'number', required: true, min: 0, max: 100 }) + score: number; + + @Field({ type: 'boolean', required: false, default: false }) + isActive: boolean; + + @Field({ type: 'array', required: false, default: [] }) + tags: string[]; + + @Field({ type: 'object', required: false }) + metadata: Record; + + @Field({ type: 'date', required: false, default: () => new Date() }) + createdAt: Date; +} +``` + +### Field Configuration Options + +Each field can be configured with various options: + +```typescript +@Field({ + // Basic type information + type: 'string', + required: true, + unique: false, + + // Default values + default: 'default-value', + default: () => Date.now(), // Function for dynamic defaults + + // Validation constraints + min: 0, // Minimum value (numbers) + max: 100, // Maximum value (numbers) + minLength: 3, // Minimum length (strings/arrays) + maxLength: 50, // Maximum length (strings/arrays) + pattern: /^[a-zA-Z0-9]+$/, // Regex pattern (strings) + + // Custom validation + validate: (value: any) => { + return value.length >= 3 && value.length <= 20; + }, + + // Field transformation + transform: (value: any) => value.toLowerCase(), + + // Serialization options + serialize: true, // Include in serialization + + // Indexing (for query optimization) + index: true +}) +fieldName: string; +``` + +### Custom Validation + +You can implement complex validation logic using custom validators: + +```typescript +export class User extends BaseModel { + @Field({ + type: 'string', + required: true, + validate: (value: string) => { + // Username validation + if (value.length < 3 || value.length > 20) { + throw new Error('Username must be between 3 and 20 characters'); + } + + if (!/^[a-zA-Z0-9_]+$/.test(value)) { + throw new Error('Username can only contain letters, numbers, and underscores'); + } + + return true; + }, + }) + username: string; + + @Field({ + type: 'string', + required: true, + validate: (value: string) => { + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + throw new Error('Invalid email format'); + } + return true; + }, + }) + email: string; + + @Field({ + type: 'number', + required: false, + validate: (value: number) => { + // Age validation + if (value < 13 || value > 120) { + throw new Error('Age must be between 13 and 120'); + } + return true; + }, + }) + age?: number; +} +``` + +### Field Transformation + +Transform field values before storage or after retrieval: + +```typescript +export class User extends BaseModel { + @Field({ + type: 'string', + required: true, + transform: (value: string) => value.toLowerCase().trim(), + }) + username: string; + + @Field({ + type: 'string', + required: true, + transform: (value: string) => value.toLowerCase(), + }) + email: string; + + @Field({ + type: 'array', + required: false, + default: [], + transform: (tags: string[]) => { + // Normalize and deduplicate tags + return [...new Set(tags.map((tag) => tag.toLowerCase().trim()))]; + }, + }) + tags: string[]; +} +``` + +## Model Scoping + +### User-Scoped Models + +User-scoped models create separate databases for each user, providing data isolation: + +```typescript +@Model({ + scope: 'user', // Each user gets their own database + type: 'docstore', + sharding: { + strategy: 'user', + count: 2, + key: 'userId', + }, +}) +export class UserPost extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true }) + content: string; + + @Field({ type: 'string', required: true }) + userId: string; // Required for user-scoped models +} +``` + +### Global Models + +Global models are shared across all users: + +```typescript +@Model({ + scope: 'global', // Shared across all users + type: 'docstore', + sharding: { + strategy: 'hash', + count: 8, + key: 'id', + }, +}) +export class GlobalNews extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true }) + content: string; + + @Field({ type: 'string', required: true }) + category: string; + + @Field({ type: 'boolean', required: false, default: true }) + isPublished: boolean; +} +``` + +## Model Hooks + +Use hooks to execute code at specific points in the model lifecycle: + +```typescript +import { + BeforeCreate, + AfterCreate, + BeforeUpdate, + AfterUpdate, + BeforeDelete, +} from 'debros-framework'; + +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + @Field({ type: 'string', required: true }) + passwordHash: string; + + @Field({ type: 'number', required: false }) + createdAt: number; + + @Field({ type: 'number', required: false }) + updatedAt: number; + + @BeforeCreate() + async beforeCreate() { + this.createdAt = Date.now(); + this.updatedAt = Date.now(); + + // Hash password before saving + if (this.passwordHash && !this.passwordHash.startsWith('$2b$')) { + this.passwordHash = await this.hashPassword(this.passwordHash); + } + } + + @BeforeUpdate() + async beforeUpdate() { + this.updatedAt = Date.now(); + + // Hash password if it was changed + if (this.isFieldModified('passwordHash') && !this.passwordHash.startsWith('$2b$')) { + this.passwordHash = await this.hashPassword(this.passwordHash); + } + } + + @AfterCreate() + async afterCreate() { + // Send welcome email + await this.sendWelcomeEmail(); + } + + @BeforeDelete() + async beforeDelete() { + // Clean up user's data + await this.cleanupUserData(); + } + + private async hashPassword(password: string): Promise { + // Implementation of password hashing + const bcrypt = require('bcrypt'); + return await bcrypt.hash(password, 10); + } + + private async sendWelcomeEmail(): Promise { + // Implementation of welcome email + console.log(`Welcome email sent to ${this.username}`); + } + + private async cleanupUserData(): Promise { + // Implementation of data cleanup + console.log(`Cleaning up data for user ${this.username}`); + } +} +``` + +## Advanced Model Features + +### Computed Properties + +Create computed properties that are automatically calculated: + +```typescript +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + firstName: string; + + @Field({ type: 'string', required: true }) + lastName: string; + + @Field({ type: 'string', required: true }) + email: string; + + // Computed property + get fullName(): string { + return `${this.firstName} ${this.lastName}`; + } + + get emailDomain(): string { + return this.email.split('@')[1]; + } + + // Virtual field (not stored but serialized) + @Field({ type: 'string', virtual: true }) + get displayName(): string { + return this.fullName || this.email.split('@')[0]; + } +} +``` + +### Model Methods + +Add custom methods to your models: + +```typescript +export class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true }) + content: string; + + @Field({ type: 'array', required: false, default: [] }) + tags: string[]; + + @Field({ type: 'number', required: false, default: 0 }) + viewCount: number; + + // Instance methods + async incrementViews(): Promise { + this.viewCount += 1; + await this.save(); + } + + addTag(tag: string): void { + if (!this.tags.includes(tag)) { + this.tags.push(tag); + } + } + + removeTag(tag: string): void { + this.tags = this.tags.filter((t) => t !== tag); + } + + getWordCount(): number { + return this.content.split(/\s+/).length; + } + + // Static methods + static async findByTag(tag: string): Promise { + return await this.query().where('tags', 'includes', tag).find(); + } + + static async findPopular(limit: number = 10): Promise { + return await this.query().orderBy('viewCount', 'desc').limit(limit).find(); + } +} +``` + +### Model Serialization + +Control how models are serialized: + +```typescript +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + @Field({ type: 'string', required: true }) + email: string; + + @Field({ + type: 'string', + required: true, + serialize: false, // Don't include in serialization + }) + passwordHash: string; + + @Field({ type: 'string', required: false }) + profilePicture?: string; + + // Custom serialization + toJSON(): any { + const json = super.toJSON(); + + // Add computed fields + json.initials = this.getInitials(); + + // Remove sensitive data + delete json.passwordHash; + + return json; + } + + // Safe serialization for public APIs + toPublic(): any { + return { + id: this.id, + username: this.username, + profilePicture: this.profilePicture, + initials: this.getInitials(), + }; + } + + private getInitials(): string { + return this.username.substring(0, 2).toUpperCase(); + } +} +``` + +## Model Inheritance + +Create base models for common functionality: + +```typescript +// Base model with common fields +abstract class TimestampedModel extends BaseModel { + @Field({ type: 'number', required: false, default: () => Date.now() }) + createdAt: number; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + updatedAt: number; + + @BeforeUpdate() + updateTimestamp() { + this.updatedAt = Date.now(); + } +} + +// User model extending base +@Model({ scope: 'global', type: 'docstore' }) +export class User extends TimestampedModel { + @Field({ type: 'string', required: true, unique: true }) + username: string; + + @Field({ type: 'string', required: true, unique: true }) + email: string; +} + +// Post model extending base +@Model({ scope: 'user', type: 'docstore' }) +export class Post extends TimestampedModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true }) + content: string; + + @Field({ type: 'string', required: true }) + userId: string; +} +``` + +## Best Practices + +### Model Design + +1. **Use appropriate scoping**: Choose 'user' or 'global' scope based on your data access patterns +2. **Design for sharding**: Consider how your data will be distributed when choosing sharding keys +3. **Validate early**: Use field validation to catch errors early in the development process +4. **Use TypeScript**: Take advantage of TypeScript's type safety throughout your models + +### Performance Optimization + +1. **Index frequently queried fields**: Add indexes to fields you query often +2. **Use computed properties sparingly**: Heavy computations can impact performance +3. **Optimize serialization**: Only serialize the data you need +4. **Consider caching**: Use caching for expensive operations + +### Security Considerations + +1. **Validate all input**: Never trust user input without validation +2. **Sanitize data**: Clean data before storage +3. **Control serialization**: Be careful about what data you expose in APIs +4. **Use appropriate scoping**: User-scoped models provide better data isolation + +### Code Organization + +1. **Keep models focused**: Each model should have a single responsibility +2. **Use inheritance wisely**: Create base models for common functionality +3. **Document your models**: Use clear names and add comments for complex logic +4. **Test thoroughly**: Write comprehensive tests for your model logic + +This comprehensive model system provides the foundation for building scalable, maintainable decentralized applications with DebrosFramework. diff --git a/docs/docs/examples/basic-usage.md b/docs/docs/examples/basic-usage.md new file mode 100644 index 0000000..d2d805d --- /dev/null +++ b/docs/docs/examples/basic-usage.md @@ -0,0 +1,935 @@ +--- +sidebar_position: 1 +--- + +# Basic Usage Examples + +This guide provides practical examples of using DebrosFramework for common development tasks. These examples will help you understand how to implement typical application features using the framework. + +## Setting Up Your First Application + +### 1. Project Setup + +```bash +mkdir my-debros-app +cd my-debros-app +npm init -y +npm install debros-framework @orbitdb/core @helia/helia +npm install --save-dev typescript @types/node ts-node +``` + +### 2. Basic Configuration + +Create `src/config.ts`: + +```typescript +export const config = { + ipfs: { + // IPFS configuration + addresses: { + swarm: ['/ip4/0.0.0.0/tcp/4001'], + api: '/ip4/127.0.0.1/tcp/5001', + gateway: '/ip4/127.0.0.1/tcp/8080', + }, + }, + orbitdb: { + // OrbitDB configuration + directory: './orbitdb', + }, + framework: { + // Framework configuration + cacheSize: 1000, + enableQueryOptimization: true, + enableAutomaticPinning: true, + }, +}; +``` + +## Simple Blog Application + +Let's build a simple blog application to demonstrate basic DebrosFramework usage. + +### 1. Define Models + +Create `src/models/User.ts`: + +```typescript +import { BaseModel, Model, Field, HasMany, BeforeCreate, AfterCreate } from 'debros-framework'; +import { Post } from './Post'; + +@Model({ + scope: 'global', + type: 'docstore', + sharding: { + strategy: 'hash', + count: 4, + key: 'id', + }, +}) +export class User extends BaseModel { + @Field({ + type: 'string', + required: true, + unique: true, + minLength: 3, + maxLength: 20, + validate: (username: string) => /^[a-zA-Z0-9_]+$/.test(username), + }) + username: string; + + @Field({ + type: 'string', + required: true, + unique: true, + validate: (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), + transform: (email: string) => email.toLowerCase(), + }) + email: string; + + @Field({ type: 'string', required: false, maxLength: 500 }) + bio?: string; + + @Field({ type: 'string', required: false }) + avatarUrl?: string; + + @Field({ type: 'boolean', required: false, default: true }) + isActive: boolean; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + registeredAt: number; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + lastLoginAt: number; + + // Relationships + @HasMany(() => Post, 'authorId') + posts: Post[]; + + @BeforeCreate() + setupNewUser() { + this.registeredAt = Date.now(); + this.lastLoginAt = Date.now(); + this.isActive = true; + } + + @AfterCreate() + async afterUserCreated() { + console.log(`New user created: ${this.username}`); + // Here you could send a welcome email, create default settings, etc. + } + + // Helper methods + updateLastLogin() { + this.lastLoginAt = Date.now(); + return this.save(); + } + + async getPostCount(): Promise { + return await Post.query().where('authorId', this.id).count(); + } + + async getRecentPosts(limit: number = 5): Promise { + return await Post.query() + .where('authorId', this.id) + .orderBy('createdAt', 'desc') + .limit(limit) + .find(); + } +} +``` + +Create `src/models/Post.ts`: + +```typescript +import { + BaseModel, + Model, + Field, + BelongsTo, + HasMany, + BeforeCreate, + BeforeUpdate, +} from 'debros-framework'; +import { User } from './User'; +import { Comment } from './Comment'; + +@Model({ + scope: 'user', + type: 'docstore', + sharding: { + strategy: 'user', + count: 2, + key: 'authorId', + }, +}) +export class Post extends BaseModel { + @Field({ type: 'string', required: true, minLength: 1, maxLength: 200 }) + title: string; + + @Field({ type: 'string', required: true, minLength: 1, maxLength: 10000 }) + content: string; + + @Field({ type: 'string', required: true }) + authorId: string; + + @Field({ type: 'array', required: false, default: [] }) + tags: string[]; + + @Field({ type: 'boolean', required: false, default: false }) + isPublished: boolean; + + @Field({ type: 'number', required: false, default: 0 }) + viewCount: number; + + @Field({ type: 'number', required: false, default: 0 }) + likeCount: number; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + createdAt: number; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + updatedAt: number; + + @Field({ type: 'number', required: false }) + publishedAt?: number; + + // Relationships + @BelongsTo(() => User, 'authorId') + author: User; + + @HasMany(() => Comment, 'postId') + comments: Comment[]; + + @BeforeCreate() + setupNewPost() { + this.createdAt = Date.now(); + this.updatedAt = Date.now(); + this.viewCount = 0; + this.likeCount = 0; + } + + @BeforeUpdate() + updateTimestamp() { + this.updatedAt = Date.now(); + } + + // Helper methods + async publish(): Promise { + this.isPublished = true; + this.publishedAt = Date.now(); + await this.save(); + } + + async unpublish(): Promise { + this.isPublished = false; + this.publishedAt = undefined; + await this.save(); + } + + async incrementViews(): Promise { + this.viewCount += 1; + await this.save(); + } + + async like(): Promise { + this.likeCount += 1; + await this.save(); + } + + async unlike(): Promise { + if (this.likeCount > 0) { + this.likeCount -= 1; + await this.save(); + } + } + + addTag(tag: string): void { + const normalizedTag = tag.toLowerCase().trim(); + if (normalizedTag && !this.tags.includes(normalizedTag)) { + this.tags.push(normalizedTag); + } + } + + removeTag(tag: string): void { + this.tags = this.tags.filter((t) => t !== tag.toLowerCase().trim()); + } + + getExcerpt(length: number = 150): string { + if (this.content.length <= length) { + return this.content; + } + return this.content.substring(0, length).trim() + '...'; + } + + getReadingTime(): number { + const wordsPerMinute = 200; + const wordCount = this.content.split(/\s+/).length; + return Math.ceil(wordCount / wordsPerMinute); + } +} +``` + +Create `src/models/Comment.ts`: + +```typescript +import { BaseModel, Model, Field, BelongsTo, BeforeCreate } from 'debros-framework'; +import { User } from './User'; +import { Post } from './Post'; + +@Model({ + scope: 'user', + type: 'docstore', + sharding: { + strategy: 'user', + count: 2, + key: 'authorId', + }, +}) +export class Comment extends BaseModel { + @Field({ type: 'string', required: true, minLength: 1, maxLength: 1000 }) + content: string; + + @Field({ type: 'string', required: true }) + postId: string; + + @Field({ type: 'string', required: true }) + authorId: string; + + @Field({ type: 'boolean', required: false, default: true }) + isApproved: boolean; + + @Field({ type: 'number', required: false, default: 0 }) + likeCount: number; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + createdAt: number; + + // Relationships + @BelongsTo(() => Post, 'postId') + post: Post; + + @BelongsTo(() => User, 'authorId') + author: User; + + @BeforeCreate() + setupNewComment() { + this.createdAt = Date.now(); + this.isApproved = true; // Auto-approve for now + } + + // Helper methods + async approve(): Promise { + this.isApproved = true; + await this.save(); + } + + async reject(): Promise { + this.isApproved = false; + await this.save(); + } + + async like(): Promise { + this.likeCount += 1; + await this.save(); + } + + async unlike(): Promise { + if (this.likeCount > 0) { + this.likeCount -= 1; + await this.save(); + } + } +} +``` + +Create `src/models/index.ts`: + +```typescript +export { User } from './User'; +export { Post } from './Post'; +export { Comment } from './Comment'; +``` + +### 2. Application Service + +Create `src/BlogService.ts`: + +```typescript +import { DebrosFramework } from 'debros-framework'; +import { User, Post, Comment } from './models'; + +export class BlogService { + private framework: DebrosFramework; + + constructor(framework: DebrosFramework) { + this.framework = framework; + } + + // User operations + async createUser(userData: { + username: string; + email: string; + bio?: string; + avatarUrl?: string; + }): Promise { + return await User.create(userData); + } + + async getUserByUsername(username: string): Promise { + return await User.query().where('username', username).findOne(); + } + + async getUserById(id: string): Promise { + return await User.findById(id); + } + + async updateUserProfile( + userId: string, + updates: { + bio?: string; + avatarUrl?: string; + }, + ): Promise { + const user = await User.findById(userId); + if (!user) { + throw new Error('User not found'); + } + + if (updates.bio !== undefined) user.bio = updates.bio; + if (updates.avatarUrl !== undefined) user.avatarUrl = updates.avatarUrl; + + await user.save(); + return user; + } + + // Post operations + async createPost( + authorId: string, + postData: { + title: string; + content: string; + tags?: string[]; + }, + ): Promise { + const post = await Post.create({ + title: postData.title, + content: postData.content, + authorId, + tags: postData.tags || [], + }); + + return post; + } + + async getPostById(id: string): Promise { + const post = await Post.findById(id); + if (post) { + await post.incrementViews(); // Track view + } + return post; + } + + async getPostWithDetails(id: string): Promise { + return await Post.query().where('id', id).with(['author', 'comments.author']).findOne(); + } + + async getPublishedPosts( + options: { + page?: number; + limit?: number; + tag?: string; + authorId?: string; + } = {}, + ): Promise<{ posts: Post[]; total: number }> { + const { page = 1, limit = 10, tag, authorId } = options; + + let query = Post.query().where('isPublished', true).with(['author']); + + if (tag) { + query = query.where('tags', 'includes', tag); + } + + if (authorId) { + query = query.where('authorId', authorId); + } + + const result = await query.orderBy('publishedAt', 'desc').paginate(page, limit); + + return { + posts: result.data, + total: result.total, + }; + } + + async getUserPosts(userId: string, includeUnpublished: boolean = false): Promise { + let query = Post.query().where('authorId', userId); + + if (!includeUnpublished) { + query = query.where('isPublished', true); + } + + return await query.orderBy('createdAt', 'desc').find(); + } + + async updatePost( + postId: string, + updates: { + title?: string; + content?: string; + tags?: string[]; + }, + ): Promise { + const post = await Post.findById(postId); + if (!post) { + throw new Error('Post not found'); + } + + if (updates.title !== undefined) post.title = updates.title; + if (updates.content !== undefined) post.content = updates.content; + if (updates.tags !== undefined) post.tags = updates.tags; + + await post.save(); + return post; + } + + async deletePost(postId: string): Promise { + const post = await Post.findById(postId); + if (!post) { + throw new Error('Post not found'); + } + + // Delete associated comments first + const comments = await Comment.query().where('postId', postId).find(); + + for (const comment of comments) { + await comment.delete(); + } + + await post.delete(); + } + + // Comment operations + async createComment(authorId: string, postId: string, content: string): Promise { + const comment = await Comment.create({ + content, + postId, + authorId, + }); + + return comment; + } + + async getPostComments(postId: string): Promise { + return await Comment.query() + .where('postId', postId) + .where('isApproved', true) + .with(['author']) + .orderBy('createdAt', 'asc') + .find(); + } + + async deleteComment(commentId: string): Promise { + const comment = await Comment.findById(commentId); + if (!comment) { + throw new Error('Comment not found'); + } + + await comment.delete(); + } + + // Search and discovery + async searchPosts( + query: string, + options: { + limit?: number; + tags?: string[]; + } = {}, + ): Promise { + const { limit = 20, tags } = options; + + let searchQuery = Post.query() + .where('isPublished', true) + .where((q) => { + q.where('title', 'like', `%${query}%`).orWhere('content', 'like', `%${query}%`); + }); + + if (tags && tags.length > 0) { + searchQuery = searchQuery.where('tags', 'includes any', tags); + } + + return await searchQuery + .with(['author']) + .orderBy('likeCount', 'desc') + .orderBy('createdAt', 'desc') + .limit(limit) + .find(); + } + + async getPopularPosts(timeframe: 'day' | 'week' | 'month' | 'all' = 'week'): Promise { + let sinceTime: number; + + switch (timeframe) { + case 'day': + sinceTime = Date.now() - 24 * 60 * 60 * 1000; + break; + case 'week': + sinceTime = Date.now() - 7 * 24 * 60 * 60 * 1000; + break; + case 'month': + sinceTime = Date.now() - 30 * 24 * 60 * 60 * 1000; + break; + default: + sinceTime = 0; + } + + let query = Post.query().where('isPublished', true); + + if (sinceTime > 0) { + query = query.where('publishedAt', '>', sinceTime); + } + + return await query + .with(['author']) + .orderBy('likeCount', 'desc') + .orderBy('viewCount', 'desc') + .limit(10) + .find(); + } + + async getTags(): Promise> { + // Get all published posts + const posts = await Post.query().where('isPublished', true).select(['tags']).find(); + + // Count tags + const tagCounts = new Map(); + + posts.forEach((post) => { + post.tags.forEach((tag) => { + tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); + }); + }); + + // Convert to array and sort by count + return Array.from(tagCounts.entries()) + .map(([tag, count]) => ({ tag, count })) + .sort((a, b) => b.count - a.count); + } + + // Analytics + async getUserStats(userId: string): Promise<{ + postCount: number; + totalViews: number; + totalLikes: number; + commentCount: number; + }> { + const [posts, comments] = await Promise.all([ + Post.query().where('authorId', userId).find(), + Comment.query().where('authorId', userId).count(), + ]); + + const totalViews = posts.reduce((sum, post) => sum + post.viewCount, 0); + const totalLikes = posts.reduce((sum, post) => sum + post.likeCount, 0); + + return { + postCount: posts.length, + totalViews, + totalLikes, + commentCount: comments, + }; + } + + async getSystemStats(): Promise<{ + userCount: number; + postCount: number; + commentCount: number; + publishedPostCount: number; + }> { + const [userCount, postCount, commentCount, publishedPostCount] = await Promise.all([ + User.query().count(), + Post.query().count(), + Comment.query().count(), + Post.query().where('isPublished', true).count(), + ]); + + return { + userCount, + postCount, + commentCount, + publishedPostCount, + }; + } +} +``` + +### 3. Main Application + +Create `src/index.ts`: + +```typescript +import { DebrosFramework } from 'debros-framework'; +import { BlogService } from './BlogService'; +import { User, Post, Comment } from './models'; +import { setupServices } from './setup'; + +async function main() { + try { + console.log('🚀 Starting DebrosFramework Blog Application...'); + + // Initialize services + const { orbitDBService, ipfsService } = await setupServices(); + + // Initialize framework + const framework = new DebrosFramework(); + await framework.initialize(orbitDBService, ipfsService); + + console.log('✅ Framework initialized successfully'); + + // Create blog service + const blogService = new BlogService(framework); + + // Demo: Create sample data + await createSampleData(blogService); + + // Demo: Query data + await demonstrateQueries(blogService); + + // Keep running for demo + console.log('📝 Blog application is running...'); + console.log('Press Ctrl+C to stop'); + + // Graceful shutdown + process.on('SIGINT', async () => { + console.log('\n🛑 Shutting down...'); + await framework.stop(); + process.exit(0); + }); + } catch (error) { + console.error('❌ Application failed to start:', error); + process.exit(1); + } +} + +async function createSampleData(blogService: BlogService) { + console.log('\n📝 Creating sample data...'); + + // Create users + const alice = await blogService.createUser({ + username: 'alice', + email: 'alice@example.com', + bio: 'Tech enthusiast and blogger', + avatarUrl: 'https://example.com/avatars/alice.jpg', + }); + + const bob = await blogService.createUser({ + username: 'bob', + email: 'bob@example.com', + bio: 'Developer and writer', + }); + + console.log(`✅ Created users: ${alice.username}, ${bob.username}`); + + // Create posts + const post1 = await blogService.createPost(alice.id, { + title: 'Getting Started with DebrosFramework', + content: 'DebrosFramework makes it easy to build decentralized applications...', + tags: ['debros', 'tutorial', 'decentralized'], + }); + + const post2 = await blogService.createPost(bob.id, { + title: 'Building Scalable dApps', + content: 'Scalability is crucial for decentralized applications...', + tags: ['scaling', 'dapps', 'blockchain'], + }); + + // Publish posts + await post1.publish(); + await post2.publish(); + + console.log(`✅ Created and published posts: "${post1.title}", "${post2.title}"`); + + // Create comments + const comment1 = await blogService.createComment( + bob.id, + post1.id, + 'Great introduction to DebrosFramework!', + ); + + const comment2 = await blogService.createComment( + alice.id, + post2.id, + 'Very insightful article about scaling.', + ); + + console.log(`✅ Created ${[comment1, comment2].length} comments`); + + // Add some interactions + await post1.like(); + await post1.like(); + await post2.like(); + await comment1.like(); + + console.log('✅ Added sample interactions (likes)'); +} + +async function demonstrateQueries(blogService: BlogService) { + console.log('\n🔍 Demonstrating queries...'); + + // Get all published posts + const { posts, total } = await blogService.getPublishedPosts({ limit: 10 }); + console.log(`📚 Found ${total} published posts:`); + posts.forEach((post) => { + console.log( + ` - "${post.title}" by ${post.author.username} (${post.likeCount} likes, ${post.viewCount} views)`, + ); + }); + + // Search posts + const searchResults = await blogService.searchPosts('DebrosFramework'); + console.log(`\n🔍 Search results for "DebrosFramework": ${searchResults.length} posts`); + searchResults.forEach((post) => { + console.log(` - "${post.title}" by ${post.author.username}`); + }); + + // Get popular posts + const popularPosts = await blogService.getPopularPosts('week'); + console.log(`\n⭐ Popular posts this week: ${popularPosts.length}`); + popularPosts.forEach((post) => { + console.log(` - "${post.title}" (${post.likeCount} likes)`); + }); + + // Get tags + const tags = await blogService.getTags(); + console.log(`\n🏷️ Popular tags:`); + tags.slice(0, 5).forEach(({ tag, count }) => { + console.log(` - ${tag}: ${count} posts`); + }); + + // Get user stats + const users = await User.query().find(); + for (const user of users) { + const stats = await blogService.getUserStats(user.id); + console.log(`\n📊 Stats for ${user.username}:`); + console.log(` - Posts: ${stats.postCount}`); + console.log(` - Total views: ${stats.totalViews}`); + console.log(` - Total likes: ${stats.totalLikes}`); + console.log(` - Comments: ${stats.commentCount}`); + } + + // System stats + const systemStats = await blogService.getSystemStats(); + console.log(`\n🌐 System stats:`); + console.log(` - Users: ${systemStats.userCount}`); + console.log(` - Posts: ${systemStats.postCount} (${systemStats.publishedPostCount} published)`); + console.log(` - Comments: ${systemStats.commentCount}`); +} + +// Run the application +main().catch(console.error); +``` + +### 4. Service Setup + +Create `src/setup.ts`: + +```typescript +import { createHelia } from '@helia/helia'; +import { createOrbitDB } from '@orbitdb/core'; + +export async function setupServices() { + console.log('🔧 Setting up IPFS and OrbitDB services...'); + + // Create IPFS instance + const ipfs = await createHelia({ + // Configure as needed for your environment + }); + + // Create OrbitDB instance + const orbitdb = await createOrbitDB({ ipfs }); + + // Wrap services for DebrosFramework + const ipfsService = { + async init() { + /* Already initialized */ + }, + getHelia: () => ipfs, + getLibp2pInstance: () => ipfs.libp2p, + async stop() { + await ipfs.stop(); + }, + }; + + const orbitDBService = { + async init() { + /* Already initialized */ + }, + async openDB(name: string, type: string) { + return await orbitdb.open(name, { type }); + }, + getOrbitDB: () => orbitdb, + async stop() { + await orbitdb.stop(); + }, + }; + + console.log('✅ Services setup complete'); + + return { ipfsService, orbitDBService }; +} +``` + +### 5. Running the Application + +Add to your `package.json`: + +```json +{ + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts" + } +} +``` + +Run the application: + +```bash +npm run dev +``` + +## Key Concepts Demonstrated + +### 1. Model Definition + +- Using decorators to define models and fields +- Implementing validation and transformation +- Setting up relationships between models +- Using hooks for lifecycle management + +### 2. Database Scoping + +- Global models for shared data (User) +- User-scoped models for private data (Post, Comment) +- Automatic sharding based on model configuration + +### 3. Query Operations + +- Basic CRUD operations +- Complex queries with filtering and sorting +- Relationship loading (eager and lazy) +- Pagination and search functionality + +### 4. Business Logic + +- Service layer for application logic +- Model methods for domain-specific operations +- Data validation and transformation +- Analytics and reporting + +### 5. Error Handling + +- Graceful error handling in service methods +- Validation error handling +- Application lifecycle management + +This example provides a solid foundation for building more complex applications with DebrosFramework. You can extend it by adding features like user authentication, real-time notifications, file uploads, and more advanced search capabilities. diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md new file mode 100644 index 0000000..794dab1 --- /dev/null +++ b/docs/docs/getting-started.md @@ -0,0 +1,449 @@ +--- +sidebar_position: 2 +--- + +# Getting Started + +This guide will help you set up DebrosFramework and create your first decentralized application in just a few minutes. + +## Prerequisites + +Before you begin, make sure you have: + +- **Node.js** (version 18.0 or above) +- **npm** or **pnpm** package manager +- Basic knowledge of **TypeScript** and **decorators** +- Familiarity with **async/await** patterns + +## Installation + +### 1. Create a New Project + +```bash +mkdir my-debros-app +cd my-debros-app +npm init -y +``` + +### 2. Install @debros/network + +```bash +npm install @debros/network +npm install --save-dev typescript @types/node +``` + +### 3. Set Up TypeScript Configuration + +Create a `tsconfig.json` file: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### 4. Install OrbitDB and IPFS Dependencies + +DebrosFramework requires OrbitDB and IPFS services: + +```bash +npm install @orbitdb/core @helia/helia @helia/unixfs @libp2p/peer-id +``` + +## Your First Application + +Let's create a simple social media application to demonstrate DebrosFramework's capabilities. + +### 1. Create the Project Structure + +``` +src/ +├── models/ +│ ├── User.ts +│ ├── Post.ts +│ └── index.ts +├── services/ +│ ├── orbitdb.ts +│ └── ipfs.ts +└── index.ts +``` + +### 2. Set Up IPFS Service + +Create `src/services/ipfs.ts`: + +```typescript +import { createHelia } from '@helia/helia'; +import { createLibp2p } from 'libp2p'; +// Add other necessary imports based on your setup + +export class IPFSService { + private helia: any; + private libp2p: any; + + async init(): Promise { + // Initialize your IPFS/Helia instance + // This is a simplified example - customize based on your needs + this.libp2p = await createLibp2p({ + // Your libp2p configuration + }); + + this.helia = await createHelia({ + libp2p: this.libp2p, + }); + } + + getHelia() { + return this.helia; + } + + getLibp2pInstance() { + return this.libp2p; + } + + async stop(): Promise { + if (this.helia) { + await this.helia.stop(); + } + } +} +``` + +### 3. Set Up OrbitDB Service + +Create `src/services/orbitdb.ts`: + +```typescript +import { createOrbitDB } from '@orbitdb/core'; + +export class OrbitDBService { + private orbitdb: any; + private ipfs: any; + + constructor(ipfsService: any) { + this.ipfs = ipfsService; + } + + async init(): Promise { + this.orbitdb = await createOrbitDB({ + ipfs: this.ipfs.getHelia(), + // Add other OrbitDB configuration options + }); + } + + async openDB(name: string, type: string): Promise { + return await this.orbitdb.open(name, { type }); + } + + getOrbitDB() { + return this.orbitdb; + } + + async stop(): Promise { + if (this.orbitdb) { + await this.orbitdb.stop(); + } + } +} +``` + +### 4. Define Your First Model + +Create `src/models/User.ts`: + +```typescript +import { BaseModel, Model, Field, HasMany } from 'debros-framework'; +import { Post } from './Post'; + +@Model({ + scope: 'global', // Global model - shared across all users + type: 'docstore', + sharding: { + strategy: 'hash', + count: 4, + key: 'id', + }, +}) +export class User extends BaseModel { + @Field({ + type: 'string', + required: true, + unique: true, + validate: (value: string) => value.length >= 3 && value.length <= 20, + }) + username: string; + + @Field({ + type: 'string', + required: true, + unique: true, + validate: (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), + }) + email: string; + + @Field({ type: 'string', required: false }) + bio?: string; + + @Field({ type: 'string', required: false }) + profilePicture?: string; + + @Field({ type: 'boolean', required: false, default: true }) + isActive: boolean; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + registeredAt: number; + + // Relationship: One user has many posts + @HasMany(() => Post, 'userId') + posts: Post[]; +} +``` + +### 5. Create a Post Model + +Create `src/models/Post.ts`: + +```typescript +import { BaseModel, Model, Field, BelongsTo } from 'debros-framework'; +import { User } from './User'; + +@Model({ + scope: 'user', // User-scoped model - each user has their own database + type: 'docstore', + sharding: { + strategy: 'user', + count: 2, + key: 'userId', + }, +}) +export class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ + type: 'string', + required: true, + validate: (value: string) => value.length <= 5000, + }) + content: string; + + @Field({ type: 'string', required: true }) + userId: string; + + @Field({ type: 'array', required: false, default: [] }) + tags: string[]; + + @Field({ type: 'boolean', required: false, default: true }) + isPublic: boolean; + + @Field({ type: 'number', required: false, default: 0 }) + likeCount: number; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + createdAt: number; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + updatedAt: number; + + // Relationship: Post belongs to a user + @BelongsTo(() => User, 'userId') + user: User; +} +``` + +### 6. Export Your Models + +Create `src/models/index.ts`: + +```typescript +export { User } from './User'; +export { Post } from './Post'; +``` + +### 7. Create the Main Application + +Create `src/index.ts`: + +```typescript +import { DebrosFramework } from 'debros-framework'; +import { IPFSService } from './services/ipfs'; +import { OrbitDBService } from './services/orbitdb'; +import { User, Post } from './models'; + +async function main() { + // Initialize services + const ipfsService = new IPFSService(); + await ipfsService.init(); + + const orbitDBService = new OrbitDBService(ipfsService); + await orbitDBService.init(); + + // Initialize DebrosFramework + const framework = new DebrosFramework(); + await framework.initialize(orbitDBService, ipfsService); + + console.log('🚀 DebrosFramework initialized successfully!'); + + // Create a user + const user = await User.create({ + username: 'alice', + email: 'alice@example.com', + bio: 'Hello, I am Alice!', + isActive: true, + }); + + console.log('✅ Created user:', user.id); + + // Create a post for the user + const post = await Post.create({ + title: 'My First Post', + content: 'This is my first post using DebrosFramework!', + userId: user.id, + tags: ['introduction', 'debros'], + isPublic: true, + }); + + console.log('✅ Created post:', post.id); + + // Query users with their posts + const usersWithPosts = await User.query().where('isActive', true).with(['posts']).find(); + + console.log('📊 Users with posts:'); + usersWithPosts.forEach((user) => { + console.log(`- ${user.username}: ${user.posts.length} posts`); + }); + + // Find posts by tags + const taggedPosts = await Post.query() + .where('tags', 'includes', 'debros') + .with(['user']) + .orderBy('createdAt', 'desc') + .find(); + + console.log('🏷️ Posts tagged with "debros":'); + taggedPosts.forEach((post) => { + console.log(`- "${post.title}" by ${post.user.username}`); + }); + + // Clean up + await framework.stop(); + console.log('👋 Framework stopped successfully'); +} + +main().catch(console.error); +``` + +### 8. Add Package.json Scripts + +Update your `package.json`: + +```json +{ + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts" + } +} +``` + +### 9. Install Additional Development Dependencies + +```bash +npm install --save-dev ts-node +``` + +## Running Your Application + +### 1. Build the Project + +```bash +npm run build +``` + +### 2. Run the Application + +```bash +npm start +``` + +Or for development with hot reloading: + +```bash +npm run dev +``` + +You should see output similar to: + +``` +🚀 DebrosFramework initialized successfully! +✅ Created user: user_abc123 +✅ Created post: post_def456 +📊 Users with posts: +- alice: 1 posts +🏷️ Posts tagged with "debros": +- "My First Post" by alice +👋 Framework stopped successfully +``` + +## What's Next? + +Congratulations! You've successfully created your first DebrosFramework application. Here's what you can explore next: + +### Learn Core Concepts + +- [Architecture Overview](./core-concepts/architecture) - Understand how DebrosFramework works +- [Models and Decorators](./core-concepts/models) - Deep dive into model definition +- [Database Management](./core-concepts/database-management) - Learn about user-scoped vs global databases + +### Explore Advanced Features + +- [Query System](./query-system/query-builder) - Build complex queries +- [Relationships](./query-system/relationships) - Work with model relationships +- [Automatic Pinning](./advanced/automatic-pinning) - Optimize data availability +- [Migrations](./advanced/migrations) - Evolve your schema over time + +### Check Out Examples + +- [Social Platform Example](./examples/social-platform) - Complete social media application +- [Complex Queries](./examples/complex-queries) - Advanced query patterns +- [Migration Examples](./examples/migrations) - Schema evolution patterns + +## Common Issues + +### TypeScript Decorator Errors + +Make sure you have `"experimentalDecorators": true` and `"emitDecoratorMetadata": true` in your `tsconfig.json`. + +### IPFS/OrbitDB Connection Issues + +Ensure your IPFS and OrbitDB services are properly configured. Check the console for connection errors and verify your network configuration. + +### Model Registration Issues + +Models are automatically registered when imported. Make sure you're importing your models before using DebrosFramework. + +## Need Help? + +- 📖 Check our [comprehensive documentation](./core-concepts/architecture) +- 💻 Browse [example code](https://github.com/debros/network/tree/main/examples) +- 💬 Join our [Discord community](#) +- 📧 Contact [support](#) + +Happy coding with DebrosFramework! 🎉 diff --git a/docs/docs/intro.md b/docs/docs/intro.md new file mode 100644 index 0000000..bdcee8b --- /dev/null +++ b/docs/docs/intro.md @@ -0,0 +1,121 @@ +--- +sidebar_position: 1 +--- + +# Welcome to DebrosFramework + +**DebrosFramework** is a powerful Node.js framework that provides an ORM-like abstraction over OrbitDB and IPFS, making it easy to build scalable decentralized applications. + +## What is DebrosFramework? + +DebrosFramework simplifies the development of decentralized applications by providing: + +- **Model-based Abstraction**: Define your data models using decorators and TypeScript classes +- **Automatic Database Management**: Handle user-scoped and global databases automatically +- **Smart Sharding**: Distribute data across multiple databases for scalability +- **Advanced Query System**: Rich query capabilities with relationship loading and caching +- **Automatic Features**: Built-in pinning strategies and PubSub event publishing +- **Migration System**: Schema evolution and data transformation capabilities +- **Type Safety**: Full TypeScript support with strong typing throughout + +## Key Features + +### 🏗️ Model-Driven Development + +Define your data models using familiar decorator patterns: + +```typescript +@Model({ + scope: 'user', + type: 'docstore', + sharding: { strategy: 'hash', count: 4, key: 'userId' }, +}) +class User extends BaseModel { + @Field({ type: 'string', required: true, unique: true }) + username: string; + + @Field({ type: 'string', required: true, unique: true }) + email: string; + + @HasMany(() => Post, 'userId') + posts: Post[]; +} +``` + +### 🔍 Powerful Query System + +Build complex queries with relationship loading: + +```typescript +const users = await User.query() + .where('isActive', true) + .where('registeredAt', '>', Date.now() - 30 * 24 * 60 * 60 * 1000) + .with(['posts', 'followers']) + .orderBy('username') + .limit(20) + .find(); +``` + +### 🚀 Automatic Scaling + +Handle millions of users with automatic sharding and pinning: + +```typescript +// Framework automatically: +// - Creates user-scoped databases +// - Distributes data across shards +// - Manages pinning strategies +// - Optimizes query routing +``` + +### 🔄 Schema Evolution + +Migrate your data structures safely: + +```typescript +const migration = createMigration('add_user_profiles', '1.1.0') + .addField('User', 'profilePicture', { type: 'string', required: false }) + .addField('User', 'bio', { type: 'string', required: false }) + .transformData('User', (user) => ({ + ...user, + displayName: user.username || 'Anonymous', + })) + .build(); +``` + +## Architecture Overview + +DebrosFramework is built around several core components: + +1. **Models & Decorators**: Define your data structure and behavior +2. **Database Manager**: Handles database creation and management +3. **Shard Manager**: Distributes data across multiple databases +4. **Query System**: Processes queries with optimization and caching +5. **Relationship Manager**: Handles complex relationships between models +6. **Migration System**: Manages schema evolution over time +7. **Automatic Features**: Pinning, PubSub, and performance optimization + +## Who Should Use DebrosFramework? + +DebrosFramework is perfect for developers who want to: + +- Build decentralized applications without dealing with low-level OrbitDB complexities +- Create scalable applications that can handle millions of users +- Use familiar ORM patterns in a decentralized environment +- Implement complex data relationships in distributed systems +- Focus on business logic rather than infrastructure concerns + +## Getting Started + +Ready to build your first decentralized application? Check out our [Getting Started Guide](./getting-started) to set up your development environment and create your first models. + +## Community and Support + +- 📖 [Documentation](./getting-started) - Comprehensive guides and examples +- 💻 [GitHub Repository](https://github.com/debros/network) - Source code and issue tracking +- 💬 [Discord Community](#) - Chat with other developers +- 📧 [Support Email](#) - Get help from the core team + +--- + +_DebrosFramework is designed to make decentralized application development as simple as traditional web development, while providing the benefits of distributed systems._ diff --git a/docs/docs/query-system/query-builder.md b/docs/docs/query-system/query-builder.md new file mode 100644 index 0000000..6d47a6c --- /dev/null +++ b/docs/docs/query-system/query-builder.md @@ -0,0 +1,528 @@ +--- +sidebar_position: 1 +--- + +# Query Builder + +The DebrosFramework Query Builder provides a powerful, type-safe, and intuitive API for querying your decentralized data. It supports complex filtering, ordering, pagination, and relationship loading across distributed databases. + +## Basic Query Syntax + +### Simple Queries + +```typescript +import { User, Post } from './models'; + +// Find all users +const allUsers = await User.query().find(); + +// Find a single user +const user = await User.query().findOne(); + +// Find user by ID +const userById = await User.findById('user_123'); + +// Count users +const userCount = await User.query().count(); + +// Check if any users exist +const hasUsers = await User.query().exists(); +``` + +### Where Clauses + +The query builder supports various where clause operators: + +```typescript +// Basic equality +const activeUsers = await User.query().where('isActive', true).find(); + +// Comparison operators +const recentPosts = await Post.query() + .where('createdAt', '>', Date.now() - 24 * 60 * 60 * 1000) + .find(); + +const popularPosts = await Post.query().where('likeCount', '>=', 100).find(); + +// Multiple conditions (AND) +const recentPopularPosts = await Post.query() + .where('createdAt', '>', Date.now() - 24 * 60 * 60 * 1000) + .where('likeCount', '>=', 50) + .where('isPublished', true) + .find(); + +// IN operator +const specificUsers = await User.query().where('id', 'in', ['user_1', 'user_2', 'user_3']).find(); + +// NOT IN operator +const excludedUsers = await User.query().where('username', 'not in', ['admin', 'test']).find(); + +// LIKE operator (for strings) +const usersStartingWithA = await User.query().where('username', 'like', 'a%').find(); + +// Regular expressions +const emailUsers = await User.query() + .where('email', 'regex', /@gmail\.com$/) + .find(); + +// Null checks +const usersWithoutBio = await User.query().where('bio', 'is null').find(); + +const usersWithBio = await User.query().where('bio', 'is not null').find(); + +// Array operations +const techPosts = await Post.query().where('tags', 'includes', 'technology').find(); + +const multipleTags = await Post.query() + .where('tags', 'includes any', ['tech', 'programming', 'code']) + .find(); + +const allTags = await Post.query().where('tags', 'includes all', ['react', 'typescript']).find(); +``` + +#### Supported Where Operators + +| Operator | Description | Example | +| -------------- | ------------------------- | -------------------------------------------- | +| `=` or `eq` | Equal to | `.where('status', 'active')` | +| `!=` or `ne` | Not equal to | `.where('status', '!=', 'deleted')` | +| `>` or `gt` | Greater than | `.where('score', '>', 100)` | +| `>=` or `gte` | Greater than or equal | `.where('score', '>=', 100)` | +| `<` or `lt` | Less than | `.where('age', '<', 18)` | +| `<=` or `lte` | Less than or equal | `.where('age', '<=', 65)` | +| `in` | In array | `.where('id', 'in', ['1', '2'])` | +| `not in` | Not in array | `.where('status', 'not in', ['deleted'])` | +| `like` | Pattern matching | `.where('name', 'like', 'John%')` | +| `regex` | Regular expression | `.where('email', 'regex', /@gmail/)` | +| `is null` | Is null | `.where('deletedAt', 'is null')` | +| `is not null` | Is not null | `.where('email', 'is not null')` | +| `includes` | Array includes value | `.where('tags', 'includes', 'tech')` | +| `includes any` | Array includes any value | `.where('tags', 'includes any', ['a', 'b'])` | +| `includes all` | Array includes all values | `.where('tags', 'includes all', ['a', 'b'])` | + +### OR Conditions + +Use `orWhere` to create OR conditions: + +```typescript +// Users who are either active OR have logged in recently +const relevantUsers = await User.query() + .where('isActive', true) + .orWhere('lastLoginAt', '>', Date.now() - 7 * 24 * 60 * 60 * 1000) + .find(); + +// Complex OR conditions with grouping +const complexQuery = await Post.query() + .where('isPublished', true) + .where((query) => { + query.where('category', 'tech').orWhere('category', 'programming'); + }) + .orWhere((query) => { + query.where('likeCount', '>', 100).where('commentCount', '>', 10); + }) + .find(); +``` + +### Query Grouping + +Group conditions using nested query builders: + +```typescript +// (status = 'active' OR status = 'pending') AND (role = 'admin' OR role = 'moderator') +const privilegedUsers = await User.query() + .where((query) => { + query.where('status', 'active').orWhere('status', 'pending'); + }) + .where((query) => { + query.where('role', 'admin').orWhere('role', 'moderator'); + }) + .find(); + +// Complex nested conditions +const complexPosts = await Post.query() + .where('isPublished', true) + .where((query) => { + query + .where((subQuery) => { + subQuery.where('category', 'tech').where('difficulty', 'beginner'); + }) + .orWhere((subQuery) => { + subQuery.where('category', 'tutorial').where('likeCount', '>', 50); + }); + }) + .find(); +``` + +## Ordering and Pagination + +### Ordering Results + +```typescript +// Order by single field +const usersByName = await User.query().orderBy('username').find(); + +// Order by multiple fields +const postsByPopularity = await Post.query() + .orderBy('likeCount', 'desc') + .orderBy('createdAt', 'desc') + .find(); + +// Order by computed fields +const usersByActivity = await User.query() + .orderBy('lastLoginAt', 'desc') + .orderBy('username', 'asc') + .find(); + +// Random ordering +const randomPosts = await Post.query().orderBy('random').limit(10).find(); +``` + +### Pagination + +```typescript +// Limit results +const latestPosts = await Post.query().orderBy('createdAt', 'desc').limit(10).find(); + +// Offset pagination +const secondPage = await Post.query().orderBy('createdAt', 'desc').limit(10).offset(10).find(); + +// Cursor-based pagination (more efficient for large datasets) +const cursorPosts = await Post.query() + .orderBy('createdAt', 'desc') + .after('cursor_value_here') + .limit(10) + .find(); + +// Get pagination info +const paginatedResult = await Post.query().orderBy('createdAt', 'desc').paginate(1, 20); // page 1, 20 items per page + +console.log(paginatedResult.data); // Results +console.log(paginatedResult.total); // Total count +console.log(paginatedResult.page); // Current page +console.log(paginatedResult.perPage); // Items per page +console.log(paginatedResult.totalPages); // Total pages +``` + +## Relationship Loading + +### Eager Loading + +Load relationships along with the main query: + +```typescript +// Load user with their posts +const usersWithPosts = await User.query().with(['posts']).find(); + +// Load nested relationships +const usersWithPostsAndComments = await User.query().with(['posts.comments']).find(); + +// Load multiple relationships +const fullUserData = await User.query().with(['posts', 'comments', 'followers']).find(); + +// Load relationships with conditions +const usersWithRecentPosts = await User.query() + .with(['posts'], (query) => { + query + .where('createdAt', '>', Date.now() - 30 * 24 * 60 * 60 * 1000) + .orderBy('createdAt', 'desc') + .limit(5); + }) + .find(); + +// Complex relationship loading +const complexUserData = await User.query() + .with([ + 'posts.comments.user', // Deep nested relationships + 'followers.posts', // Multiple levels + 'settings', // Simple relationship + ]) + .find(); +``` + +### Lazy Loading + +Relationships are loaded automatically when accessed: + +```typescript +const user = await User.findById('user_123'); + +// Posts are loaded when first accessed +console.log(user.posts.length); // Triggers lazy load + +// Subsequent access uses cached data +user.posts.forEach((post) => console.log(post.title)); +``` + +### Relationship Constraints + +Apply constraints to relationship loading: + +```typescript +// Load users with their published posts only +const usersWithPublishedPosts = await User.query() + .with(['posts'], (query) => { + query.where('isPublished', true).orderBy('publishedAt', 'desc'); + }) + .find(); + +// Load posts with recent comments only +const postsWithRecentComments = await Post.query() + .with(['comments'], (query) => { + query + .where('createdAt', '>', Date.now() - 24 * 60 * 60 * 1000) + .with(['user']) // Load comment users too + .orderBy('createdAt', 'desc'); + }) + .find(); + +// Load users with follower count > 100 +const popularUsers = await User.query() + .with(['followers'], (query) => { + query.limit(100); // Limit followers loaded + }) + .having('followers.length', '>', 100) + .find(); +``` + +## Advanced Query Features + +### Aggregation + +```typescript +// Count records +const userCount = await User.query().where('isActive', true).count(); + +// Count distinct values +const uniqueCategories = await Post.query().countDistinct('category'); + +// Sum values +const totalLikes = await Post.query().sum('likeCount'); + +// Average values +const averageScore = await User.query().average('score'); + +// Min/Max values +const oldestPost = await Post.query().min('createdAt'); + +const newestPost = await Post.query().max('createdAt'); + +// Group by aggregations +const postsByCategory = await Post.query() + .groupBy('category') + .select(['category', 'COUNT(*) as count', 'AVG(likeCount) as avgLikes']) + .find(); +``` + +### Having Clauses + +Use `having` for filtering aggregated results: + +```typescript +// Categories with more than 10 posts +const popularCategories = await Post.query() + .groupBy('category') + .having('COUNT(*)', '>', 10) + .select(['category', 'COUNT(*) as postCount']) + .find(); + +// Users with high average post likes +const influentialUsers = await User.query() + .join('posts', 'users.id', 'posts.userId') + .groupBy('users.id') + .having('AVG(posts.likeCount)', '>', 50) + .select(['users.*', 'AVG(posts.likeCount) as avgLikes']) + .find(); +``` + +### Subqueries + +```typescript +// Users who have posts +const usersWithPosts = await User.query() + .whereExists(User.query().select('1').from('posts').whereColumn('posts.userId', 'users.id')) + .find(); + +// Users who don't have posts +const usersWithoutPosts = await User.query() + .whereNotExists(User.query().select('1').from('posts').whereColumn('posts.userId', 'users.id')) + .find(); + +// Posts with above-average like count +const popularPosts = await Post.query() + .where('likeCount', '>', (query) => { + query.select('AVG(likeCount)').from('posts'); + }) + .find(); +``` + +### Raw Queries + +For complex queries that can't be expressed with the query builder: + +```typescript +// Raw where clause +const complexUsers = await User.query() + .whereRaw('score > ? AND (status = ? OR created_at > ?)', [ + 100, + 'active', + Date.now() - 30 * 24 * 60 * 60 * 1000, + ]) + .find(); + +// Raw select +const userStats = await User.query() + .selectRaw('username, COUNT(posts.id) as post_count, AVG(posts.like_count) as avg_likes') + .join('posts', 'users.id', 'posts.user_id') + .groupBy('users.id', 'username') + .find(); + +// Completely raw query +const customResults = await User.raw( + ` + SELECT u.username, COUNT(p.id) as posts + FROM users u + LEFT JOIN posts p ON u.id = p.user_id + WHERE u.created_at > ? + GROUP BY u.id, u.username + HAVING COUNT(p.id) > ? +`, + [Date.now() - 30 * 24 * 60 * 60 * 1000, 5], +); +``` + +## Query Caching + +### Basic Caching + +```typescript +// Cache query results for 5 minutes +const cachedUsers = await User.query() + .where('isActive', true) + .cache(300) // 300 seconds + .find(); + +// Cache with custom key +const cachedPosts = await Post.query() + .where('category', 'tech') + .cache(600, 'tech-posts') // Custom cache key + .find(); + +// Disable caching for sensitive data +const sensitiveData = await User.query().where('role', 'admin').noCache().find(); +``` + +### Cache Tags + +Use cache tags for intelligent cache invalidation: + +```typescript +// Tag cache entries +const taggedQuery = await Post.query() + .where('category', 'tech') + .cacheTag(['posts', 'tech-posts']) + .cache(600) + .find(); + +// Invalidate tagged cache entries +await Post.invalidateCacheTag('tech-posts'); +``` + +## Query Optimization + +### Indexes + +Ensure your frequently queried fields are indexed: + +```typescript +@Model({ + indexes: [ + { fields: ['username'], unique: true }, + { fields: ['email'], unique: true }, + { fields: ['createdAt', 'isActive'] }, + { fields: ['category', 'publishedAt'] }, + ], +}) +export class User extends BaseModel { + // Model definition +} +``` + +### Query Hints + +Provide hints to the query optimizer: + +```typescript +// Hint about expected result size +const users = await User.query() + .where('isActive', true) + .hint('small_result_set') // Expects < 100 results + .find(); + +// Hint about query pattern +const recentPosts = await Post.query() + .where('createdAt', '>', Date.now() - 24 * 60 * 60 * 1000) + .hint('time_range_query') + .orderBy('createdAt', 'desc') + .find(); +``` + +### Batch Operations + +Optimize multiple queries with batching: + +```typescript +// Batch multiple queries +const [users, posts, comments] = await Promise.all([ + User.query().where('isActive', true).find(), + Post.query().where('isPublished', true).find(), + Comment.query().where('isApproved', true).find(), +]); + +// Batch with shared cache +const batchResults = await Query.batch() + .add('users', User.query().where('isActive', true)) + .add('posts', Post.query().where('isPublished', true)) + .add('comments', Comment.query().where('isApproved', true)) + .cache(300) + .execute(); +``` + +## Error Handling + +```typescript +try { + const users = await User.query() + .where('invalidField', 'value') // This might cause an error + .find(); +} catch (error) { + if (error.code === 'INVALID_FIELD') { + console.error('Invalid field in query:', error.field); + } else if (error.code === 'NETWORK_ERROR') { + console.error('Network error during query:', error.message); + } else { + console.error('Unexpected error:', error); + } +} + +// Query with error recovery +const safeUsers = await User.query() + .where('isActive', true) + .fallback(() => { + // Fallback to cache if query fails + return User.fromCache('active-users') || []; + }) + .find(); +``` + +## Performance Best Practices + +1. **Use indexes** for frequently queried fields +2. **Limit results** with `.limit()` to avoid loading large datasets +3. **Use caching** for expensive or repeated queries +4. **Eager load relationships** when you know you'll need them +5. **Use pagination** for large result sets +6. **Monitor query performance** and optimize slow queries +7. **Batch related queries** when possible +8. **Use appropriate data types** and avoid unnecessary type conversions + +The DebrosFramework Query Builder provides a powerful and flexible way to query your decentralized data while maintaining type safety and performance across distributed systems. diff --git a/docs/docs/tutorial-basics/_category_.json b/docs/docs/tutorial-basics/_category_.json new file mode 100644 index 0000000..2e6db55 --- /dev/null +++ b/docs/docs/tutorial-basics/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Tutorial - Basics", + "position": 2, + "link": { + "type": "generated-index", + "description": "5 minutes to learn the most important Docusaurus concepts." + } +} diff --git a/docs/docs/tutorial-basics/congratulations.md b/docs/docs/tutorial-basics/congratulations.md new file mode 100644 index 0000000..04771a0 --- /dev/null +++ b/docs/docs/tutorial-basics/congratulations.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 6 +--- + +# Congratulations! + +You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. + +Docusaurus has **much more to offer**! + +Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. + +Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) + +## What's next? + +- Read the [official documentation](https://docusaurus.io/) +- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) +- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) +- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) +- Add a [search bar](https://docusaurus.io/docs/search) +- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) +- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) diff --git a/docs/docs/tutorial-basics/create-a-blog-post.md b/docs/docs/tutorial-basics/create-a-blog-post.md new file mode 100644 index 0000000..550ae17 --- /dev/null +++ b/docs/docs/tutorial-basics/create-a-blog-post.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 3 +--- + +# Create a Blog Post + +Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... + +## Create your first Post + +Create a file at `blog/2021-02-28-greetings.md`: + +```md title="blog/2021-02-28-greetings.md" +--- +slug: greetings +title: Greetings! +authors: + - name: Joel Marcey + title: Co-creator of Docusaurus 1 + url: https://github.com/JoelMarcey + image_url: https://github.com/JoelMarcey.png + - name: Sébastien Lorber + title: Docusaurus maintainer + url: https://sebastienlorber.com + image_url: https://github.com/slorber.png +tags: [greetings] +--- + +Congratulations, you have made your first post! + +Feel free to play around and edit this post as much as you like. +``` + +A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). diff --git a/docs/docs/tutorial-basics/create-a-document.md b/docs/docs/tutorial-basics/create-a-document.md new file mode 100644 index 0000000..c22fe29 --- /dev/null +++ b/docs/docs/tutorial-basics/create-a-document.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 2 +--- + +# Create a Document + +Documents are **groups of pages** connected through: + +- a **sidebar** +- **previous/next navigation** +- **versioning** + +## Create your first Doc + +Create a Markdown file at `docs/hello.md`: + +```md title="docs/hello.md" +# Hello + +This is my **first Docusaurus document**! +``` + +A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). + +## Configure the Sidebar + +Docusaurus automatically **creates a sidebar** from the `docs` folder. + +Add metadata to customize the sidebar label and position: + +```md title="docs/hello.md" {1-4} +--- +sidebar_label: 'Hi!' +sidebar_position: 3 +--- + +# Hello + +This is my **first Docusaurus document**! +``` + +It is also possible to create your sidebar explicitly in `sidebars.js`: + +```js title="sidebars.js" +export default { + tutorialSidebar: [ + 'intro', + // highlight-next-line + 'hello', + { + type: 'category', + label: 'Tutorial', + items: ['tutorial-basics/create-a-document'], + }, + ], +}; +``` diff --git a/docs/docs/tutorial-basics/create-a-page.md b/docs/docs/tutorial-basics/create-a-page.md new file mode 100644 index 0000000..20e2ac3 --- /dev/null +++ b/docs/docs/tutorial-basics/create-a-page.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 1 +--- + +# Create a Page + +Add **Markdown or React** files to `src/pages` to create a **standalone page**: + +- `src/pages/index.js` → `localhost:3000/` +- `src/pages/foo.md` → `localhost:3000/foo` +- `src/pages/foo/bar.js` → `localhost:3000/foo/bar` + +## Create your first React Page + +Create a file at `src/pages/my-react-page.js`: + +```jsx title="src/pages/my-react-page.js" +import React from 'react'; +import Layout from '@theme/Layout'; + +export default function MyReactPage() { + return ( + +

My React page

+

This is a React page

+
+ ); +} +``` + +A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). + +## Create your first Markdown Page + +Create a file at `src/pages/my-markdown-page.md`: + +```mdx title="src/pages/my-markdown-page.md" +# My Markdown page + +This is a Markdown page +``` + +A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). diff --git a/docs/docs/tutorial-basics/deploy-your-site.md b/docs/docs/tutorial-basics/deploy-your-site.md new file mode 100644 index 0000000..1c50ee0 --- /dev/null +++ b/docs/docs/tutorial-basics/deploy-your-site.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 5 +--- + +# Deploy your site + +Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). + +It builds your site as simple **static HTML, JavaScript and CSS files**. + +## Build your site + +Build your site **for production**: + +```bash +npm run build +``` + +The static files are generated in the `build` folder. + +## Deploy your site + +Test your production build locally: + +```bash +npm run serve +``` + +The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). + +You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). diff --git a/docs/docs/tutorial-basics/markdown-features.mdx b/docs/docs/tutorial-basics/markdown-features.mdx new file mode 100644 index 0000000..35e0082 --- /dev/null +++ b/docs/docs/tutorial-basics/markdown-features.mdx @@ -0,0 +1,152 @@ +--- +sidebar_position: 4 +--- + +# Markdown Features + +Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. + +## Front Matter + +Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/): + +```text title="my-doc.md" +// highlight-start +--- +id: my-doc-id +title: My document title +description: My document description +slug: /my-custom-url +--- +// highlight-end + +## Markdown heading + +Markdown text with [links](./hello.md) +``` + +## Links + +Regular Markdown links are supported, using url paths or relative file paths. + +```md +Let's see how to [Create a page](/create-a-page). +``` + +```md +Let's see how to [Create a page](./create-a-page.md). +``` + +**Result:** Let's see how to [Create a page](./create-a-page.md). + +## Images + +Regular Markdown images are supported. + +You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`): + +```md +![Docusaurus logo](/img/docusaurus.png) +``` + +![Docusaurus logo](/img/docusaurus.png) + +You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them: + +```md +![Docusaurus logo](./img/docusaurus.png) +``` + +## Code Blocks + +Markdown code blocks are supported with Syntax highlighting. + +````md +```jsx title="src/components/HelloDocusaurus.js" +function HelloDocusaurus() { + return

Hello, Docusaurus!

; +} +``` +```` + +```jsx title="src/components/HelloDocusaurus.js" +function HelloDocusaurus() { + return

Hello, Docusaurus!

; +} +``` + +## Admonitions + +Docusaurus has a special syntax to create admonitions and callouts: + +```md +:::tip My tip + +Use this awesome feature option + +::: + +:::danger Take care + +This action is dangerous + +::: +``` + +:::tip My tip + +Use this awesome feature option + +::: + +:::danger Take care + +This action is dangerous + +::: + +## MDX and React Components + +[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**: + +```jsx +export const Highlight = ({children, color}) => ( + { + alert(`You clicked the color ${color} with label ${children}`) + }}> + {children} + +); + +This is Docusaurus green ! + +This is Facebook blue ! +``` + +export const Highlight = ({children, color}) => ( + { + alert(`You clicked the color ${color} with label ${children}`); + }}> + {children} + +); + +This is Docusaurus green ! + +This is Facebook blue ! diff --git a/docs/docs/tutorial-extras/_category_.json b/docs/docs/tutorial-extras/_category_.json new file mode 100644 index 0000000..a8ffcc1 --- /dev/null +++ b/docs/docs/tutorial-extras/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Tutorial - Extras", + "position": 3, + "link": { + "type": "generated-index" + } +} diff --git a/docs/docs/tutorial-extras/img/docsVersionDropdown.png b/docs/docs/tutorial-extras/img/docsVersionDropdown.png new file mode 100644 index 0000000..97e4164 Binary files /dev/null and b/docs/docs/tutorial-extras/img/docsVersionDropdown.png differ diff --git a/docs/docs/tutorial-extras/img/localeDropdown.png b/docs/docs/tutorial-extras/img/localeDropdown.png new file mode 100644 index 0000000..e257edc Binary files /dev/null and b/docs/docs/tutorial-extras/img/localeDropdown.png differ diff --git a/docs/docs/tutorial-extras/manage-docs-versions.md b/docs/docs/tutorial-extras/manage-docs-versions.md new file mode 100644 index 0000000..ccda0b9 --- /dev/null +++ b/docs/docs/tutorial-extras/manage-docs-versions.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 1 +--- + +# Manage Docs Versions + +Docusaurus can manage multiple versions of your docs. + +## Create a docs version + +Release a version 1.0 of your project: + +```bash +npm run docusaurus docs:version 1.0 +``` + +The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created. + +Your docs now have 2 versions: + +- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs +- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs** + +## Add a Version Dropdown + +To navigate seamlessly across versions, add a version dropdown. + +Modify the `docusaurus.config.js` file: + +```js title="docusaurus.config.js" +export default { + themeConfig: { + navbar: { + items: [ + // highlight-start + { + type: 'docsVersionDropdown', + }, + // highlight-end + ], + }, + }, +}; +``` + +The docs version dropdown appears in your navbar: + +![Docs Version Dropdown](./img/docsVersionDropdown.png) + +## Update an existing version + +It is possible to edit versioned docs in their respective folder: + +- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello` +- `docs/hello.md` updates `http://localhost:3000/docs/next/hello` diff --git a/docs/docs/tutorial-extras/translate-your-site.md b/docs/docs/tutorial-extras/translate-your-site.md new file mode 100644 index 0000000..b5a644a --- /dev/null +++ b/docs/docs/tutorial-extras/translate-your-site.md @@ -0,0 +1,88 @@ +--- +sidebar_position: 2 +--- + +# Translate your site + +Let's translate `docs/intro.md` to French. + +## Configure i18n + +Modify `docusaurus.config.js` to add support for the `fr` locale: + +```js title="docusaurus.config.js" +export default { + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + }, +}; +``` + +## Translate a doc + +Copy the `docs/intro.md` file to the `i18n/fr` folder: + +```bash +mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ + +cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md +``` + +Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French. + +## Start your localized site + +Start your site on the French locale: + +```bash +npm run start -- --locale fr +``` + +Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. + +:::caution + +In development, you can only use one locale at a time. + +::: + +## Add a Locale Dropdown + +To navigate seamlessly across languages, add a locale dropdown. + +Modify the `docusaurus.config.js` file: + +```js title="docusaurus.config.js" +export default { + themeConfig: { + navbar: { + items: [ + // highlight-start + { + type: 'localeDropdown', + }, + // highlight-end + ], + }, + }, +}; +``` + +The locale dropdown now appears in your navbar: + +![Locale Dropdown](./img/localeDropdown.png) + +## Build your localized site + +Build your site for a specific locale: + +```bash +npm run build -- --locale fr +``` + +Or build your site to include all the locales at once: + +```bash +npm run build +``` diff --git a/docs/docs/updated-intro.md b/docs/docs/updated-intro.md new file mode 100644 index 0000000..69f14a1 --- /dev/null +++ b/docs/docs/updated-intro.md @@ -0,0 +1,140 @@ +--- +sidebar_position: 1 +--- + +# Welcome to @debros/network + +**@debros/network** is a powerful Node.js library that provides a simple, database-like API over OrbitDB and IPFS, making it easy to build decentralized applications with familiar database operations. + +## What is @debros/network? + +@debros/network simplifies decentralized application development by providing: + +- **Simple Database API**: Familiar CRUD operations for decentralized data +- **Multiple Store Types**: KeyValue, Document, Feed, and Counter stores +- **Schema Validation**: Built-in validation for data integrity +- **Transaction Support**: Batch operations for consistency +- **File Storage**: Built-in file upload and retrieval with IPFS +- **Real-time Events**: Subscribe to data changes +- **TypeScript Support**: Full TypeScript support with type safety +- **Connection Management**: Handle multiple database connections + +## Key Features + +### 🗄️ Database-like Operations + +Perform familiar database operations on decentralized data: + +```typescript +import { initDB, create, get, query } from '@debros/network'; + +// Initialize the database +await initDB(); + +// Create documents +await create('users', 'user123', { + username: 'alice', + email: 'alice@example.com', +}); + +// Query with filtering +const activeUsers = await query('users', (user) => user.isActive === true, { + limit: 10, + sort: { field: 'createdAt', order: 'desc' }, +}); +``` + +### 📁 File Storage + +Upload and manage files on IPFS: + +```typescript +import { uploadFile, getFile } from '@debros/network'; + +// Upload a file +const fileData = Buffer.from('Hello World'); +const result = await uploadFile(fileData, { + filename: 'hello.txt', + metadata: { type: 'text' }, +}); + +// Retrieve file +const file = await getFile(result.cid); +``` + +### 🔄 Real-time Updates + +Subscribe to data changes: + +```typescript +import { subscribe } from '@debros/network'; + +const unsubscribe = subscribe('document:created', (data) => { + console.log(`New document created: ${data.id}`); +}); +``` + +### 📊 Schema Validation + +Define schemas for data validation: + +```typescript +import { defineSchema } from '@debros/network'; + +defineSchema('users', { + properties: { + username: { type: 'string', required: true, min: 3 }, + email: { type: 'string', pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$' }, + }, +}); +``` + +## Architecture Overview + +@debros/network provides a clean abstraction layer over OrbitDB and IPFS: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your Application │ +├─────────────────────────────────────────────────────────────┤ +│ @debros/network API │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Database │ │ File │ │ Schema │ │ +│ │ Operations │ │ Storage │ │ Validation │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │Transaction │ │ Events │ │ Connection │ │ +│ │ System │ │ System │ │ Management │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ OrbitDB Layer │ +├─────────────────────────────────────────────────────────────┤ +│ IPFS Layer │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Who Should Use @debros/network? + +@debros/network is perfect for developers who want to: + +- Build decentralized applications with familiar database patterns +- Store and query data in a distributed manner +- Handle file storage on IPFS seamlessly +- Create applications with real-time data synchronization +- Use TypeScript for type-safe decentralized development +- Avoid dealing with low-level OrbitDB and IPFS complexities + +## Getting Started + +Ready to build your first decentralized application? Check out our [Getting Started Guide](./getting-started) to set up your development environment and start building. + +## Community and Support + +- 📖 [Documentation](./getting-started) - Comprehensive guides and examples +- 💻 [GitHub Repository](https://github.com/debros/network) - Source code and issue tracking +- 💬 [Discord Community](#) - Chat with other developers +- 📧 [Support Email](#) - Get help from the core team + +--- + +_@debros/network makes decentralized application development as simple as traditional database operations, while providing the benefits of distributed systems._ diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts new file mode 100644 index 0000000..801fe9c --- /dev/null +++ b/docs/docusaurus.config.ts @@ -0,0 +1,154 @@ +import {themes as prismThemes} from 'prism-react-renderer'; +import type {Config} from '@docusaurus/types'; +import type * as Preset from '@docusaurus/preset-classic'; + +// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) + +const config: Config = { + title: 'DebrosFramework', + tagline: 'Build scalable decentralized applications with ease', + favicon: 'img/favicon.ico', + + // Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future + future: { + v4: true, // Improve compatibility with the upcoming Docusaurus v4 + }, + + // Set the production url of your site here + url: 'https://debros-framework.example.com', + // Set the // pathname under which your site is served + // For GitHub pages deployment, it is often '//' + baseUrl: '/', + + // GitHub pages deployment config. + // If you aren't using GitHub pages, you don't need these. + organizationName: 'debros', // Usually your GitHub org/user name. + projectName: 'debros-framework', // Usually your repo name. + + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', + + // Even if you don't use internationalization, you can use this field to set + // useful metadata like html lang. For example, if your site is Chinese, you + // may want to replace "en" with "zh-Hans". + i18n: { + defaultLocale: 'en', + locales: ['en'], + }, + + presets: [ + [ + 'classic', + { + docs: { + sidebarPath: './sidebars.ts', + // Please change this to your repo. + // Remove this to remove the "edit this page" links. + editUrl: + 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', + }, + blog: { + showReadingTime: true, + feedOptions: { + type: ['rss', 'atom'], + xslt: true, + }, + // Please change this to your repo. + // Remove this to remove the "edit this page" links. + editUrl: + 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', + // Useful options to enforce blogging best practices + onInlineTags: 'warn', + onInlineAuthors: 'warn', + onUntruncatedBlogPosts: 'warn', + }, + theme: { + customCss: './src/css/custom.css', + }, + } satisfies Preset.Options, + ], + ], + + themeConfig: { + // Replace with your project's social card + image: 'img/docusaurus-social-card.jpg', + navbar: { + title: 'DebrosFramework', + logo: { + alt: 'DebrosFramework Logo', + src: 'img/logo.svg', + }, + items: [ + { + type: 'docSidebar', + sidebarId: 'tutorialSidebar', + position: 'left', + label: 'Documentation', + }, + { + type: 'docSidebar', + sidebarId: 'apiSidebar', + position: 'left', + label: 'API Reference', + }, + {to: '/blog', label: 'Blog', position: 'left'}, + { + href: 'https://github.com/debros/network', + label: 'GitHub', + position: 'right', + }, + ], + }, + footer: { + style: 'dark', + links: [ + { + title: 'Docs', + items: [ + { + label: 'Tutorial', + to: '/docs/intro', + }, + ], + }, + { + title: 'Community', + items: [ + { + label: 'Stack Overflow', + href: 'https://stackoverflow.com/questions/tagged/docusaurus', + }, + { + label: 'Discord', + href: 'https://discordapp.com/invite/docusaurus', + }, + { + label: 'X', + href: 'https://x.com/docusaurus', + }, + ], + }, + { + title: 'More', + items: [ + { + label: 'Blog', + to: '/blog', + }, + { + label: 'GitHub', + href: 'https://github.com/facebook/docusaurus', + }, + ], + }, + ], + copyright: `Copyright © ${new Date().getFullYear()} DebrosFramework. Built with Docusaurus.`, + }, + prism: { + theme: prismThemes.github, + darkTheme: prismThemes.dracula, + }, + } satisfies Preset.ThemeConfig, +}; + +export default config; diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..27c6942 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,47 @@ +{ + "name": "docs", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids", + "typecheck": "tsc" + }, + "dependencies": { + "@docusaurus/core": "3.8.1", + "@docusaurus/preset-classic": "3.8.1", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "prism-react-renderer": "^2.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/tsconfig": "3.8.1", + "@docusaurus/types": "3.8.1", + "typescript": "~5.6.2" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome version", + "last 3 firefox version", + "last 5 safari version" + ] + }, + "engines": { + "node": ">=18.0" + } +} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 0000000..a6fc711 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,11012 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@docusaurus/core': + specifier: 3.8.1 + version: 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/preset-classic': + specifier: 3.8.1 + version: 3.8.1(@algolia/client-search@5.28.0)(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)(typescript@5.6.3) + '@mdx-js/react': + specifier: ^3.0.0 + version: 3.1.0(@types/react@19.1.8)(react@19.1.0) + clsx: + specifier: ^2.0.0 + version: 2.1.1 + prism-react-renderer: + specifier: ^2.3.0 + version: 2.4.1(react@19.1.0) + react: + specifier: ^19.0.0 + version: 19.1.0 + react-dom: + specifier: ^19.0.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@docusaurus/module-type-aliases': + specifier: 3.8.1 + version: 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/tsconfig': + specifier: 3.8.1 + version: 3.8.1 + '@docusaurus/types': + specifier: 3.8.1 + version: 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + typescript: + specifier: ~5.6.2 + version: 5.6.3 + +packages: + + '@algolia/autocomplete-core@1.17.9': + resolution: {integrity: sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.9': + resolution: {integrity: sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.9': + resolution: {integrity: sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.9': + resolution: {integrity: sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.28.0': + resolution: {integrity: sha512-oGMaBCIpvz3n+4rCz/73ldo/Dw95YFx6+MAQkNiCfsgolB2tduaiZvNOvdkm86eKqSKDDBGBo54GQXZ5YX6Bjg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.28.0': + resolution: {integrity: sha512-G+TTdNnuwUSy8evolyNE3I74uSIXPU4LLDnJmB4d6TkLvvzMAjwsMBuHHjwYpw37+c4tH0dT4u+39cyxrZNojg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.28.0': + resolution: {integrity: sha512-lqa0Km1/YWfPplNB8jX9kstaCl2LO6ziQAJEBtHxw2sJp/mlxJIAuudBUbEhoUrKQvI7N4erNYawl6ejic7gfw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.28.0': + resolution: {integrity: sha512-pGsDrlnt0UMXDjQuIpKQSfl7PVx+KcqcwVgkgITwQ45akckTwmbpaV4rZF2k3wgIbOECFZGnpArWF5cSrE4T3g==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.28.0': + resolution: {integrity: sha512-d/Uot/LH8YJeFyqpAmTN/LxueqV5mLD5K4aAKTDVP4CBNNubX4Z+0sveRcxWQZiORVLrs5zR1G5Buxmab2Xb9w==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.28.0': + resolution: {integrity: sha512-XygCxyxJ5IwqsTrzpsAG2O/lr8GsnMA3ih7wzbXtot+ZyAhzDUFwlQSjCCmjACNbrBEaIvtiGbjX/z+HZd902Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.28.0': + resolution: {integrity: sha512-zLEddu9TEwFT/YUJkA3oUwqQYHeGEj64fi0WyVRq+siJVfxt4AYkFfcMBcSr2iR1Wo9Mk10IPOhk3DUr0TSncg==} + engines: {node: '>= 14.0.0'} + + '@algolia/events@4.0.1': + resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} + + '@algolia/ingestion@1.28.0': + resolution: {integrity: sha512-dmkoSQ+bzC5ryDu2J4MTRDxuh5rZg6sHNawgBfSC/iNttEzeogCyvdxg+uWMErJuSlZk9oENykhETMkSFurwpQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.28.0': + resolution: {integrity: sha512-XwVpkxc2my2rNUWbHo4Dk1Mx/JOrq6CLOAC3dmIrMt2Le2bIPMIDA6Iyjz4F4kXvp7H8q1R26cRMlYmhL31Jlg==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.28.0': + resolution: {integrity: sha512-MVqY7zIw0TdQUExefGthydLXccbe5CHH/uOxIG8/QiSD0ZmAmg95UwfmJiJBfuXGGi/cmCrW3JQiDbAM9vx6PA==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.28.0': + resolution: {integrity: sha512-RfxbCinf+coQgxRkDKmRiB/ovOt3Fz0md84LmogsQIabrJVKoQrFON4Vc9YdK2bTTn6iBHtnezm0puNTk+n3SA==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.28.0': + resolution: {integrity: sha512-85ZBqPTQ5tjiZ925V89ttE/vUJXpJjy2cCF7PAWq9v32JGGF+v+mDm8NiEBRk9AS7+4klb/uR80KBdcg5bO7cA==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.28.0': + resolution: {integrity: sha512-U3F4WeExiKx1Ig6OxO9dDzzk04HKgtEn47TwjgKmGSDPFM7WZ5KyP1EAZEbfd3/nw6hp0z9RKdTfMql6Sd1/2Q==} + engines: {node: '>= 14.0.0'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.27.5': + resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.27.4': + resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.5': + resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.27.1': + resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.27.1': + resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.4': + resolution: {integrity: sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.27.1': + resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.5': + resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': + resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1': + resolution: {integrity: sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.27.1': + resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.27.1': + resolution: {integrity: sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.27.1': + resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.27.5': + resolution: {integrity: sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.27.1': + resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.27.1': + resolution: {integrity: sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.27.1': + resolution: {integrity: sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.27.1': + resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.27.3': + resolution: {integrity: sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.27.1': + resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.27.1': + resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.27.1': + resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1': + resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.27.1': + resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': + resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.27.1': + resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.27.3': + resolution: {integrity: sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.27.1': + resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.27.1': + resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.1': + resolution: {integrity: sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.27.1': + resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.27.1': + resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-constant-elements@7.27.1': + resolution: {integrity: sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.27.1': + resolution: {integrity: sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.27.1': + resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.27.5': + resolution: {integrity: sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.27.1': + resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.27.4': + resolution: {integrity: sha512-D68nR5zxU64EUzV8i7T3R5XP0Xhrou/amNnddsRQssx6GrTLdZl1rLxyjtVZBd+v/NVX4AbTPOB5aU8thAZV1A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.27.1': + resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.27.1': + resolution: {integrity: sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.27.1': + resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1': + resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.27.2': + resolution: {integrity: sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-react@7.27.1': + resolution: {integrity: sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.27.1': + resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime-corejs3@7.27.6': + resolution: {integrity: sha512-vDVrlmRAY8z9Ul/HxT+8ceAru95LQgkSKiXkSYZvqtbkPSfhZJgpRp45Cldbh1GJ1kxzQkI70AqyrTI58KpaWQ==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.4': + resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.6': + resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} + engines: {node: '>=6.9.0'} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@csstools/cascade-layer-name-parser@2.0.5': + resolution: {integrity: sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.0.10': + resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/postcss-cascade-layers@5.0.1': + resolution: {integrity: sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-function@4.0.10': + resolution: {integrity: sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-mix-function@3.0.10': + resolution: {integrity: sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.0': + resolution: {integrity: sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-content-alt-text@2.0.6': + resolution: {integrity: sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-exponential-functions@2.0.9': + resolution: {integrity: sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-font-format-keywords@4.0.0': + resolution: {integrity: sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-gamut-mapping@2.0.10': + resolution: {integrity: sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-gradients-interpolation-method@5.0.10': + resolution: {integrity: sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-hwb-function@4.0.10': + resolution: {integrity: sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-ic-unit@4.0.2': + resolution: {integrity: sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-initial@2.0.1': + resolution: {integrity: sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-is-pseudo-class@5.0.3': + resolution: {integrity: sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-light-dark-function@2.0.9': + resolution: {integrity: sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-float-and-clear@3.0.0': + resolution: {integrity: sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-overflow@2.0.0': + resolution: {integrity: sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-overscroll-behavior@2.0.0': + resolution: {integrity: sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-resize@3.0.0': + resolution: {integrity: sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-viewport-units@3.0.4': + resolution: {integrity: sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-media-minmax@2.0.9': + resolution: {integrity: sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5': + resolution: {integrity: sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-nested-calc@4.0.0': + resolution: {integrity: sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-normalize-display-values@4.0.0': + resolution: {integrity: sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-oklab-function@4.0.10': + resolution: {integrity: sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-progressive-custom-properties@4.1.0': + resolution: {integrity: sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-random-function@2.0.1': + resolution: {integrity: sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-relative-color-syntax@3.0.10': + resolution: {integrity: sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-scope-pseudo-class@4.0.1': + resolution: {integrity: sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-sign-functions@1.1.4': + resolution: {integrity: sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-stepped-value-functions@4.0.9': + resolution: {integrity: sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-text-decoration-shorthand@4.0.2': + resolution: {integrity: sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-trigonometric-functions@4.0.9': + resolution: {integrity: sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-unset-value@4.0.0': + resolution: {integrity: sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/selector-resolve-nested@3.1.0': + resolution: {integrity: sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@csstools/utilities@2.0.0': + resolution: {integrity: sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + + '@docsearch/css@3.9.0': + resolution: {integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==} + + '@docsearch/react@3.9.0': + resolution: {integrity: sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==} + peerDependencies: + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@docusaurus/babel@3.8.1': + resolution: {integrity: sha512-3brkJrml8vUbn9aeoZUlJfsI/GqyFcDgQJwQkmBtclJgWDEQBKKeagZfOgx0WfUQhagL1sQLNW0iBdxnI863Uw==} + engines: {node: '>=18.0'} + + '@docusaurus/bundler@3.8.1': + resolution: {integrity: sha512-/z4V0FRoQ0GuSLToNjOSGsk6m2lQUG4FRn8goOVoZSRsTrU8YR2aJacX5K3RG18EaX9b+52pN4m1sL3MQZVsQA==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/faster': '*' + peerDependenciesMeta: + '@docusaurus/faster': + optional: true + + '@docusaurus/core@3.8.1': + resolution: {integrity: sha512-ENB01IyQSqI2FLtOzqSI3qxG2B/jP4gQPahl2C3XReiLebcVh5B5cB9KYFvdoOqOWPyr5gXK4sjgTKv7peXCrA==} + engines: {node: '>=18.0'} + hasBin: true + peerDependencies: + '@mdx-js/react': ^3.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/cssnano-preset@3.8.1': + resolution: {integrity: sha512-G7WyR2N6SpyUotqhGznERBK+x84uyhfMQM2MmDLs88bw4Flom6TY46HzkRkSEzaP9j80MbTN8naiL1fR17WQug==} + engines: {node: '>=18.0'} + + '@docusaurus/logger@3.8.1': + resolution: {integrity: sha512-2wjeGDhKcExEmjX8k1N/MRDiPKXGF2Pg+df/bDDPnnJWHXnVEZxXj80d6jcxp1Gpnksl0hF8t/ZQw9elqj2+ww==} + engines: {node: '>=18.0'} + + '@docusaurus/mdx-loader@3.8.1': + resolution: {integrity: sha512-DZRhagSFRcEq1cUtBMo4TKxSNo/W6/s44yhr8X+eoXqCLycFQUylebOMPseHi5tc4fkGJqwqpWJLz6JStU9L4w==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/module-type-aliases@3.8.1': + resolution: {integrity: sha512-6xhvAJiXzsaq3JdosS7wbRt/PwEPWHr9eM4YNYqVlbgG1hSK3uQDXTVvQktasp3VO6BmfYWPozueLWuj4gB+vg==} + peerDependencies: + react: '*' + react-dom: '*' + + '@docusaurus/plugin-content-blog@3.8.1': + resolution: {integrity: sha512-vNTpMmlvNP9n3hGEcgPaXyvTljanAKIUkuG9URQ1DeuDup0OR7Ltvoc8yrmH+iMZJbcQGhUJF+WjHLwuk8HSdw==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/plugin-content-docs': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-content-docs@3.8.1': + resolution: {integrity: sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-content-pages@3.8.1': + resolution: {integrity: sha512-a+V6MS2cIu37E/m7nDJn3dcxpvXb6TvgdNI22vJX8iUTp8eoMoPa0VArEbWvCxMY/xdC26WzNv4wZ6y0iIni/w==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-css-cascade-layers@3.8.1': + resolution: {integrity: sha512-VQ47xRxfNKjHS5ItzaVXpxeTm7/wJLFMOPo1BkmoMG4Cuz4nuI+Hs62+RMk1OqVog68Swz66xVPK8g9XTrBKRw==} + engines: {node: '>=18.0'} + + '@docusaurus/plugin-debug@3.8.1': + resolution: {integrity: sha512-nT3lN7TV5bi5hKMB7FK8gCffFTBSsBsAfV84/v293qAmnHOyg1nr9okEw8AiwcO3bl9vije5nsUvP0aRl2lpaw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-google-analytics@3.8.1': + resolution: {integrity: sha512-Hrb/PurOJsmwHAsfMDH6oVpahkEGsx7F8CWMjyP/dw1qjqmdS9rcV1nYCGlM8nOtD3Wk/eaThzUB5TSZsGz+7Q==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-google-gtag@3.8.1': + resolution: {integrity: sha512-tKE8j1cEZCh8KZa4aa80zpSTxsC2/ZYqjx6AAfd8uA8VHZVw79+7OTEP2PoWi0uL5/1Is0LF5Vwxd+1fz5HlKg==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-google-tag-manager@3.8.1': + resolution: {integrity: sha512-iqe3XKITBquZq+6UAXdb1vI0fPY5iIOitVjPQ581R1ZKpHr0qe+V6gVOrrcOHixPDD/BUKdYwkxFjpNiEN+vBw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-sitemap@3.8.1': + resolution: {integrity: sha512-+9YV/7VLbGTq8qNkjiugIelmfUEVkTyLe6X8bWq7K5qPvGXAjno27QAfFq63mYfFFbJc7z+pudL63acprbqGzw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-svgr@3.8.1': + resolution: {integrity: sha512-rW0LWMDsdlsgowVwqiMb/7tANDodpy1wWPwCcamvhY7OECReN3feoFwLjd/U4tKjNY3encj0AJSTxJA+Fpe+Gw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/preset-classic@3.8.1': + resolution: {integrity: sha512-yJSjYNHXD8POMGc2mKQuj3ApPrN+eG0rO1UPgSx7jySpYU+n4WjBikbrA2ue5ad9A7aouEtMWUoiSRXTH/g7KQ==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/react-loadable@6.0.0': + resolution: {integrity: sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==} + peerDependencies: + react: '*' + + '@docusaurus/theme-classic@3.8.1': + resolution: {integrity: sha512-bqDUCNqXeYypMCsE1VcTXSI1QuO4KXfx8Cvl6rYfY0bhhqN6d2WZlRkyLg/p6pm+DzvanqHOyYlqdPyP0iz+iw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/theme-common@3.8.1': + resolution: {integrity: sha512-UswMOyTnPEVRvN5Qzbo+l8k4xrd5fTFu2VPPfD6FcW/6qUtVLmJTQCktbAL3KJ0BVXGm5aJXz/ZrzqFuZERGPw==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/plugin-content-docs': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/theme-search-algolia@3.8.1': + resolution: {integrity: sha512-NBFH5rZVQRAQM087aYSRKQ9yGEK9eHd+xOxQjqNpxMiV85OhJDD4ZGz6YJIod26Fbooy54UWVdzNU0TFeUUUzQ==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/theme-translations@3.8.1': + resolution: {integrity: sha512-OTp6eebuMcf2rJt4bqnvuwmm3NVXfzfYejL+u/Y1qwKhZPrjPoKWfk1CbOP5xH5ZOPkiAsx4dHdQBRJszK3z2g==} + engines: {node: '>=18.0'} + + '@docusaurus/tsconfig@3.8.1': + resolution: {integrity: sha512-XBWCcqhRHhkhfolnSolNL+N7gj3HVE3CoZVqnVjfsMzCoOsuQw2iCLxVVHtO+rePUUfouVZHURDgmqIySsF66A==} + + '@docusaurus/types@3.8.1': + resolution: {integrity: sha512-ZPdW5AB+pBjiVrcLuw3dOS6BFlrG0XkS2lDGsj8TizcnREQg3J8cjsgfDviszOk4CweNfwo1AEELJkYaMUuOPg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/utils-common@3.8.1': + resolution: {integrity: sha512-zTZiDlvpvoJIrQEEd71c154DkcriBecm4z94OzEE9kz7ikS3J+iSlABhFXM45mZ0eN5pVqqr7cs60+ZlYLewtg==} + engines: {node: '>=18.0'} + + '@docusaurus/utils-validation@3.8.1': + resolution: {integrity: sha512-gs5bXIccxzEbyVecvxg6upTwaUbfa0KMmTj7HhHzc016AGyxH2o73k1/aOD0IFrdCsfJNt37MqNI47s2MgRZMA==} + engines: {node: '>=18.0'} + + '@docusaurus/utils@3.8.1': + resolution: {integrity: sha512-P1ml0nvOmEFdmu0smSXOqTS1sxU5tqvnc0dA4MTKV39kye+bhQnjkIKEE18fNOvxjyB86k8esoCIFM3x4RykOQ==} + engines: {node: '>=18.0'} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@mdx-js/mdx@3.1.0': + resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + + '@mdx-js/react@3.1.0': + resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.3.1': + resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + engines: {node: '>=12'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@slorber/react-helmet-async@1.3.0': + resolution: {integrity: sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@slorber/remark-comment@1.0.0': + resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0': + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0': + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0': + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0': + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-preset@8.1.0': + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/core@8.1.0': + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + + '@svgr/hast-util-to-babel-ast@8.0.0': + resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} + engines: {node: '>=14'} + + '@svgr/plugin-jsx@8.1.0': + resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + + '@svgr/plugin-svgo@8.1.0': + resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + + '@svgr/webpack@8.1.0': + resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} + engines: {node: '>=14'} + + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/bonjour@3.5.13': + resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + + '@types/connect-history-api-fallback@1.5.4': + resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + + '@types/express@4.17.23': + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + + '@types/gtag.js@0.0.12': + resolution: {integrity: sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + + '@types/html-minifier-terser@6.1.0': + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/http-proxy@1.17.16': + resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node-forge@1.3.11': + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/node@24.0.3': + resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==} + + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-router-config@5.0.11': + resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} + + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + + '@types/react@19.1.8': + resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-index@1.9.4': + resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + + '@types/sockjs@0.3.36': + resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + algoliasearch-helper@3.26.0: + resolution: {integrity: sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + + algoliasearch@5.28.0: + resolution: {integrity: sha512-FCRzwW+/TJFQIfo+DxObo2gfn4+0aGa7sVQgCN1/ojKqrhb/7Scnuyi4FBS0zvNCgOZBMms+Ci2hyQwsgAqIzg==} + engines: {node: '>= 14.0.0'} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + babel-loader@9.2.1: + resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5' + + babel-plugin-dynamic-import-node@2.3.3: + resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} + + babel-plugin-polyfill-corejs2@0.4.13: + resolution: {integrity: sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.11.1: + resolution: {integrity: sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.4: + resolution: {integrity: sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bonjour-service@1.3.0: + resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@6.2.1: + resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001723: + resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combine-promises@1.2.0: + resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} + engines: {node: '>=10'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.0: + resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@6.0.0: + resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==} + engines: {node: '>=12'} + + connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@0.5.2: + resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} + engines: {node: '>= 0.6'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + copy-text-to-clipboard@3.2.0: + resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==} + engines: {node: '>=12'} + + copy-webpack-plugin@11.0.0: + resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==} + engines: {node: '>= 14.15.0'} + peerDependencies: + webpack: ^5.1.0 + + core-js-compat@3.43.0: + resolution: {integrity: sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==} + + core-js-pure@3.43.0: + resolution: {integrity: sha512-i/AgxU2+A+BbJdMxh3v7/vxi2SbFqxiFmg6VsDwYB4jkucrd1BZNA9a9gphC0fYMG5IBSgQcbQnk865VCLe7xA==} + + core-js@3.43.0: + resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + + css-blank-pseudo@7.0.1: + resolution: {integrity: sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + css-declaration-sorter@7.2.0: + resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.0.9 + + css-has-pseudo@7.0.2: + resolution: {integrity: sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + css-minimizer-webpack-plugin@5.0.1: + resolution: {integrity: sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@parcel/css': '*' + '@swc/css': '*' + clean-css: '*' + csso: '*' + esbuild: '*' + lightningcss: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@parcel/css': + optional: true + '@swc/css': + optional: true + clean-css: + optional: true + csso: + optional: true + esbuild: + optional: true + lightningcss: + optional: true + + css-prefers-color-scheme@10.0.0: + resolution: {integrity: sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssdb@8.3.0: + resolution: {integrity: sha512-c7bmItIg38DgGjSwDPZOYF/2o0QU/sSgkWOMyl8votOfgFuyiFKWPesmCGEsrGLxEA9uL540cp8LdaGEjUGsZQ==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-preset-advanced@6.1.2: + resolution: {integrity: sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-preset-default@6.1.2: + resolution: {integrity: sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-utils@4.0.2: + resolution: {integrity: sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano@6.1.2: + resolution: {integrity: sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + detect-port@1.6.1: + resolution: {integrity: sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==} + engines: {node: '>= 4.0.0'} + hasBin: true + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + + dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.170: + resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + emoticon@4.1.0: + resolution: {integrity: sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-value-to-estree@3.4.0: + resolution: {integrity: sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eta@2.2.0: + resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==} + engines: {node: '>=6.0.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eval@0.1.8: + resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} + engines: {node: '>= 0.8'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + + feed@4.2.2: + resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} + engines: {node: '>=0.4.0'} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-loader@6.2.0: + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-cache-dir@4.0.0: + resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} + engines: {node: '>=14.16'} + + find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs-monkey@1.0.6: + resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + github-slugger@1.5.0: + resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-yarn@3.0.0: + resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + history@4.10.1: + resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hpack.js@2.1.6: + resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + html-webpack-plugin@5.6.3: + resolution: {integrity: sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.20.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-deceiver@1.2.7: + resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + + http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-middleware@2.0.9: + resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + image-size@2.0.2: + resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} + engines: {node: '>=16.x'} + hasBin: true + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infima@0.2.0-alpha.45: + resolution: {integrity: sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==} + engines: {node: '>=12'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-npm@6.0.0: + resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-yarn-global@0.4.1: + resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} + engines: {node: '>=12'} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + latest-version@7.0.0: + resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} + engines: {node: '>=14.16'} + + launch-editor@2.10.0: + resolution: {integrity: sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.33.0: + resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.18: + resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + mini-css-extract-plugin@2.9.2: + resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-url@8.0.2: + resolution: {integrity: sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==} + engines: {node: '>=14.16'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + null-loader@4.0.1: + resolution: {integrity: sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + package-json@8.1.1: + resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} + engines: {node: '>=14.16'} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + + path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + + postcss-attribute-case-insensitive@7.0.1: + resolution: {integrity: sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-calc@9.0.1: + resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.2 + + postcss-clamp@4.1.0: + resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} + engines: {node: '>=7.6.0'} + peerDependencies: + postcss: ^8.4.6 + + postcss-color-functional-notation@7.0.10: + resolution: {integrity: sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-color-hex-alpha@10.0.0: + resolution: {integrity: sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-color-rebeccapurple@10.0.0: + resolution: {integrity: sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-colormin@6.1.0: + resolution: {integrity: sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-convert-values@6.1.0: + resolution: {integrity: sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-custom-media@11.0.6: + resolution: {integrity: sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-custom-properties@14.0.6: + resolution: {integrity: sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-custom-selectors@8.0.5: + resolution: {integrity: sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-dir-pseudo-class@9.0.1: + resolution: {integrity: sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-discard-comments@6.0.2: + resolution: {integrity: sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-duplicates@6.0.3: + resolution: {integrity: sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-empty@6.0.3: + resolution: {integrity: sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-overridden@6.0.2: + resolution: {integrity: sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-unused@6.0.5: + resolution: {integrity: sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-double-position-gradients@6.0.2: + resolution: {integrity: sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-focus-visible@10.0.1: + resolution: {integrity: sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-focus-within@9.0.1: + resolution: {integrity: sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-font-variant@5.0.0: + resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} + peerDependencies: + postcss: ^8.1.0 + + postcss-gap-properties@6.0.0: + resolution: {integrity: sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-image-set-function@7.0.0: + resolution: {integrity: sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-lab-function@7.0.10: + resolution: {integrity: sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-loader@7.3.4: + resolution: {integrity: sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + + postcss-logical@8.1.0: + resolution: {integrity: sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-merge-idents@6.0.3: + resolution: {integrity: sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-longhand@6.0.5: + resolution: {integrity: sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-rules@6.1.1: + resolution: {integrity: sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-font-values@6.1.0: + resolution: {integrity: sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-gradients@6.0.3: + resolution: {integrity: sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-params@6.1.0: + resolution: {integrity: sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-selectors@6.0.4: + resolution: {integrity: sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-nesting@13.0.2: + resolution: {integrity: sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-normalize-charset@6.0.2: + resolution: {integrity: sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-display-values@6.0.2: + resolution: {integrity: sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-positions@6.0.2: + resolution: {integrity: sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-repeat-style@6.0.2: + resolution: {integrity: sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-string@6.0.2: + resolution: {integrity: sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-timing-functions@6.0.2: + resolution: {integrity: sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-unicode@6.1.0: + resolution: {integrity: sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-url@6.0.2: + resolution: {integrity: sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-whitespace@6.0.2: + resolution: {integrity: sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-opacity-percentage@3.0.0: + resolution: {integrity: sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-ordered-values@6.0.2: + resolution: {integrity: sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-overflow-shorthand@6.0.0: + resolution: {integrity: sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-page-break@3.0.4: + resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} + peerDependencies: + postcss: ^8 + + postcss-place@10.0.0: + resolution: {integrity: sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-preset-env@10.2.3: + resolution: {integrity: sha512-zlQN1yYmA7lFeM1wzQI14z97mKoM8qGng+198w1+h6sCud/XxOjcKtApY9jWr7pXNS3yHDEafPlClSsWnkY8ow==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-pseudo-class-any-link@10.0.1: + resolution: {integrity: sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-reduce-idents@6.0.3: + resolution: {integrity: sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-initial@6.1.0: + resolution: {integrity: sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-transforms@6.0.2: + resolution: {integrity: sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-replace-overflow-wrap@4.0.0: + resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} + peerDependencies: + postcss: ^8.0.3 + + postcss-selector-not@8.0.1: + resolution: {integrity: sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-sort-media-queries@5.2.0: + resolution: {integrity: sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.4.23 + + postcss-svgo@6.0.3: + resolution: {integrity: sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==} + engines: {node: ^14 || ^16 || >= 18} + peerDependencies: + postcss: ^8.4.31 + + postcss-unique-selectors@6.0.4: + resolution: {integrity: sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss-zindex@6.0.2: + resolution: {integrity: sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + + pretty-time@1.1.0: + resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} + engines: {node: '>=4'} + + prism-react-renderer@2.4.1: + resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} + peerDependencies: + react: '>=16.0.0' + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pupa@3.1.0: + resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} + engines: {node: '>=12.20'} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.0: + resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} + engines: {node: '>= 0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-json-view-lite@2.4.1: + resolution: {integrity: sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA==} + engines: {node: '>=18'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + react-loadable-ssr-addon-v5-slorber@1.0.1: + resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} + engines: {node: '>=10.13.0'} + peerDependencies: + react-loadable: '*' + webpack: '>=4.41.1 || 5.x' + + react-router-config@5.1.1: + resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} + peerDependencies: + react: '>=15' + react-router: '>=5' + + react-router-dom@5.3.4: + resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} + peerDependencies: + react: '>=15' + + react-router@5.3.4: + resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} + peerDependencies: + react: '>=15' + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.0: + resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + registry-auth-token@5.1.0: + resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + remark-directive@3.0.1: + resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + + remark-emoji@4.0.1: + resolution: {integrity: sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx@3.1.0: + resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-like@0.1.2: + resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pathname@3.0.0: + resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rtlcss@4.3.0: + resolution: {integrity: sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==} + engines: {node: '>=12.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + schema-dts@1.1.5: + resolution: {integrity: sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.2: + resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + engines: {node: '>= 10.13.0'} + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + select-hose@2.0.0: + resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + + semver-diff@4.0.0: + resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} + engines: {node: '>=12'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-handler@6.1.6: + resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} + + serve-index@1.9.1: + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sitemap@7.1.2: + resolution: {integrity: sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==} + engines: {node: '>=12.0.0', npm: '>=5.6.0'} + hasBin: true + + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + sockjs@0.3.24: + resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + + sort-css-media-queries@2.2.0: + resolution: {integrity: sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==} + engines: {node: '>= 6.3.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + spdy-transport@3.0.0: + resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + + spdy@4.0.2: + resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} + engines: {node: '>=6.0.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + srcset@4.0.0: + resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} + engines: {node: '>=12'} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-to-js@1.1.17: + resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} + + style-to-object@1.0.9: + resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} + + stylehacks@6.1.1: + resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.43.0: + resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==} + engines: {node: '>=10'} + hasBin: true + + thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-notifier@6.0.2: + resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} + engines: {node: '>=14.16'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-loader@4.1.1: + resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + file-loader: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + file-loader: + optional: true + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + value-equal@1.0.1: + resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + engines: {node: '>=10.13.0'} + + wbuf@1.7.3: + resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + webpack-bundle-analyzer@4.10.2: + resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} + engines: {node: '>= 10.13.0'} + hasBin: true + + webpack-dev-middleware@5.3.4: + resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + webpack-dev-server@4.15.2: + resolution: {integrity: sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + + webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + + webpack-merge@6.0.1: + resolution: {integrity: sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==} + engines: {node: '>=18.0.0'} + + webpack-sources@3.3.2: + resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==} + engines: {node: '>=10.13.0'} + + webpack@5.99.9: + resolution: {integrity: sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + webpackbar@6.0.1: + resolution: {integrity: sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + webpack: 3 || 4 || 5 + + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0)': + dependencies: + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0) + '@algolia/client-search': 5.28.0 + algoliasearch: 5.28.0 + + '@algolia/autocomplete-shared@1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0)': + dependencies: + '@algolia/client-search': 5.28.0 + algoliasearch: 5.28.0 + + '@algolia/client-abtesting@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/client-analytics@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/client-common@5.28.0': {} + + '@algolia/client-insights@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/client-personalization@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/client-query-suggestions@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/client-search@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/events@4.0.1': {} + + '@algolia/ingestion@1.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/monitoring@1.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/recommend@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + '@algolia/requester-browser-xhr@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + + '@algolia/requester-fetch@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + + '@algolia/requester-node-http@5.28.0': + dependencies: + '@algolia/client-common': 5.28.0 + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.5': {} + + '@babel/core@7.27.4': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.27.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.5': + dependencies: + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.27.6 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.27.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.27.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.27.6 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.27.1': + dependencies: + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.6 + + '@babel/parser@7.27.5': + dependencies: + '@babel/types': 7.27.6 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-async-generator-functions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.4) + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.27.5(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.4) + '@babel/traverse': 7.27.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + + '@babel/plugin-transform-destructuring@7.27.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-object-rest-spread@7.27.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.27.3(@babel/core@7.27.4) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.4) + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-display-name@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regenerator@7.27.5(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-runtime@7.27.4(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.27.4) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.4) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.27.4) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/preset-env@7.27.2(@babel/core@7.27.4)': + dependencies: + '@babel/compat-data': 7.27.5 + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.27.4) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.27.4) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-async-generator-functions': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-block-scoping': 7.27.5(@babel/core@7.27.4) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-classes': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-destructuring': 7.27.3(@babel/core@7.27.4) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-object-rest-spread': 7.27.3(@babel/core@7.27.4) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-regenerator': 7.27.5(@babel/core@7.27.4) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.27.4) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.27.4) + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.27.4) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.4) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.27.4) + core-js-compat: 3.43.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.27.6 + esutils: 2.0.3 + + '@babel/preset-react@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/runtime-corejs3@7.27.6': + dependencies: + core-js-pure: 3.43.0 + + '@babel/runtime@7.27.6': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 + + '@babel/traverse@7.27.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.5 + '@babel/template': 7.27.2 + '@babel/types': 7.27.6 + debug: 4.4.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@colors/colors@1.5.0': + optional: true + + '@csstools/cascade-layer-name-parser@2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/color-helpers@5.0.2': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/postcss-cascade-layers@5.0.1(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + '@csstools/postcss-color-function@4.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-color-mix-function@3.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.0(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-content-alt-text@2.0.6(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.6)': + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-gamut-mapping@2.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-gradients-interpolation-method@5.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-hwb-function@4.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-ic-unit@4.0.2(postcss@8.5.6)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-initial@2.0.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + '@csstools/postcss-light-dark-function@2.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.6)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.6)': + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-normalize-display-values@4.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-oklab-function@4.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-progressive-custom-properties@4.1.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-random-function@2.0.1(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-relative-color-syntax@3.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-text-decoration-shorthand@4.0.2(postcss@8.5.6)': + dependencies: + '@csstools/color-helpers': 5.0.2 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@csstools/utilities@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@discoveryjs/json-ext@0.5.7': {} + + '@docsearch/css@3.9.0': {} + + '@docsearch/react@3.9.0(@algolia/client-search@5.28.0)(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.9(@algolia/client-search@5.28.0)(algoliasearch@5.28.0) + '@docsearch/css': 3.9.0 + algoliasearch: 5.28.0 + optionalDependencies: + '@types/react': 19.1.8 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@docusaurus/babel@3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/core': 7.27.4 + '@babel/generator': 7.27.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-transform-runtime': 7.27.4(@babel/core@7.27.4) + '@babel/preset-env': 7.27.2(@babel/core@7.27.4) + '@babel/preset-react': 7.27.1(@babel/core@7.27.4) + '@babel/preset-typescript': 7.27.1(@babel/core@7.27.4) + '@babel/runtime': 7.27.6 + '@babel/runtime-corejs3': 7.27.6 + '@babel/traverse': 7.27.4 + '@docusaurus/logger': 3.8.1 + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + babel-plugin-dynamic-import-node: 2.3.3 + fs-extra: 11.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/bundler@3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@babel/core': 7.27.4 + '@docusaurus/babel': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/cssnano-preset': 3.8.1 + '@docusaurus/logger': 3.8.1 + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + babel-loader: 9.2.1(@babel/core@7.27.4)(webpack@5.99.9) + clean-css: 5.3.3 + copy-webpack-plugin: 11.0.0(webpack@5.99.9) + css-loader: 6.11.0(webpack@5.99.9) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.99.9) + cssnano: 6.1.2(postcss@8.5.6) + file-loader: 6.2.0(webpack@5.99.9) + html-minifier-terser: 7.2.0 + mini-css-extract-plugin: 2.9.2(webpack@5.99.9) + null-loader: 4.0.1(webpack@5.99.9) + postcss: 8.5.6 + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.6.3)(webpack@5.99.9) + postcss-preset-env: 10.2.3(postcss@8.5.6) + terser-webpack-plugin: 5.3.14(webpack@5.99.9) + tslib: 2.8.1 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9) + webpack: 5.99.9 + webpackbar: 6.0.1(webpack@5.99.9) + transitivePeerDependencies: + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - csso + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - typescript + - uglify-js + - webpack-cli + + '@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/babel': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/bundler': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.1.0) + boxen: 6.2.1 + chalk: 4.1.2 + chokidar: 3.6.0 + cli-table3: 0.6.5 + combine-promises: 1.2.0 + commander: 5.1.0 + core-js: 3.43.0 + detect-port: 1.6.1 + escape-html: 1.0.3 + eta: 2.2.0 + eval: 0.1.8 + execa: 5.1.1 + fs-extra: 11.3.0 + html-tags: 3.3.1 + html-webpack-plugin: 5.6.3(webpack@5.99.9) + leven: 3.1.0 + lodash: 4.17.21 + open: 8.4.2 + p-map: 4.0.0 + prompts: 2.4.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.1.0)' + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@19.1.0))(webpack@5.99.9) + react-router: 5.3.4(react@19.1.0) + react-router-config: 5.1.1(react-router@5.3.4(react@19.1.0))(react@19.1.0) + react-router-dom: 5.3.4(react@19.1.0) + semver: 7.7.2 + serve-handler: 6.1.6 + tinypool: 1.1.1 + tslib: 2.8.1 + update-notifier: 6.0.2 + webpack: 5.99.9 + webpack-bundle-analyzer: 4.10.2 + webpack-dev-server: 4.15.2(webpack@5.99.9) + webpack-merge: 6.0.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/cssnano-preset@3.8.1': + dependencies: + cssnano-preset-advanced: 6.1.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-sort-media-queries: 5.2.0(postcss@8.5.6) + tslib: 2.8.1 + + '@docusaurus/logger@3.8.1': + dependencies: + chalk: 4.1.2 + tslib: 2.8.1 + + '@docusaurus/mdx-loader@3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@docusaurus/logger': 3.8.1 + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mdx-js/mdx': 3.1.0(acorn@8.15.0) + '@slorber/remark-comment': 1.0.0 + escape-html: 1.0.3 + estree-util-value-to-estree: 3.4.0 + file-loader: 6.2.0(webpack@5.99.9) + fs-extra: 11.3.0 + image-size: 2.0.2 + mdast-util-mdx: 3.0.0 + mdast-util-to-string: 4.0.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + rehype-raw: 7.0.0 + remark-directive: 3.0.1 + remark-emoji: 4.0.1 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.1 + stringify-object: 3.3.0 + tslib: 2.8.1 + unified: 11.0.5 + unist-util-visit: 5.0.0 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9) + vfile: 6.0.3 + webpack: 5.99.9 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/module-type-aliases@3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/history': 4.7.11 + '@types/react': 19.1.8 + '@types/react-router-config': 5.0.11 + '@types/react-router-dom': 5.3.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.1.0)' + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/plugin-content-blog@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + cheerio: 1.0.0-rc.12 + feed: 4.2.2 + fs-extra: 11.3.0 + lodash: 4.17.21 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + schema-dts: 1.1.5 + srcset: 4.0.0 + tslib: 2.8.1 + unist-util-visit: 5.0.0 + utility-types: 3.11.0 + webpack: 5.99.9 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/module-type-aliases': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/react-router-config': 5.0.11 + combine-promises: 1.2.0 + fs-extra: 11.3.0 + js-yaml: 4.1.0 + lodash: 4.17.21 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + schema-dts: 1.1.5 + tslib: 2.8.1 + utility-types: 3.11.0 + webpack: 5.99.9 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-content-pages@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + fs-extra: 11.3.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + webpack: 5.99.9 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-css-cascade-layers@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-debug@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + fs-extra: 11.3.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-json-view-lite: 2.4.1(react@19.1.0) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-analytics@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-gtag@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/gtag.js': 0.0.12 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-tag-manager@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-sitemap@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + fs-extra: 11.3.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + sitemap: 7.1.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-svgr@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@svgr/core': 8.1.0(typescript@5.6.3) + '@svgr/webpack': 8.1.0(typescript@5.6.3) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + webpack: 5.99.9 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/preset-classic@3.8.1(@algolia/client-search@5.28.0)(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-content-blog': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-content-pages': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-css-cascade-layers': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-debug': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-google-analytics': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-google-gtag': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-google-tag-manager': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-sitemap': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-svgr': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/theme-classic': 3.8.1(@types/react@19.1.8)(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/theme-search-algolia': 3.8.1(@algolia/client-search@5.28.0)(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)(typescript@5.6.3) + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@algolia/client-search' + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - search-insights + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/react-loadable@6.0.0(react@19.1.0)': + dependencies: + '@types/react': 19.1.8 + react: 19.1.0 + + '@docusaurus/theme-classic@3.8.1(@types/react@19.1.8)(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/module-type-aliases': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/plugin-content-blog': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/plugin-content-pages': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/theme-translations': 3.8.1 + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.1.0) + clsx: 2.1.1 + copy-text-to-clipboard: 3.2.0 + infima: 0.2.0-alpha.45 + lodash: 4.17.21 + nprogress: 0.2.0 + postcss: 8.5.6 + prism-react-renderer: 2.4.1(react@19.1.0) + prismjs: 1.30.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-router-dom: 5.3.4(react@19.1.0) + rtlcss: 4.3.0 + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/theme-common@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@docusaurus/mdx-loader': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/module-type-aliases': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/history': 4.7.11 + '@types/react': 19.1.8 + '@types/react-router-config': 5.0.11 + clsx: 2.1.1 + parse-numeric-range: 1.3.0 + prism-react-renderer: 2.4.1(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/theme-search-algolia@3.8.1(@algolia/client-search@5.28.0)(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)(typescript@5.6.3)': + dependencies: + '@docsearch/react': 3.9.0(@algolia/client-search@5.28.0)(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3) + '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/plugin-content-docs': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3))(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/theme-translations': 3.8.1 + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-validation': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + algoliasearch: 5.28.0 + algoliasearch-helper: 3.26.0(algoliasearch@5.28.0) + clsx: 2.1.1 + eta: 2.2.0 + fs-extra: 11.3.0 + lodash: 4.17.21 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@algolia/client-search' + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - search-insights + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/theme-translations@3.8.1': + dependencies: + fs-extra: 11.3.0 + tslib: 2.8.1 + + '@docusaurus/tsconfig@3.8.1': {} + + '@docusaurus/types@3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@mdx-js/mdx': 3.1.0(acorn@8.15.0) + '@types/history': 4.7.11 + '@types/react': 19.1.8 + commander: 5.1.0 + joi: 17.13.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)' + utility-types: 3.11.0 + webpack: 5.99.9 + webpack-merge: 5.10.0 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/utils-common@3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/utils-validation@3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@docusaurus/logger': 3.8.1 + '@docusaurus/utils': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + fs-extra: 11.3.0 + joi: 17.13.3 + js-yaml: 4.1.0 + lodash: 4.17.21 + tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/utils@3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@docusaurus/logger': 3.8.1 + '@docusaurus/types': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@docusaurus/utils-common': 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + escape-string-regexp: 4.0.0 + execa: 5.1.1 + file-loader: 6.2.0(webpack@5.99.9) + fs-extra: 11.3.0 + github-slugger: 1.5.0 + globby: 11.1.0 + gray-matter: 4.0.3 + jiti: 1.21.7 + js-yaml: 4.1.0 + lodash: 4.17.21 + micromatch: 4.0.8 + p-queue: 6.6.2 + prompts: 2.4.2 + resolve-pathname: 3.0.0 + tslib: 2.8.1 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9) + utility-types: 3.11.0 + webpack: 5.99.9 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.0.3 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@leichtgewicht/ip-codec@2.0.5': {} + + '@mdx-js/mdx@3.1.0(acorn@8.15.0)': + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.0(acorn@8.15.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.4 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - acorn + - supports-color + + '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.1.8 + react: 19.1.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.3.1': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@polka/url@1.0.0-next.29': {} + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@sinclair/typebox@0.27.8': {} + + '@sindresorhus/is@4.6.0': {} + + '@sindresorhus/is@5.6.0': {} + + '@slorber/react-helmet-async@1.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.6 + invariant: 2.2.4 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + + '@slorber/remark-comment@1.0.0': + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-preset@8.1.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.27.4) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.27.4) + + '@svgr/core@8.1.0(typescript@5.6.3)': + dependencies: + '@babel/core': 7.27.4 + '@svgr/babel-preset': 8.1.0(@babel/core@7.27.4) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@5.6.3) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@svgr/hast-util-to-babel-ast@8.0.0': + dependencies: + '@babel/types': 7.27.6 + entities: 4.5.0 + + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.6.3))': + dependencies: + '@babel/core': 7.27.4 + '@svgr/babel-preset': 8.1.0(@babel/core@7.27.4) + '@svgr/core': 8.1.0(typescript@5.6.3) + '@svgr/hast-util-to-babel-ast': 8.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.6.3))(typescript@5.6.3)': + dependencies: + '@svgr/core': 8.1.0(typescript@5.6.3) + cosmiconfig: 8.3.6(typescript@5.6.3) + deepmerge: 4.3.1 + svgo: 3.3.2 + transitivePeerDependencies: + - typescript + + '@svgr/webpack@8.1.0(typescript@5.6.3)': + dependencies: + '@babel/core': 7.27.4 + '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.27.4) + '@babel/preset-env': 7.27.2(@babel/core@7.27.4) + '@babel/preset-react': 7.27.1(@babel/core@7.27.4) + '@babel/preset-typescript': 7.27.1(@babel/core@7.27.4) + '@svgr/core': 8.1.0(typescript@5.6.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.6.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.6.3))(typescript@5.6.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@szmarczak/http-timer@5.0.1': + dependencies: + defer-to-connect: 2.0.1 + + '@trysound/sax@0.2.0': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.0.3 + + '@types/bonjour@3.5.13': + dependencies: + '@types/node': 24.0.3 + + '@types/connect-history-api-fallback@1.5.4': + dependencies: + '@types/express-serve-static-core': 5.0.6 + '@types/node': 24.0.3 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.0.3 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 24.0.3 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express-serve-static-core@5.0.6': + dependencies: + '@types/node': 24.0.3 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express@4.17.23': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.8 + + '@types/gtag.js@0.0.12': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/history@4.7.11': {} + + '@types/html-minifier-terser@6.1.0': {} + + '@types/http-cache-semantics@4.0.4': {} + + '@types/http-errors@2.0.5': {} + + '@types/http-proxy@1.17.16': + dependencies: + '@types/node': 24.0.3 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/json-schema@7.0.15': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/mime@1.3.5': {} + + '@types/ms@2.1.0': {} + + '@types/node-forge@1.3.11': + dependencies: + '@types/node': 24.0.3 + + '@types/node@17.0.45': {} + + '@types/node@24.0.3': + dependencies: + undici-types: 7.8.0 + + '@types/prismjs@1.26.5': {} + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-router-config@5.0.11': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.1.8 + '@types/react-router': 5.1.20 + + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.1.8 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.1.8 + + '@types/react@19.1.8': + dependencies: + csstype: 3.1.3 + + '@types/retry@0.12.0': {} + + '@types/sax@1.2.7': + dependencies: + '@types/node': 17.0.45 + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 24.0.3 + + '@types/serve-index@1.9.4': + dependencies: + '@types/express': 4.17.23 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.0.3 + '@types/send': 0.17.5 + + '@types/sockjs@0.3.36': + dependencies: + '@types/node': 24.0.3 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.0.3 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@ungap/structured-clone@1.3.0': {} + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + address@1.2.2: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + algoliasearch-helper@3.26.0(algoliasearch@5.28.0): + dependencies: + '@algolia/events': 4.0.1 + algoliasearch: 5.28.0 + + algoliasearch@5.28.0: + dependencies: + '@algolia/client-abtesting': 5.28.0 + '@algolia/client-analytics': 5.28.0 + '@algolia/client-common': 5.28.0 + '@algolia/client-insights': 5.28.0 + '@algolia/client-personalization': 5.28.0 + '@algolia/client-query-suggestions': 5.28.0 + '@algolia/client-search': 5.28.0 + '@algolia/ingestion': 1.28.0 + '@algolia/monitoring': 1.28.0 + '@algolia/recommend': 5.28.0 + '@algolia/requester-browser-xhr': 5.28.0 + '@algolia/requester-fetch': 5.28.0 + '@algolia/requester-node-http': 5.28.0 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-html-community@0.0.8: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-flatten@1.1.1: {} + + array-union@2.1.0: {} + + astring@1.9.0: {} + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + caniuse-lite: 1.0.30001723 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + babel-loader@9.2.1(@babel/core@7.27.4)(webpack@5.99.9): + dependencies: + '@babel/core': 7.27.4 + find-cache-dir: 4.0.0 + schema-utils: 4.3.2 + webpack: 5.99.9 + + babel-plugin-dynamic-import-node@2.3.3: + dependencies: + object.assign: 4.1.7 + + babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.27.4): + dependencies: + '@babel/compat-data': 7.27.5 + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.4) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.4) + core-js-compat: 3.43.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + batch@0.6.1: {} + + big.js@5.2.2: {} + + binary-extensions@2.3.0: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bonjour-service@1.3.0: + dependencies: + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + + boolbase@1.0.0: {} + + boxen@6.2.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + + boxen@7.1.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.4.1 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.0: + dependencies: + caniuse-lite: 1.0.30001723 + electron-to-chromium: 1.5.170 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.0) + + buffer-from@1.1.2: {} + + bytes@3.0.0: {} + + bytes@3.1.2: {} + + cacheable-lookup@7.0.0: {} + + cacheable-request@10.2.14: + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 6.0.1 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.2 + responselike: 3.0.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase@6.3.0: {} + + camelcase@7.0.1: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.25.0 + caniuse-lite: 1.0.30001723 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001723: {} + + ccount@2.0.1: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + char-regex@1.0.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.0.0-rc.12: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + htmlparser2: 8.0.2 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chrome-trace-event@1.0.4: {} + + ci-info@3.9.0: {} + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + clean-stack@2.2.0: {} + + cli-boxes@3.0.0: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + clsx@2.1.1: {} + + collapse-white-space@2.1.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combine-promises@1.2.0: {} + + comma-separated-tokens@2.0.3: {} + + commander@10.0.1: {} + + commander@2.20.3: {} + + commander@5.1.0: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + common-path-prefix@3.0.0: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.0: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.0.2 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@6.0.0: + dependencies: + dot-prop: 6.0.1 + graceful-fs: 4.2.11 + unique-string: 3.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 5.1.0 + + connect-history-api-fallback@2.0.0: {} + + consola@3.4.2: {} + + content-disposition@0.5.2: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + copy-text-to-clipboard@3.2.0: {} + + copy-webpack-plugin@11.0.0(webpack@5.99.9): + dependencies: + fast-glob: 3.3.3 + glob-parent: 6.0.2 + globby: 13.2.2 + normalize-path: 3.0.0 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + webpack: 5.99.9 + + core-js-compat@3.43.0: + dependencies: + browserslist: 4.25.0 + + core-js-pure@3.43.0: {} + + core-js@3.43.0: {} + + core-util-is@1.0.3: {} + + cosmiconfig@8.3.6(typescript@5.6.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.6.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-random-string@4.0.0: + dependencies: + type-fest: 1.4.0 + + css-blank-pseudo@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + css-declaration-sorter@7.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + css-has-pseudo@7.0.2(postcss@8.5.6): + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + css-loader@6.11.0(webpack@5.99.9): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) + postcss-modules-scope: 3.2.1(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + semver: 7.7.2 + optionalDependencies: + webpack: 5.99.9 + + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.99.9): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + cssnano: 6.1.2(postcss@8.5.6) + jest-worker: 29.7.0 + postcss: 8.5.6 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + webpack: 5.99.9 + optionalDependencies: + clean-css: 5.3.3 + + css-prefers-color-scheme@10.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + cssdb@8.3.0: {} + + cssesc@3.0.0: {} + + cssnano-preset-advanced@6.1.2(postcss@8.5.6): + dependencies: + autoprefixer: 10.4.21(postcss@8.5.6) + browserslist: 4.25.0 + cssnano-preset-default: 6.1.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-discard-unused: 6.0.5(postcss@8.5.6) + postcss-merge-idents: 6.0.3(postcss@8.5.6) + postcss-reduce-idents: 6.0.3(postcss@8.5.6) + postcss-zindex: 6.0.2(postcss@8.5.6) + + cssnano-preset-default@6.1.2(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + css-declaration-sorter: 7.2.0(postcss@8.5.6) + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-calc: 9.0.1(postcss@8.5.6) + postcss-colormin: 6.1.0(postcss@8.5.6) + postcss-convert-values: 6.1.0(postcss@8.5.6) + postcss-discard-comments: 6.0.2(postcss@8.5.6) + postcss-discard-duplicates: 6.0.3(postcss@8.5.6) + postcss-discard-empty: 6.0.3(postcss@8.5.6) + postcss-discard-overridden: 6.0.2(postcss@8.5.6) + postcss-merge-longhand: 6.0.5(postcss@8.5.6) + postcss-merge-rules: 6.1.1(postcss@8.5.6) + postcss-minify-font-values: 6.1.0(postcss@8.5.6) + postcss-minify-gradients: 6.0.3(postcss@8.5.6) + postcss-minify-params: 6.1.0(postcss@8.5.6) + postcss-minify-selectors: 6.0.4(postcss@8.5.6) + postcss-normalize-charset: 6.0.2(postcss@8.5.6) + postcss-normalize-display-values: 6.0.2(postcss@8.5.6) + postcss-normalize-positions: 6.0.2(postcss@8.5.6) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.6) + postcss-normalize-string: 6.0.2(postcss@8.5.6) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.6) + postcss-normalize-unicode: 6.1.0(postcss@8.5.6) + postcss-normalize-url: 6.0.2(postcss@8.5.6) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.6) + postcss-ordered-values: 6.0.2(postcss@8.5.6) + postcss-reduce-initial: 6.1.0(postcss@8.5.6) + postcss-reduce-transforms: 6.0.2(postcss@8.5.6) + postcss-svgo: 6.0.3(postcss@8.5.6) + postcss-unique-selectors: 6.0.4(postcss@8.5.6) + + cssnano-utils@4.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + cssnano@6.1.2(postcss@8.5.6): + dependencies: + cssnano-preset-default: 6.1.2(postcss@8.5.6) + lilconfig: 3.1.3 + postcss: 8.5.6 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + csstype@3.1.3: {} + + debounce@1.2.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deepmerge@4.3.1: {} + + default-gateway@6.0.3: + dependencies: + execa: 5.1.1 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + depd@1.1.2: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-node@2.1.0: {} + + detect-port@1.6.1: + dependencies: + address: 1.2.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dns-packet@5.6.1: + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + + dom-converter@0.2.0: + dependencies: + utila: 0.4.0 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.170: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + emojilib@2.4.0: {} + + emojis-list@3.0.0: {} + + emoticon@4.1.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.15.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.2 + + escalade@3.2.0: {} + + escape-goat@4.0.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + esprima@4.0.1: {} + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.4 + + estree-util-value-to-estree@3.4.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + eta@2.2.0: {} + + etag@1.8.1: {} + + eval@0.1.8: + dependencies: + '@types/node': 24.0.3 + require-like: 0.1.2 + + eventemitter3@4.0.7: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fault@2.0.1: + dependencies: + format: 0.2.2 + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + + feed@4.2.2: + dependencies: + xml-js: 1.6.11 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-loader@6.2.0(webpack@5.99.9): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.99.9 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-cache-dir@4.0.0: + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + + find-up@6.3.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + + flat@5.0.2: {} + + follow-redirects@1.15.9: {} + + form-data-encoder@2.1.4: {} + + format@0.2.2: {} + + forwarded@0.2.0: {} + + fraction.js@4.3.7: {} + + fresh@0.5.2: {} + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-monkey@1.0.6: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-own-enumerable-property-symbols@3.0.2: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + github-slugger@1.5.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + globals@11.12.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@13.2.2: + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 4.0.0 + + gopd@1.2.0: {} + + got@12.6.1: + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + + graceful-fs@4.2.10: {} + + graceful-fs@4.2.11: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + handle-thing@2.0.1: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-yarn@3.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + he@1.2.0: {} + + history@4.10.1: + dependencies: + '@babel/runtime': 7.27.6 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + hpack.js@2.1.6: + dependencies: + inherits: 2.0.4 + obuf: 1.1.2 + readable-stream: 2.3.8 + wbuf: 1.7.3 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + html-minifier-terser@6.1.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.43.0 + + html-minifier-terser@7.2.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.43.0 + + html-tags@3.3.1: {} + + html-void-elements@3.0.0: {} + + html-webpack-plugin@5.6.3(webpack@5.99.9): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.2 + optionalDependencies: + webpack: 5.99.9 + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-cache-semantics@4.2.0: {} + + http-deceiver@1.2.7: {} + + http-errors@1.6.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-parser-js@0.5.10: {} + + http-proxy-middleware@2.0.9(@types/express@4.17.23): + dependencies: + '@types/http-proxy': 1.17.16 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + optionalDependencies: + '@types/express': 4.17.23 + transitivePeerDependencies: + - debug + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.9 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + http2-wrapper@2.2.1: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + human-signals@2.1.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + ignore@5.3.2: {} + + image-size@2.0.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-lazy@4.0.0: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + infima@0.2.0-alpha.45: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@2.0.0: {} + + inline-style-parser@0.2.4: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.2.0: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-decimal@2.0.1: {} + + is-docker@2.2.1: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-npm@6.0.0: {} + + is-number@7.0.0: {} + + is-obj@1.0.1: {} + + is-obj@2.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-regexp@1.0.0: {} + + is-stream@2.0.1: {} + + is-typedarray@1.0.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-yarn-global@0.4.1: {} + + isarray@0.0.1: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 24.0.3 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-worker@27.5.1: + dependencies: + '@types/node': 24.0.3 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 24.0.3 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jiti@1.21.7: {} + + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + latest-version@7.0.0: + dependencies: + package-json: 8.1.1 + + launch-editor@2.10.0: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.8.3 + + leven@3.1.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + loader-runner@4.3.0: {} + + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash.debounce@4.0.8: {} + + lodash.memoize@4.1.2: {} + + lodash.uniq@4.5.0: {} + + lodash@4.17.21: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lowercase-keys@3.0.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + markdown-extensions@2.0.0: {} + + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + + markdown-table@3.0.4: {} + + math-intrinsics@1.1.0: {} + + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + media-typer@0.3.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.0.6 + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@3.0.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.2 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.8 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.2 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@1.1.0: {} + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@1.1.0: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.33.0: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.18: + dependencies: + mime-db: 1.33.0 + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-response@3.1.0: {} + + mimic-response@4.0.0: {} + + mini-css-extract-plugin@2.9.2(webpack@5.99.9): + dependencies: + schema-utils: 4.3.2 + tapable: 2.2.2 + webpack: 5.99.9 + + minimalistic-assert@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + mrmime@2.0.1: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multicast-dns@7.2.5: + dependencies: + dns-packet: 5.6.1 + thunky: 1.1.0 + + nanoid@3.3.11: {} + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + neo-async@2.6.2: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + + node-forge@1.3.1: {} + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + normalize-url@8.0.2: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nprogress@0.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + null-loader@4.0.1(webpack@5.99.9): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.99.9 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + obuf@1.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + opener@1.5.2: {} + + p-cancelable@3.0.0: {} + + p-finally@1.0.0: {} + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + package-json@8.1.1: + dependencies: + got: 12.6.1 + registry-auth-token: 5.1.0 + registry-url: 6.0.1 + semver: 7.7.2 + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-numeric-range@1.3.0: {} + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-is-inside@1.0.2: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@0.1.12: {} + + path-to-regexp@1.9.0: + dependencies: + isarray: 0.0.1 + + path-to-regexp@3.3.0: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pkg-dir@7.0.0: + dependencies: + find-up: 6.3.0 + + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-calc@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-clamp@4.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-color-functional-notation@7.0.10(postcss@8.5.6): + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-color-hex-alpha@10.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-color-rebeccapurple@10.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-colormin@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-convert-values@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-media@11.0.6(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + postcss-custom-properties@14.0.6(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-selectors@8.0.5(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-dir-pseudo-class@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-discard-comments@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-duplicates@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-empty@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-overridden@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-unused@6.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-double-position-gradients@6.0.2(postcss@8.5.6): + dependencies: + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-focus-visible@10.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-focus-within@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-font-variant@5.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-gap-properties@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-image-set-function@7.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-lab-function@7.0.10(postcss@8.5.6): + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.6.3)(webpack@5.99.9): + dependencies: + cosmiconfig: 8.3.6(typescript@5.6.3) + jiti: 1.21.7 + postcss: 8.5.6 + semver: 7.7.2 + webpack: 5.99.9 + transitivePeerDependencies: + - typescript + + postcss-logical@8.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-merge-idents@6.0.3(postcss@8.5.6): + dependencies: + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-merge-longhand@6.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + stylehacks: 6.1.1(postcss@8.5.6) + + postcss-merge-rules@6.1.1(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + caniuse-api: 3.0.0 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-minify-font-values@6.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@6.0.3(postcss@8.5.6): + dependencies: + colord: 2.9.3 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-params@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@6.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.6): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-modules-values@4.0.0(postcss@8.5.6): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-nesting@13.0.2(postcss@8.5.6): + dependencies: + '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.0) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-normalize-charset@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-normalize-display-values@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-opacity-percentage@3.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-ordered-values@6.0.2(postcss@8.5.6): + dependencies: + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-overflow-shorthand@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-page-break@3.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-place@10.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-preset-env@10.2.3(postcss@8.5.6): + dependencies: + '@csstools/postcss-cascade-layers': 5.0.1(postcss@8.5.6) + '@csstools/postcss-color-function': 4.0.10(postcss@8.5.6) + '@csstools/postcss-color-mix-function': 3.0.10(postcss@8.5.6) + '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.0(postcss@8.5.6) + '@csstools/postcss-content-alt-text': 2.0.6(postcss@8.5.6) + '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.6) + '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.6) + '@csstools/postcss-gamut-mapping': 2.0.10(postcss@8.5.6) + '@csstools/postcss-gradients-interpolation-method': 5.0.10(postcss@8.5.6) + '@csstools/postcss-hwb-function': 4.0.10(postcss@8.5.6) + '@csstools/postcss-ic-unit': 4.0.2(postcss@8.5.6) + '@csstools/postcss-initial': 2.0.1(postcss@8.5.6) + '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.6) + '@csstools/postcss-light-dark-function': 2.0.9(postcss@8.5.6) + '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.6) + '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.6) + '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.6) + '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.6) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.6) + '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.6) + '@csstools/postcss-normalize-display-values': 4.0.0(postcss@8.5.6) + '@csstools/postcss-oklab-function': 4.0.10(postcss@8.5.6) + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/postcss-random-function': 2.0.1(postcss@8.5.6) + '@csstools/postcss-relative-color-syntax': 3.0.10(postcss@8.5.6) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.6) + '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.6) + '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.6) + '@csstools/postcss-text-decoration-shorthand': 4.0.2(postcss@8.5.6) + '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.6) + '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.6) + autoprefixer: 10.4.21(postcss@8.5.6) + browserslist: 4.25.0 + css-blank-pseudo: 7.0.1(postcss@8.5.6) + css-has-pseudo: 7.0.2(postcss@8.5.6) + css-prefers-color-scheme: 10.0.0(postcss@8.5.6) + cssdb: 8.3.0 + postcss: 8.5.6 + postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.6) + postcss-clamp: 4.1.0(postcss@8.5.6) + postcss-color-functional-notation: 7.0.10(postcss@8.5.6) + postcss-color-hex-alpha: 10.0.0(postcss@8.5.6) + postcss-color-rebeccapurple: 10.0.0(postcss@8.5.6) + postcss-custom-media: 11.0.6(postcss@8.5.6) + postcss-custom-properties: 14.0.6(postcss@8.5.6) + postcss-custom-selectors: 8.0.5(postcss@8.5.6) + postcss-dir-pseudo-class: 9.0.1(postcss@8.5.6) + postcss-double-position-gradients: 6.0.2(postcss@8.5.6) + postcss-focus-visible: 10.0.1(postcss@8.5.6) + postcss-focus-within: 9.0.1(postcss@8.5.6) + postcss-font-variant: 5.0.0(postcss@8.5.6) + postcss-gap-properties: 6.0.0(postcss@8.5.6) + postcss-image-set-function: 7.0.0(postcss@8.5.6) + postcss-lab-function: 7.0.10(postcss@8.5.6) + postcss-logical: 8.1.0(postcss@8.5.6) + postcss-nesting: 13.0.2(postcss@8.5.6) + postcss-opacity-percentage: 3.0.0(postcss@8.5.6) + postcss-overflow-shorthand: 6.0.0(postcss@8.5.6) + postcss-page-break: 3.0.4(postcss@8.5.6) + postcss-place: 10.0.0(postcss@8.5.6) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.6) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.6) + postcss-selector-not: 8.0.1(postcss@8.5.6) + + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-reduce-idents@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-reduce-initial@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + caniuse-api: 3.0.0 + postcss: 8.5.6 + + postcss-reduce-transforms@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-not@8.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sort-media-queries@5.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + sort-css-media-queries: 2.2.0 + + postcss-svgo@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + svgo: 3.3.2 + + postcss-unique-selectors@6.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-value-parser@4.2.0: {} + + postcss-zindex@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-error@4.0.0: + dependencies: + lodash: 4.17.21 + renderkid: 3.0.0 + + pretty-time@1.1.0: {} + + prism-react-renderer@2.4.1(react@19.1.0): + dependencies: + '@types/prismjs': 1.26.5 + clsx: 2.1.1 + react: 19.1.0 + + prismjs@1.30.0: {} + + process-nextick-args@2.0.1: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@6.5.0: {} + + property-information@7.1.0: {} + + proto-list@1.2.4: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + pupa@3.1.0: + dependencies: + escape-goat: 4.0.0 + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + quick-lru@5.1.1: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.0: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-fast-compare@3.2.2: {} + + react-is@16.13.1: {} + + react-json-view-lite@2.4.1(react@19.1.0): + dependencies: + react: 19.1.0 + + react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@19.1.0))(webpack@5.99.9): + dependencies: + '@babel/runtime': 7.27.6 + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.1.0)' + webpack: 5.99.9 + + react-router-config@5.1.1(react-router@5.3.4(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.6 + react: 19.1.0 + react-router: 5.3.4(react@19.1.0) + + react-router-dom@5.3.4(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.6 + history: 4.10.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.1.0 + react-router: 5.3.4(react@19.1.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react-router@5.3.4(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.6 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + path-to-regexp: 1.9.0 + prop-types: 15.8.1 + react: 19.1.0 + react-is: 16.13.1 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react@19.1.0: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.0(acorn@8.15.0): + dependencies: + acorn-jsx: 5.3.2(acorn@8.15.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - acorn + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.8 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regexpu-core@6.2.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + registry-auth-token@5.1.0: + dependencies: + '@pnpm/npm-conf': 2.3.1 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + relateurl@0.2.7: {} + + remark-directive@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 3.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-emoji@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + emoticon: 4.1.0 + mdast-util-find-and-replace: 3.0.2 + node-emoji: 2.2.0 + unified: 11.0.5 + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.0: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + renderkid@3.0.0: + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.17.21 + strip-ansi: 6.0.1 + + repeat-string@1.6.1: {} + + require-from-string@2.0.2: {} + + require-like@0.1.2: {} + + requires-port@1.0.0: {} + + resolve-alpn@1.2.1: {} + + resolve-from@4.0.0: {} + + resolve-pathname@3.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@3.0.0: + dependencies: + lowercase-keys: 3.0.0 + + retry@0.13.1: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rtlcss@4.3.0: + dependencies: + escalade: 3.2.0 + picocolors: 1.1.1 + postcss: 8.5.6 + strip-json-comments: 3.1.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sax@1.4.1: {} + + scheduler@0.26.0: {} + + schema-dts@1.1.5: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + schema-utils@4.3.2: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + + search-insights@2.17.3: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + select-hose@2.0.0: {} + + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.11 + node-forge: 1.3.1 + + semver-diff@4.0.0: + dependencies: + semver: 7.7.2 + + semver@6.3.1: {} + + semver@7.7.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-handler@6.1.6: + dependencies: + bytes: 3.0.0 + content-disposition: 0.5.2 + mime-types: 2.1.18 + minimatch: 3.1.2 + path-is-inside: 1.0.2 + path-to-regexp: 3.3.0 + range-parser: 1.2.0 + + serve-index@1.9.1: + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.1.0: {} + + setprototypeof@1.2.0: {} + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shallowequal@1.1.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + sitemap@7.1.2: + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.1 + + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + + slash@3.0.0: {} + + slash@4.0.0: {} + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + sockjs@0.3.24: + dependencies: + faye-websocket: 0.11.4 + uuid: 8.3.2 + websocket-driver: 0.7.4 + + sort-css-media-queries@2.2.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + space-separated-tokens@2.0.2: {} + + spdy-transport@3.0.0: + dependencies: + debug: 4.4.1 + detect-node: 2.1.0 + hpack.js: 2.1.6 + obuf: 1.1.2 + readable-stream: 3.6.2 + wbuf: 1.7.3 + transitivePeerDependencies: + - supports-color + + spdy@4.0.2: + dependencies: + debug: 4.4.1 + handle-thing: 2.0.1 + http-deceiver: 1.2.7 + select-hose: 2.0.0 + spdy-transport: 3.0.0 + transitivePeerDependencies: + - supports-color + + sprintf-js@1.0.3: {} + + srcset@4.0.0: {} + + statuses@1.5.0: {} + + statuses@2.0.1: {} + + std-env@3.9.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom-string@1.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + style-to-js@1.1.17: + dependencies: + style-to-object: 1.0.9 + + style-to-object@1.0.9: + dependencies: + inline-style-parser: 0.2.4 + + stylehacks@6.1.1(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-parser@2.0.4: {} + + svgo@3.3.2: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.1.1 + + tapable@2.2.2: {} + + terser-webpack-plugin@5.3.14(webpack@5.99.9): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.43.0 + webpack: 5.99.9 + + terser@5.43.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + thunky@1.1.0: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinypool@1.1.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + totalist@3.0.1: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tslib@2.8.1: {} + + type-fest@0.21.3: {} + + type-fest@1.4.0: {} + + type-fest@2.19.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typescript@5.6.3: {} + + undici-types@7.8.0: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-emoji-modifier-base@1.0.0: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.3(browserslist@4.25.0): + dependencies: + browserslist: 4.25.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-notifier@6.0.2: + dependencies: + boxen: 7.1.1 + chalk: 5.4.1 + configstore: 6.0.0 + has-yarn: 3.0.0 + import-lazy: 4.0.0 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + is-npm: 6.0.0 + is-yarn-global: 0.4.1 + latest-version: 7.0.0 + pupa: 3.1.0 + semver: 7.7.2 + semver-diff: 4.0.0 + xdg-basedir: 5.1.0 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-loader@4.1.1(file-loader@6.2.0(webpack@5.99.9))(webpack@5.99.9): + dependencies: + loader-utils: 2.0.4 + mime-types: 2.1.35 + schema-utils: 3.3.0 + webpack: 5.99.9 + optionalDependencies: + file-loader: 6.2.0(webpack@5.99.9) + + util-deprecate@1.0.2: {} + + utila@0.4.0: {} + + utility-types@3.11.0: {} + + utils-merge@1.0.1: {} + + uuid@8.3.2: {} + + value-equal@1.0.1: {} + + vary@1.1.2: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wbuf@1.7.3: + dependencies: + minimalistic-assert: 1.0.1 + + web-namespaces@2.0.1: {} + + webpack-bundle-analyzer@4.10.2: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.15.0 + acorn-walk: 8.3.4 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + webpack-dev-middleware@5.3.4(webpack@5.99.9): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.2 + webpack: 5.99.9 + + webpack-dev-server@4.15.2(webpack@5.99.9): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.23 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.8 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.0 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.21.2 + graceful-fs: 4.2.11 + html-entities: 2.6.0 + http-proxy-middleware: 2.0.9(@types/express@4.17.23) + ipaddr.js: 2.2.0 + launch-editor: 2.10.0 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.3.2 + selfsigned: 2.4.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 5.3.4(webpack@5.99.9) + ws: 8.18.2 + optionalDependencies: + webpack: 5.99.9 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + webpack-merge@5.10.0: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-merge@6.0.1: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-sources@3.3.2: {} + + webpack@5.99.9: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + browserslist: 4.25.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.1 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.2 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(webpack@5.99.9) + watchpack: 2.4.4 + webpack-sources: 3.3.2 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + webpackbar@6.0.1(webpack@5.99.9): + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + consola: 3.4.2 + figures: 3.2.0 + markdown-table: 2.0.0 + pretty-time: 1.1.0 + std-env: 3.9.0 + webpack: 5.99.9 + wrap-ansi: 7.0.0 + + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + + wildcard@2.0.1: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + ws@7.5.10: {} + + ws@8.18.2: {} + + xdg-basedir@5.1.0: {} + + xml-js@1.6.11: + dependencies: + sax: 1.4.1 + + yallist@3.1.1: {} + + yocto-queue@1.2.1: {} + + zwitch@2.0.4: {} diff --git a/docs/sidebars.ts b/docs/sidebars.ts new file mode 100644 index 0000000..4b17fc8 --- /dev/null +++ b/docs/sidebars.ts @@ -0,0 +1,58 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; + +// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) + +/** + * Creating a sidebar enables you to: + - create an ordered group of docs + - render a sidebar for each doc of that group + - provide next/previous navigation + + The sidebars can be generated from the filesystem, or explicitly defined here. + + Create as many sidebars as you want. + */ +const sidebars: SidebarsConfig = { + // Main documentation sidebar + tutorialSidebar: [ + 'intro', + 'getting-started', + { + type: 'category', + label: 'Core Concepts', + items: [ + 'core-concepts/architecture', + 'core-concepts/models', + 'core-concepts/decorators', + ], + }, + { + type: 'category', + label: 'Query System', + items: [ + 'query-system/query-builder', + ], + }, + { + type: 'category', + label: 'Examples', + items: [ + 'examples/basic-usage', + ], + }, + ], + + // API Reference sidebar + apiSidebar: [ + 'api/overview', + { + type: 'category', + label: 'Framework Classes', + items: [ + 'api/debros-framework', + ], + }, + ], +}; + +export default sidebars; diff --git a/docs/src/components/HomepageFeatures/index.tsx b/docs/src/components/HomepageFeatures/index.tsx new file mode 100644 index 0000000..c2551fb --- /dev/null +++ b/docs/src/components/HomepageFeatures/index.tsx @@ -0,0 +1,71 @@ +import type {ReactNode} from 'react'; +import clsx from 'clsx'; +import Heading from '@theme/Heading'; +import styles from './styles.module.css'; + +type FeatureItem = { + title: string; + Svg: React.ComponentType>; + description: ReactNode; +}; + +const FeatureList: FeatureItem[] = [ + { + title: 'Easy to Use', + Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + description: ( + <> + Docusaurus was designed from the ground up to be easily installed and + used to get your website up and running quickly. + + ), + }, + { + title: 'Focus on What Matters', + Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, + description: ( + <> + Docusaurus lets you focus on your docs, and we'll do the chores. Go + ahead and move your docs into the docs directory. + + ), + }, + { + title: 'Powered by React', + Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, + description: ( + <> + Extend or customize your website layout by reusing React. Docusaurus can + be extended while reusing the same header and footer. + + ), + }, +]; + +function Feature({title, Svg, description}: FeatureItem) { + return ( +
+
+ +
+
+ {title} +

{description}

+
+
+ ); +} + +export default function HomepageFeatures(): ReactNode { + return ( +
+
+
+ {FeatureList.map((props, idx) => ( + + ))} +
+
+
+ ); +} diff --git a/docs/src/components/HomepageFeatures/styles.module.css b/docs/src/components/HomepageFeatures/styles.module.css new file mode 100644 index 0000000..b248eb2 --- /dev/null +++ b/docs/src/components/HomepageFeatures/styles.module.css @@ -0,0 +1,11 @@ +.features { + display: flex; + align-items: center; + padding: 2rem 0; + width: 100%; +} + +.featureSvg { + height: 200px; + width: 200px; +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css new file mode 100644 index 0000000..2bc6a4c --- /dev/null +++ b/docs/src/css/custom.css @@ -0,0 +1,30 @@ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #2e8555; + --ifm-color-primary-dark: #29784c; + --ifm-color-primary-darker: #277148; + --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-light: #33925d; + --ifm-color-primary-lighter: #359962; + --ifm-color-primary-lightest: #3cad6e; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); +} + +/* For readability concerns, you should choose a lighter palette in dark mode. */ +[data-theme='dark'] { + --ifm-color-primary: #25c2a0; + --ifm-color-primary-dark: #21af90; + --ifm-color-primary-darker: #1fa588; + --ifm-color-primary-darkest: #1a8870; + --ifm-color-primary-light: #29d5b0; + --ifm-color-primary-lighter: #32d8b4; + --ifm-color-primary-lightest: #4fddbf; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); +} diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css new file mode 100644 index 0000000..9f71a5d --- /dev/null +++ b/docs/src/pages/index.module.css @@ -0,0 +1,23 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding: 2rem; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx new file mode 100644 index 0000000..2e006d1 --- /dev/null +++ b/docs/src/pages/index.tsx @@ -0,0 +1,44 @@ +import type {ReactNode} from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Layout from '@theme/Layout'; +import HomepageFeatures from '@site/src/components/HomepageFeatures'; +import Heading from '@theme/Heading'; + +import styles from './index.module.css'; + +function HomepageHeader() { + const {siteConfig} = useDocusaurusContext(); + return ( +
+
+ + {siteConfig.title} + +

{siteConfig.tagline}

+
+ + Docusaurus Tutorial - 5min ⏱️ + +
+
+
+ ); +} + +export default function Home(): ReactNode { + const {siteConfig} = useDocusaurusContext(); + return ( + + +
+ +
+
+ ); +} diff --git a/docs/src/pages/markdown-page.md b/docs/src/pages/markdown-page.md new file mode 100644 index 0000000..9756c5b --- /dev/null +++ b/docs/src/pages/markdown-page.md @@ -0,0 +1,7 @@ +--- +title: Markdown page example +--- + +# Markdown page example + +You don't need React to write simple standalone pages. diff --git a/docs/static/.nojekyll b/docs/static/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/static/img/docusaurus-social-card.jpg b/docs/static/img/docusaurus-social-card.jpg new file mode 100644 index 0000000..ffcb448 Binary files /dev/null and b/docs/static/img/docusaurus-social-card.jpg differ diff --git a/docs/static/img/docusaurus.png b/docs/static/img/docusaurus.png new file mode 100644 index 0000000..f458149 Binary files /dev/null and b/docs/static/img/docusaurus.png differ diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico new file mode 100644 index 0000000..c01d54b Binary files /dev/null and b/docs/static/img/favicon.ico differ diff --git a/docs/static/img/logo.svg b/docs/static/img/logo.svg new file mode 100644 index 0000000..9db6d0d --- /dev/null +++ b/docs/static/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/static/img/undraw_docusaurus_mountain.svg b/docs/static/img/undraw_docusaurus_mountain.svg new file mode 100644 index 0000000..af961c4 --- /dev/null +++ b/docs/static/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,171 @@ + + Easy to Use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/undraw_docusaurus_react.svg b/docs/static/img/undraw_docusaurus_react.svg new file mode 100644 index 0000000..94b5cf0 --- /dev/null +++ b/docs/static/img/undraw_docusaurus_react.svg @@ -0,0 +1,170 @@ + + Powered by React + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/undraw_docusaurus_tree.svg b/docs/static/img/undraw_docusaurus_tree.svg new file mode 100644 index 0000000..d9161d3 --- /dev/null +++ b/docs/static/img/undraw_docusaurus_tree.svg @@ -0,0 +1,40 @@ + + Focus on What Matters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000..920d7a6 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,8 @@ +{ + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + "baseUrl": "." + }, + "exclude": [".docusaurus", "build"] +} diff --git a/eslint.config.js b/eslint.config.js index 738a5fa..b009638 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,7 +7,7 @@ export default [ // Base configuration for all files { files: ['**/*.{ts}'], - ignores: ['dist/**', 'docs/**', 'src/components/bot/templates/**'], + ignores: ['dist/**', 'docs/**', 'src/components/bot/templates/**', 'examples/**'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', @@ -40,9 +40,9 @@ export default [ }, }, - // TypeScript-specific configuration + // TypeScript-specific configuration for source files { - files: ['**/*.ts', '**/*.tsx'], + files: ['src/**/*.ts', 'src/**/*.tsx'], languageOptions: { parser: tseslint.parser, parserOptions: { @@ -79,4 +79,44 @@ export default [ ], }, }, + + // TypeScript-specific configuration for test files + { + files: ['tests/**/*.ts', 'tests/**/*.tsx'], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: './tsconfig.tests.json', + }, + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'default', + format: ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + { + selector: 'property', + format: null, + }, + ], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + }, + }, ]; diff --git a/examples/automatic-features-examples.ts b/examples/automatic-features-examples.ts new file mode 100644 index 0000000..fdafd6f --- /dev/null +++ b/examples/automatic-features-examples.ts @@ -0,0 +1,661 @@ +/** + * Comprehensive Examples for Automatic Features (Phase 5) + * + * This file demonstrates the automatic pinning and PubSub capabilities: + * - Smart pinning strategies based on usage patterns + * - Automatic event publishing for model changes + * - Real-time synchronization across nodes + * - Performance optimization through intelligent caching + * - Cross-node communication and coordination + */ + +import { SocialPlatformFramework, User, Post, Comment } from './framework-integration'; +import { PinningManager } from '../src/framework/pinning/PinningManager'; +import { PubSubManager } from '../src/framework/pubsub/PubSubManager'; + +export class AutomaticFeaturesExamples { + private framework: SocialPlatformFramework; + private pinningManager: PinningManager; + private pubsubManager: PubSubManager; + + constructor(framework: SocialPlatformFramework) { + this.framework = framework; + // These would be injected from the framework + this.pinningManager = (framework as any).pinningManager; + this.pubsubManager = (framework as any).pubsubManager; + } + + async runAllExamples(): Promise { + console.log('🤖 Running comprehensive automatic features examples...\n'); + + await this.pinningStrategyExamples(); + await this.automaticEventPublishingExamples(); + await this.realTimeSynchronizationExamples(); + await this.crossNodeCommunicationExamples(); + await this.performanceOptimizationExamples(); + await this.intelligentCleanupExamples(); + + console.log('✅ All automatic features examples completed!\n'); + } + + async pinningStrategyExamples(): Promise { + console.log('📌 Smart Pinning Strategy Examples'); + console.log('==================================\n'); + + // Configure different pinning strategies for different model types + console.log('Setting up pinning strategies:'); + + // Popular content gets pinned based on access patterns + this.pinningManager.setPinningRule('Post', { + strategy: 'popularity', + factor: 1.5, + maxPins: 100, + minAccessCount: 5 + }); + + // User profiles are always pinned (important core data) + this.pinningManager.setPinningRule('User', { + strategy: 'fixed', + factor: 2.0, + maxPins: 50 + }); + + // Comments use size-based pinning (prefer smaller, more efficient content) + this.pinningManager.setPinningRule('Comment', { + strategy: 'size', + factor: 1.0, + maxPins: 200 + }); + + // Create sample content and observe pinning behavior + const posts = await Post.where('isPublic', '=', true).limit(5).exec(); + + if (posts.length > 0) { + console.log('\nDemonstrating automatic pinning:'); + + for (let i = 0; i < posts.length; i++) { + const post = posts[i]; + const hash = `hash-${post.id}-${Date.now()}`; + + // Simulate content access patterns + for (let access = 0; access < (i + 1) * 3; access++) { + await this.pinningManager.recordAccess(hash); + } + + // Pin content based on strategy + const pinned = await this.pinningManager.pinContent( + hash, + 'Post', + post.id, + { + title: post.title, + createdAt: post.createdAt, + size: post.content.length + } + ); + + console.log(`Post "${post.title}": ${pinned ? 'PINNED' : 'NOT PINNED'} (${(i + 1) * 3} accesses)`); + } + + // Show pinning metrics + const metrics = this.pinningManager.getMetrics(); + console.log('\nPinning Metrics:'); + console.log(`- Total pinned: ${metrics.totalPinned}`); + console.log(`- Total size: ${(metrics.totalSize / 1024).toFixed(2)} KB`); + console.log(`- Most accessed: ${metrics.mostAccessed?.hash || 'None'}`); + console.log(`- Strategy breakdown:`); + metrics.strategyBreakdown.forEach((count, strategy) => { + console.log(` * ${strategy}: ${count} items`); + }); + } + + console.log(''); + } + + async automaticEventPublishingExamples(): Promise { + console.log('📡 Automatic Event Publishing Examples'); + console.log('======================================\n'); + + // Set up event listeners to demonstrate automatic publishing + const events: any[] = []; + + await this.pubsubManager.subscribe('model.created', (event) => { + events.push({ type: 'created', ...event }); + console.log(`🆕 Model created: ${event.data.modelName}:${event.data.modelId}`); + }); + + await this.pubsubManager.subscribe('model.updated', (event) => { + events.push({ type: 'updated', ...event }); + console.log(`📝 Model updated: ${event.data.modelName}:${event.data.modelId}`); + }); + + await this.pubsubManager.subscribe('model.deleted', (event) => { + events.push({ type: 'deleted', ...event }); + console.log(`🗑️ Model deleted: ${event.data.modelName}:${event.data.modelId}`); + }); + + console.log('Event listeners set up, creating test data...\n'); + + // Create data and observe automatic event publishing + const testUser = await User.create({ + username: `testuser-${Date.now()}`, + email: `test${Date.now()}@example.com`, + bio: 'Testing automatic event publishing' + }); + + // Simulate event emission (in real implementation, this would be automatic) + this.pubsubManager.emit('modelEvent', 'create', testUser); + + const testPost = await Post.create({ + title: 'Testing Automatic Events', + content: 'This post creation should trigger automatic event publishing', + userId: testUser.id, + isPublic: true + }); + + this.pubsubManager.emit('modelEvent', 'create', testPost); + + // Update the post + await testPost.update({ title: 'Updated: Testing Automatic Events' }); + this.pubsubManager.emit('modelEvent', 'update', testPost, { title: 'Updated title' }); + + // Wait a moment for event processing + await new Promise(resolve => setTimeout(resolve, 1000)); + + console.log(`\nCaptured ${events.length} automatic events:`); + events.forEach((event, index) => { + console.log(`${index + 1}. ${event.type}: ${event.data?.modelName || 'unknown'}`); + }); + + console.log(''); + } + + async realTimeSynchronizationExamples(): Promise { + console.log('⚡ Real-Time Synchronization Examples'); + console.log('=====================================\n'); + + // Simulate multiple nodes subscribing to the same topics + const nodeEvents: Record = { + node1: [], + node2: [], + node3: [] + }; + + // Subscribe each "node" to model events + await this.pubsubManager.subscribe('model.*', (event) => { + nodeEvents.node1.push(event); + }, { + filter: (event) => event.data.modelName === 'Post' + }); + + await this.pubsubManager.subscribe('model.*', (event) => { + nodeEvents.node2.push(event); + }, { + filter: (event) => event.data.modelName === 'User' + }); + + await this.pubsubManager.subscribe('model.*', (event) => { + nodeEvents.node3.push(event); + }); // No filter - receives all events + + console.log('Multiple nodes subscribed to synchronization topics'); + + // Generate events that should synchronize across nodes + const syncUser = await User.create({ + username: `syncuser-${Date.now()}`, + email: `sync${Date.now()}@example.com`, + bio: 'User for testing real-time sync' + }); + + const syncPost = await Post.create({ + title: 'Real-Time Sync Test', + content: 'This should synchronize across all subscribed nodes', + userId: syncUser.id, + isPublic: true + }); + + // Emit events + await this.pubsubManager.publish('model.created', { + modelName: 'User', + modelId: syncUser.id, + timestamp: Date.now() + }); + + await this.pubsubManager.publish('model.created', { + modelName: 'Post', + modelId: syncPost.id, + timestamp: Date.now() + }); + + // Wait for synchronization + await new Promise(resolve => setTimeout(resolve, 1500)); + + console.log('\nSynchronization results:'); + console.log(`Node 1 (Post filter): ${nodeEvents.node1.length} events received`); + console.log(`Node 2 (User filter): ${nodeEvents.node2.length} events received`); + console.log(`Node 3 (No filter): ${nodeEvents.node3.length} events received`); + + // Demonstrate conflict resolution + console.log('\nSimulating conflict resolution:'); + await this.pubsubManager.publish('database.conflict', { + modelName: 'Post', + modelId: syncPost.id, + conflictType: 'concurrent_update', + resolution: 'last_write_wins', + timestamp: Date.now() + }); + + console.log(''); + } + + async crossNodeCommunicationExamples(): Promise { + console.log('🌐 Cross-Node Communication Examples'); + console.log('====================================\n'); + + // Simulate coordination between nodes + const coordinationEvents: any[] = []; + + // Set up coordination topics + await this.pubsubManager.subscribe('node.heartbeat', (event) => { + coordinationEvents.push(event); + console.log(`💓 Heartbeat from ${event.source}: ${event.data.status}`); + }); + + await this.pubsubManager.subscribe('node.resource', (event) => { + coordinationEvents.push(event); + console.log(`📊 Resource update from ${event.source}: ${event.data.type}`); + }); + + await this.pubsubManager.subscribe('cluster.rebalance', (event) => { + coordinationEvents.push(event); + console.log(`⚖️ Cluster rebalance initiated: ${event.data.reason}`); + }); + + console.log('Cross-node communication channels established\n'); + + // Simulate node communication + await this.pubsubManager.publish('node.heartbeat', { + status: 'healthy', + load: 0.65, + memory: '2.1GB', + connections: 42 + }); + + await this.pubsubManager.publish('node.resource', { + type: 'storage', + available: '5.2TB', + used: '2.8TB', + threshold: 0.8 + }); + + await this.pubsubManager.publish('cluster.rebalance', { + reason: 'load_balancing', + nodes: ['node-a', 'node-b', 'node-c'], + strategy: 'round_robin' + }); + + // Demonstrate distributed consensus + console.log('Initiating distributed consensus...'); + await this.pubsubManager.publish('consensus.propose', { + proposalId: `proposal-${Date.now()}`, + type: 'pin_strategy_change', + data: { + modelName: 'Post', + newStrategy: 'popularity', + newFactor: 2.0 + }, + requiredVotes: 3 + }); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + console.log(`\nCommunication events processed: ${coordinationEvents.length}`); + console.log('Cross-node coordination completed successfully'); + + console.log(''); + } + + async performanceOptimizationExamples(): Promise { + console.log('🚀 Performance Optimization Examples'); + console.log('====================================\n'); + + // Demonstrate intelligent cache warming + console.log('1. Intelligent Cache Warming:'); + const popularPosts = await Post + .where('isPublic', '=', true) + .where('likeCount', '>', 10) + .orderBy('likeCount', 'desc') + .limit(10) + .exec(); + + // Pre-pin popular content + for (const post of popularPosts) { + const hash = `hash-${post.id}-content`; + await this.pinningManager.pinContent(hash, 'Post', post.id, { + title: post.title, + likeCount: post.likeCount, + priority: 'high' + }); + } + console.log(`Pre-pinned ${popularPosts.length} popular posts for better performance`); + + // Demonstrate predictive pinning + console.log('\n2. Predictive Pinning:'); + const analysis = this.pinningManager.analyzePerformance(); + console.log(`Current hit rate: ${(analysis.hitRate * 100).toFixed(2)}%`); + console.log(`Storage efficiency: ${(analysis.storageEfficiency * 100).toFixed(2)}%`); + console.log(`Average priority: ${analysis.averagePriority.toFixed(3)}`); + + // Simulate access pattern analysis + const accessPatterns = this.analyzeAccessPatterns(); + console.log(`\n3. Access Pattern Analysis:`); + console.log(`Peak access time: ${accessPatterns.peakHour}:00`); + console.log(`Most accessed content type: ${accessPatterns.mostAccessedType}`); + console.log(`Cache miss rate: ${(accessPatterns.missRate * 100).toFixed(2)}%`); + + // Optimize based on patterns + if (accessPatterns.missRate > 0.1) { // 10% miss rate + console.log('\nHigh miss rate detected, optimizing...'); + await this.optimizePinningStrategy(accessPatterns); + } + + console.log(''); + } + + async intelligentCleanupExamples(): Promise { + console.log('🧹 Intelligent Cleanup Examples'); + console.log('===============================\n'); + + // Get initial stats + const initialStats = this.pinningManager.getStats(); + console.log('Initial state:'); + console.log(`- Pinned items: ${initialStats.totalPinned}`); + console.log(`- Total size: ${(initialStats.totalSize / 1024).toFixed(2)} KB`); + + // Create some test content that will be cleaned up + const testHashes = []; + for (let i = 0; i < 10; i++) { + const hash = `test-cleanup-${i}-${Date.now()}`; + testHashes.push(hash); + + await this.pinningManager.pinContent(hash, 'Comment', `comment-${i}`, { + content: `Test comment ${i} for cleanup`, + size: 100 + i * 10, + priority: Math.random() * 0.3 // Low priority + }); + } + + console.log(`\nCreated ${testHashes.length} test items for cleanup`); + + // Simulate time passing (items become stale) + console.log('Simulating passage of time...'); + + // Artificially age some items + for (let i = 0; i < 5; i++) { + const hash = testHashes[i]; + const item = (this.pinningManager as any).pinnedItems.get(hash); + if (item) { + item.lastAccessed = Date.now() - (8 * 24 * 60 * 60 * 1000); // 8 days ago + item.accessCount = 1; // Very low access + } + } + + // Trigger cleanup + console.log('Triggering intelligent cleanup...'); + const cleanedItems = await (this.pinningManager as any).performCleanup(); + + const finalStats = this.pinningManager.getStats(); + console.log('\nCleanup results:'); + console.log(`- Items after cleanup: ${finalStats.totalPinned}`); + console.log(`- Size freed: ${((initialStats.totalSize - finalStats.totalSize) / 1024).toFixed(2)} KB`); + console.log(`- Cleanup efficiency: ${((initialStats.totalPinned - finalStats.totalPinned) / initialStats.totalPinned * 100).toFixed(2)}%`); + + // Demonstrate memory optimization + console.log('\nMemory optimization metrics:'); + const memoryAnalysis = this.analyzeMemoryUsage(); + console.log(`- Memory utilization: ${(memoryAnalysis.utilization * 100).toFixed(2)}%`); + console.log(`- Fragmentation ratio: ${(memoryAnalysis.fragmentation * 100).toFixed(2)}%`); + console.log(`- Recommended cleanup interval: ${memoryAnalysis.recommendedInterval}ms`); + + console.log(''); + } + + // Helper methods for analysis and optimization + + private analyzeAccessPatterns(): any { + // Simulate access pattern analysis + return { + peakHour: 14, // 2 PM + mostAccessedType: 'Post', + missRate: 0.15, + trendsDetected: ['increased_mobile_access', 'peak_evening_hours'], + recommendations: ['increase_post_pinning', 'reduce_comment_pinning'] + }; + } + + private async optimizePinningStrategy(patterns: any): Promise { + console.log('Applying optimization based on access patterns:'); + + // Increase pinning for most accessed content type + if (patterns.mostAccessedType === 'Post') { + this.pinningManager.setPinningRule('Post', { + strategy: 'popularity', + factor: 2.0, + maxPins: 150 // Increased from 100 + }); + console.log('- Increased Post pinning capacity'); + } + + // Adjust cleanup frequency based on miss rate + if (patterns.missRate > 0.2) { + // More aggressive cleanup needed + console.log('- Enabled more aggressive cleanup'); + } + + console.log('Optimization complete'); + } + + private analyzeMemoryUsage(): any { + const stats = this.pinningManager.getStats(); + + return { + utilization: stats.totalSize / (10 * 1024 * 1024), // Assuming 10MB limit + fragmentation: 0.12, // 12% fragmentation + recommendedInterval: stats.totalPinned > 100 ? 30000 : 60000, // More frequent cleanup if many items + hotspots: ['user_profiles', 'recent_posts'], + coldSpots: ['old_comments', 'archived_content'] + }; + } + + async demonstrateAdvancedAutomation(): Promise { + console.log('🤖 Advanced Automation Demonstration'); + console.log('===================================\n'); + + // Demonstrate self-healing capabilities + console.log('1. Self-Healing System:'); + + // Simulate node failure detection + await this.pubsubManager.publish('node.failure', { + nodeId: 'node-beta', + reason: 'network_timeout', + lastSeen: Date.now() - 30000 + }); + + // Automatic rebalancing + await this.pubsubManager.publish('cluster.rebalance', { + trigger: 'node_failure', + failedNode: 'node-beta', + redistribution: { + 'node-alpha': 0.6, + 'node-gamma': 0.4 + } + }); + + console.log('Self-healing sequence initiated and completed'); + + // Demonstrate adaptive optimization + console.log('\n2. Adaptive Optimization:'); + const performance = this.pinningManager.analyzePerformance(); + + if (performance.hitRate < 0.8) { + console.log('Low hit rate detected, adapting pinning strategy...'); + // Auto-adjust pinning factors + this.pinningManager.setPinningRule('Post', { + strategy: 'popularity', + factor: performance.averagePriority + 0.5 // Increase based on current performance + }); + console.log('Pinning strategy adapted automatically'); + } + + // Demonstrate predictive scaling + console.log('\n3. Predictive Scaling:'); + const predictions = this.generateLoadPredictions(); + console.log(`Predicted load increase: ${predictions.expectedIncrease}%`); + console.log(`Recommended action: ${predictions.recommendation}`); + + if (predictions.expectedIncrease > 50) { + console.log('Preemptively scaling resources...'); + await this.pubsubManager.publish('cluster.scale', { + type: 'predictive', + factor: 1.5, + reason: 'anticipated_load_increase' + }); + } + + console.log('Advanced automation demonstration completed\n'); + } + + private generateLoadPredictions(): any { + // Simulate machine learning-based load prediction + return { + expectedIncrease: Math.random() * 100, + confidence: 0.85, + timeframe: '2 hours', + recommendation: 'scale_up', + factors: ['user_growth', 'content_creation_spike', 'viral_post_detected'] + }; + } +} + +// Usage function +export async function runAutomaticFeaturesExamples( + orbitDBService: any, + ipfsService: any +): Promise { + const framework = new SocialPlatformFramework(); + + try { + await framework.initialize(orbitDBService, ipfsService, 'development'); + + // Initialize automatic features (would be done in framework initialization) + const pinningManager = new PinningManager(ipfsService, { + maxTotalPins: 1000, + maxTotalSize: 50 * 1024 * 1024, // 50MB + cleanupIntervalMs: 30000 // 30 seconds for demo + }); + + const pubsubManager = new PubSubManager(ipfsService, { + enabled: true, + autoPublishModelEvents: true, + autoPublishDatabaseEvents: true, + topicPrefix: 'debros-demo' + }); + + await pubsubManager.initialize(); + + // Inject into framework for examples + (framework as any).pinningManager = pinningManager; + (framework as any).pubsubManager = pubsubManager; + + // Ensure sample data exists + await createSampleDataForAutomaticFeatures(framework); + + // Run all examples + const examples = new AutomaticFeaturesExamples(framework); + await examples.runAllExamples(); + await examples.demonstrateAdvancedAutomation(); + + // Show final statistics + console.log('📊 Final System Statistics:'); + console.log('=========================='); + + const pinningStats = pinningManager.getStats(); + const pubsubStats = pubsubManager.getStats(); + const frameworkStats = await framework.getFrameworkStats(); + + console.log('\nPinning System:'); + console.log(`- Total pinned: ${pinningStats.totalPinned}`); + console.log(`- Total size: ${(pinningStats.totalSize / 1024).toFixed(2)} KB`); + console.log(`- Active strategies: ${Object.keys(pinningStats.strategies).join(', ')}`); + + console.log('\nPubSub System:'); + console.log(`- Messages published: ${pubsubStats.totalPublished}`); + console.log(`- Messages received: ${pubsubStats.totalReceived}`); + console.log(`- Active subscriptions: ${pubsubStats.totalSubscriptions}`); + console.log(`- Average latency: ${pubsubStats.averageLatency.toFixed(2)}ms`); + + console.log('\nFramework:'); + console.log(`- Models registered: ${frameworkStats.registeredModels.length}`); + console.log(`- Cache hit rate: ${(frameworkStats.cache.query.stats.hitRate * 100).toFixed(2)}%`); + + // Cleanup + await pinningManager.shutdown(); + await pubsubManager.shutdown(); + + } catch (error) { + console.error('❌ Automatic features examples failed:', error); + } finally { + await framework.stop(); + } +} + +async function createSampleDataForAutomaticFeatures(framework: SocialPlatformFramework): Promise { + console.log('🗄️ Creating sample data for automatic features...\n'); + + try { + // Create users with varied activity patterns + const users = []; + for (let i = 0; i < 5; i++) { + const user = await framework.createUser({ + username: `autouser${i}`, + email: `autouser${i}@example.com`, + bio: `Automatic features test user ${i}` + }); + users.push(user); + } + + // Create posts with different popularity levels + const posts = []; + for (let i = 0; i < 15; i++) { + const user = users[i % users.length]; + const post = await framework.createPost(user.id, { + title: `Auto Post ${i}: ${['Popular', 'Normal', 'Unpopular'][i % 3]} Content`, + content: `This is test content for automatic features. Post ${i} with length ${100 + i * 50}.`, + tags: ['automation', 'testing', i % 2 === 0 ? 'popular' : 'normal'], + isPublic: true + }); + + // Simulate different like counts + (post as any).likeCount = i < 5 ? 20 + i * 5 : i < 10 ? 5 + i : i % 3; + await post.save(); + + posts.push(post); + } + + // Create comments to establish relationships + for (let i = 0; i < 25; i++) { + const user = users[i % users.length]; + const post = posts[i % posts.length]; + await framework.createComment( + user.id, + post.id, + `Auto comment ${i}: This is a test comment for automatic features testing.` + ); + } + + console.log(`✅ Created ${users.length} users, ${posts.length} posts, and 25 comments\n`); + + } catch (error) { + console.warn('⚠️ Some sample data creation failed:', error); + } +} \ No newline at end of file diff --git a/examples/basic-usage.ts b/examples/basic-usage.ts new file mode 100644 index 0000000..622e8f3 --- /dev/null +++ b/examples/basic-usage.ts @@ -0,0 +1,114 @@ +import { BaseModel, Model, Field, BelongsTo, HasMany } from '../src/framework'; + +// Example User model +@Model({ + scope: 'global', + type: 'docstore', + pinning: { strategy: 'fixed', factor: 2 } +}) +export class User extends BaseModel { + @Field({ type: 'string', required: true }) + username!: string; + + @Field({ type: 'string', required: true }) + email!: string; + + @Field({ type: 'string', required: false }) + bio?: string; + + @Field({ type: 'number', default: 0 }) + postCount!: number; + + @HasMany(Post, 'userId') + posts!: Post[]; +} + +// Example Post model +@Model({ + scope: 'user', + type: 'docstore', + pinning: { strategy: 'popularity', factor: 3 } +}) +export class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title!: string; + + @Field({ type: 'string', required: true }) + content!: string; + + @Field({ type: 'string', required: true }) + userId!: string; + + @Field({ type: 'boolean', default: true }) + isPublic!: boolean; + + @Field({ type: 'array', default: [] }) + tags!: string[]; + + @BelongsTo(User, 'userId') + author!: User; + + @HasMany(Comment, 'postId') + comments!: Comment[]; +} + +// Example Comment model +@Model({ + scope: 'user', + type: 'docstore' +}) +export class Comment extends BaseModel { + @Field({ type: 'string', required: true }) + content!: string; + + @Field({ type: 'string', required: true }) + userId!: string; + + @Field({ type: 'string', required: true }) + postId!: string; + + @BelongsTo(User, 'userId') + author!: User; + + @BelongsTo(Post, 'postId') + post!: Post; +} + +// Example usage (this would work once database integration is complete) +async function exampleUsage() { + try { + // Create a new user + const user = new User({ + username: 'john_doe', + email: 'john@example.com', + bio: 'A passionate developer' + }); + + // The decorators ensure validation + await user.save(); // This will validate fields and run hooks + + // Create a post + const post = new Post({ + title: 'My First Post', + content: 'This is my first post using the DebrosFramework!', + userId: user.id, + tags: ['framework', 'orbitdb', 'ipfs'] + }); + + await post.save(); + + // Query posts (these methods will work once QueryExecutor is implemented) + // const publicPosts = await Post + // .where('isPublic', '=', true) + // .load(['author']) + // .orderBy('createdAt', 'desc') + // .limit(10) + // .exec(); + + console.log('Models created successfully!'); + } catch (error) { + console.error('Error:', error); + } +} + +export { exampleUsage }; \ No newline at end of file diff --git a/examples/complete-framework-example.ts b/examples/complete-framework-example.ts new file mode 100644 index 0000000..9117105 --- /dev/null +++ b/examples/complete-framework-example.ts @@ -0,0 +1,793 @@ +/** + * Complete DebrosFramework Example + * + * This example demonstrates the complete DebrosFramework in action, + * showcasing all major features and capabilities in a real-world scenario: + * - Framework initialization with all components + * - Model definition with decorators and relationships + * - Database operations and querying + * - Automatic features (pinning, PubSub, caching) + * - Migration system for schema evolution + * - Performance monitoring and optimization + * - Error handling and recovery + */ + +import { + DebrosFramework, + BaseModel, + Model, + Field, + BelongsTo, + HasMany, + BeforeCreate, + AfterCreate, + createMigration, + DEVELOPMENT_CONFIG, + PRODUCTION_CONFIG +} from '../src/framework'; + +// Define comprehensive models for a decentralized social platform + +@Model({ + scope: 'global', + type: 'docstore', + pinning: { strategy: 'fixed', factor: 3 }, + sharding: { strategy: 'hash', count: 8, key: 'id' } +}) +export class User extends BaseModel { + @Field({ type: 'string', required: true, unique: true }) + username!: string; + + @Field({ type: 'string', required: true, unique: true }) + email!: string; + + @Field({ type: 'string', required: false }) + bio?: string; + + @Field({ type: 'string', required: false }) + profilePicture?: string; + + @Field({ type: 'boolean', default: false }) + isVerified!: boolean; + + @Field({ type: 'number', default: 0 }) + followerCount!: number; + + @Field({ type: 'number', default: 0 }) + followingCount!: number; + + @Field({ type: 'object', default: {} }) + settings!: any; + + @HasMany(Post, 'userId') + posts!: Post[]; + + @HasMany(Follow, 'followerId') + following!: Follow[]; + + @BeforeCreate() + async validateUser() { + if (this.username.length < 3) { + throw new Error('Username must be at least 3 characters long'); + } + + if (!this.email.includes('@')) { + throw new Error('Invalid email format'); + } + } + + @AfterCreate() + async setupUserDefaults() { + this.settings = { + theme: 'light', + notifications: true, + privacy: 'public', + createdAt: Date.now() + }; + } + + // Custom methods + async updateProfile(updates: { bio?: string; profilePicture?: string }): Promise { + Object.assign(this, updates); + await this.save(); + } + + async getPopularPosts(limit: number = 10): Promise { + return await Post + .whereUser(this.id) + .where('isPublic', '=', true) + .orderBy('likeCount', 'desc') + .limit(limit) + .exec(); + } +} + +@Model({ + scope: 'user', + type: 'docstore', + pinning: { strategy: 'popularity', factor: 1.5 } +}) +export class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title!: string; + + @Field({ type: 'string', required: true }) + content!: string; + + @Field({ type: 'string', required: true }) + userId!: string; + + @Field({ type: 'boolean', default: true }) + isPublic!: boolean; + + @Field({ type: 'array', default: [] }) + tags!: string[]; + + @Field({ type: 'number', default: 0 }) + likeCount!: number; + + @Field({ type: 'number', default: 0 }) + commentCount!: number; + + @Field({ type: 'string', default: 'text' }) + contentType!: string; + + @Field({ type: 'object', default: {} }) + metadata!: any; + + @BelongsTo(User, 'userId') + author!: User; + + @HasMany(Comment, 'postId') + comments!: Comment[]; + + @BeforeCreate() + async processContent() { + // Auto-detect content type and extract metadata + this.metadata = { + wordCount: this.content.split(' ').length, + hasLinks: /https?:\/\//.test(this.content), + hashtags: this.extractHashtags(), + readTime: Math.ceil(this.content.split(' ').length / 200) // Reading speed + }; + + if (this.metadata.hasLinks) { + this.contentType = 'rich'; + } + } + + private extractHashtags(): string[] { + const hashtags = this.content.match(/#\w+/g) || []; + return hashtags.map(tag => tag.slice(1).toLowerCase()); + } + + async toggleLike(): Promise { + this.likeCount += 1; + await this.save(); + } + + async addComment(userId: string, content: string): Promise { + const comment = await Comment.create({ + content, + userId, + postId: this.id + }); + + this.commentCount += 1; + await this.save(); + + return comment; + } +} + +@Model({ + scope: 'user', + type: 'docstore' +}) +export class Comment extends BaseModel { + @Field({ type: 'string', required: true }) + content!: string; + + @Field({ type: 'string', required: true }) + userId!: string; + + @Field({ type: 'string', required: true }) + postId!: string; + + @Field({ type: 'string', required: false }) + parentId?: string; + + @Field({ type: 'number', default: 0 }) + likeCount!: number; + + @Field({ type: 'number', default: 0 }) + threadDepth!: number; + + @BelongsTo(User, 'userId') + author!: User; + + @BelongsTo(Post, 'postId') + post!: Post; + + @BelongsTo(Comment, 'parentId') + parent?: Comment; + + @HasMany(Comment, 'parentId') + replies!: Comment[]; +} + +@Model({ + scope: 'global', + type: 'keyvalue' +}) +export class Follow extends BaseModel { + @Field({ type: 'string', required: true }) + followerId!: string; + + @Field({ type: 'string', required: true }) + followingId!: string; + + @Field({ type: 'boolean', default: false }) + isMutual!: boolean; + + @Field({ type: 'string', default: 'general' }) + category!: string; + + @BelongsTo(User, 'followerId') + follower!: User; + + @BelongsTo(User, 'followingId') + following!: User; +} + +export class CompleteFrameworkExample { + private framework: DebrosFramework; + private sampleUsers: User[] = []; + private samplePosts: Post[] = []; + + constructor() { + // Initialize framework with comprehensive configuration + this.framework = new DebrosFramework({ + ...DEVELOPMENT_CONFIG, + features: { + autoMigration: true, + automaticPinning: true, + pubsub: true, + queryCache: true, + relationshipCache: true + }, + performance: { + queryTimeout: 30000, + migrationTimeout: 300000, + maxConcurrentOperations: 200, + batchSize: 100 + }, + monitoring: { + enableMetrics: true, + logLevel: 'info', + metricsInterval: 30000 + } + }); + } + + async runCompleteExample(): Promise { + console.log('🎯 Running Complete DebrosFramework Example'); + console.log('==========================================\n'); + + try { + await this.initializeFramework(); + await this.setupModelsAndMigrations(); + await this.demonstrateModelOperations(); + await this.demonstrateQuerySystem(); + await this.demonstrateRelationships(); + await this.demonstrateAutomaticFeatures(); + await this.demonstratePerformanceOptimization(); + await this.demonstrateErrorHandling(); + await this.showFrameworkStatistics(); + + console.log('✅ Complete framework example finished successfully!\n'); + + } catch (error) { + console.error('❌ Framework example failed:', error); + throw error; + } finally { + await this.cleanup(); + } + } + + async initializeFramework(): Promise { + console.log('🚀 Initializing DebrosFramework'); + console.log('===============================\n'); + + // In a real application, you would pass actual OrbitDB and IPFS instances + const mockOrbitDB = this.createMockOrbitDB(); + const mockIPFS = this.createMockIPFS(); + + await this.framework.initialize(mockOrbitDB, mockIPFS); + + // Register models + this.framework.registerModel(User); + this.framework.registerModel(Post); + this.framework.registerModel(Comment); + this.framework.registerModel(Follow); + + console.log('Framework initialization completed'); + console.log(`Status: ${this.framework.getStatus().healthy ? 'Healthy' : 'Unhealthy'}`); + console.log(`Environment: ${this.framework.getStatus().environment}`); + console.log(''); + } + + async setupModelsAndMigrations(): Promise { + console.log('🔄 Setting Up Models and Migrations'); + console.log('===================================\n'); + + // Create sample migrations + const addProfileEnhancements = createMigration( + 'add_profile_enhancements', + '1.1.0', + 'Add profile enhancements to User model' + ) + .description('Add profile picture and verification status to users') + .addField('User', 'profilePicture', { + type: 'string', + required: false + }) + .addField('User', 'isVerified', { + type: 'boolean', + default: false + }) + .build(); + + const addPostMetadata = createMigration( + 'add_post_metadata', + '1.2.0', + 'Add metadata to Post model' + ) + .description('Add content metadata and engagement metrics') + .addField('Post', 'contentType', { + type: 'string', + default: 'text' + }) + .addField('Post', 'metadata', { + type: 'object', + default: {} + }) + .transformData('Post', (post) => { + return { + ...post, + metadata: { + wordCount: post.content ? post.content.split(' ').length : 0, + transformedAt: Date.now() + } + }; + }) + .build(); + + // Register migrations + await this.framework.registerMigration(addProfileEnhancements); + await this.framework.registerMigration(addPostMetadata); + + // Run pending migrations + const pendingMigrations = this.framework.getPendingMigrations(); + console.log(`Found ${pendingMigrations.length} pending migrations`); + + if (pendingMigrations.length > 0) { + const migrationManager = this.framework.getMigrationManager(); + if (migrationManager) { + const results = await migrationManager.runPendingMigrations({ + dryRun: false, + stopOnError: true + }); + console.log(`Completed ${results.filter(r => r.success).length} migrations`); + } + } + + console.log(''); + } + + async demonstrateModelOperations(): Promise { + console.log('👥 Demonstrating Model Operations'); + console.log('=================================\n'); + + // Create users with validation and hooks + console.log('Creating users...'); + for (let i = 0; i < 5; i++) { + const user = await User.create({ + username: `frameuser${i}`, + email: `frameuser${i}@example.com`, + bio: `Framework test user ${i} with comprehensive features`, + isVerified: i < 2 // First two users are verified + }); + + this.sampleUsers.push(user); + console.log(`✅ Created user: ${user.username} (verified: ${user.isVerified})`); + } + + // Create posts with automatic content processing + console.log('\nCreating posts...'); + for (let i = 0; i < 10; i++) { + const user = this.sampleUsers[i % this.sampleUsers.length]; + const post = await Post.create({ + title: `Framework Demo Post ${i + 1}`, + content: `This is a comprehensive demo post ${i + 1} showcasing the DebrosFramework capabilities. #framework #demo #orbitdb ${i % 3 === 0 ? 'https://example.com' : ''}`, + userId: user.id, + isPublic: true, + tags: ['framework', 'demo', 'test'] + }); + + this.samplePosts.push(post); + console.log(`✅ Created post: "${post.title}" by ${user.username}`); + console.log(` Content type: ${post.contentType}, Word count: ${post.metadata.wordCount}`); + } + + // Create comments and follows + console.log('\nCreating interactions...'); + let commentCount = 0; + let followCount = 0; + + for (let i = 0; i < 15; i++) { + const user = this.sampleUsers[Math.floor(Math.random() * this.sampleUsers.length)]; + const post = this.samplePosts[Math.floor(Math.random() * this.samplePosts.length)]; + + await Comment.create({ + content: `This is comment ${i + 1} on the framework demo post. Great work!`, + userId: user.id, + postId: post.id + }); + commentCount++; + } + + // Create follow relationships + for (let i = 0; i < this.sampleUsers.length; i++) { + for (let j = 0; j < this.sampleUsers.length; j++) { + if (i !== j && Math.random() > 0.6) { + await Follow.create({ + followerId: this.sampleUsers[i].id, + followingId: this.sampleUsers[j].id, + category: 'general' + }); + followCount++; + } + } + } + + console.log(`✅ Created ${commentCount} comments and ${followCount} follow relationships`); + console.log(''); + } + + async demonstrateQuerySystem(): Promise { + console.log('🔍 Demonstrating Advanced Query System'); + console.log('======================================\n'); + + // Complex queries with caching + console.log('1. Complex filtering and sorting:'); + const popularPosts = await Post + .where('isPublic', '=', true) + .where('likeCount', '>', 0) + .orderBy('likeCount', 'desc') + .orderBy('createdAt', 'desc') + .limit(5) + .exec(); + + console.log(`Found ${popularPosts.length} popular posts`); + + // User-scoped queries + console.log('\n2. User-scoped queries:'); + const userPosts = await Post + .whereUser(this.sampleUsers[0].id) + .where('isPublic', '=', true) + .exec(); + + console.log(`User ${this.sampleUsers[0].username} has ${userPosts.length} public posts`); + + // Aggregation queries + console.log('\n3. Aggregation queries:'); + const totalPosts = await Post.count(); + const totalPublicPosts = await Post.where('isPublic', '=', true).count(); + const averageLikes = await Post.avg('likeCount'); + + console.log(`Total posts: ${totalPosts}`); + console.log(`Public posts: ${totalPublicPosts}`); + console.log(`Average likes: ${averageLikes.toFixed(2)}`); + + // Query with relationships + console.log('\n4. Queries with relationships:'); + const postsWithAuthors = await Post + .where('isPublic', '=', true) + .with(['author']) + .limit(3) + .exec(); + + console.log('Posts with preloaded authors:'); + postsWithAuthors.forEach(post => { + const author = post.getRelation('author'); + console.log(`- "${post.title}" by ${author ? author.username : 'Unknown'}`); + }); + + console.log(''); + } + + async demonstrateRelationships(): Promise { + console.log('🔗 Demonstrating Relationship System'); + console.log('====================================\n'); + + const user = this.sampleUsers[0]; + const post = this.samplePosts[0]; + + // Lazy loading + console.log('1. Lazy loading relationships:'); + console.log(`Loading posts for user: ${user.username}`); + const userPosts = await user.loadRelation('posts'); + console.log(`Loaded ${Array.isArray(userPosts) ? userPosts.length : 0} posts`); + + console.log(`\nLoading comments for post: "${post.title}"`); + const comments = await post.loadRelation('comments'); + console.log(`Loaded ${Array.isArray(comments) ? comments.length : 0} comments`); + + // Eager loading + console.log('\n2. Eager loading for multiple items:'); + const relationshipManager = this.framework.getRelationshipManager(); + if (relationshipManager) { + await relationshipManager.eagerLoadRelationships( + this.samplePosts.slice(0, 3), + ['author', 'comments'] + ); + + console.log('Eager loaded author and comments for 3 posts:'); + this.samplePosts.slice(0, 3).forEach((post, index) => { + const author = post.getRelation('author'); + const comments = post.getRelation('comments') || []; + console.log(`${index + 1}. "${post.title}" by ${author ? author.username : 'Unknown'} (${comments.length} comments)`); + }); + } + + // Relationship constraints + console.log('\n3. Constrained relationship loading:'); + const recentComments = await post.loadRelationWithConstraints('comments', (query) => + query.where('createdAt', '>', Date.now() - 86400000) // Last 24 hours + .orderBy('createdAt', 'desc') + .limit(3) + ); + + console.log(`Loaded ${Array.isArray(recentComments) ? recentComments.length : 0} recent comments`); + + console.log(''); + } + + async demonstrateAutomaticFeatures(): Promise { + console.log('🤖 Demonstrating Automatic Features'); + console.log('===================================\n'); + + // Pinning system + console.log('1. Automatic pinning system:'); + const pinningManager = this.framework.getPinningManager(); + if (pinningManager) { + // Setup pinning rules + pinningManager.setPinningRule('Post', { + strategy: 'popularity', + factor: 1.5, + maxPins: 50 + }); + + pinningManager.setPinningRule('User', { + strategy: 'fixed', + factor: 2.0, + maxPins: 20 + }); + + // Simulate content pinning + for (let i = 0; i < 5; i++) { + const post = this.samplePosts[i]; + const hash = `content-hash-${post.id}`; + + // Simulate access + await pinningManager.recordAccess(hash); + await pinningManager.recordAccess(hash); + + const pinned = await pinningManager.pinContent(hash, 'Post', post.id, { + title: post.title, + likeCount: post.likeCount + }); + + console.log(`Post "${post.title}": ${pinned ? 'PINNED' : 'NOT PINNED'}`); + } + + const pinningStats = pinningManager.getStats(); + console.log(`Pinning stats: ${pinningStats.totalPinned} items pinned`); + } + + // PubSub system + console.log('\n2. PubSub event system:'); + const pubsubManager = this.framework.getPubSubManager(); + if (pubsubManager) { + let eventCount = 0; + + // Subscribe to model events + await pubsubManager.subscribe('model.*', (event) => { + eventCount++; + console.log(`📡 Event: ${event.type} for ${event.data?.modelName || 'unknown'}`); + }); + + // Simulate model events + await pubsubManager.publish('model.created', { + modelName: 'Post', + modelId: 'demo-post-1', + userId: 'demo-user-1' + }); + + await pubsubManager.publish('model.updated', { + modelName: 'User', + modelId: 'demo-user-1', + changes: { bio: 'Updated bio' } + }); + + // Wait for event processing + await new Promise(resolve => setTimeout(resolve, 1000)); + + console.log(`Processed ${eventCount} events`); + + const pubsubStats = pubsubManager.getStats(); + console.log(`PubSub stats: ${pubsubStats.totalPublished} published, ${pubsubStats.totalReceived} received`); + } + + console.log(''); + } + + async demonstratePerformanceOptimization(): Promise { + console.log('🚀 Demonstrating Performance Features'); + console.log('=====================================\n'); + + // Cache warming + console.log('1. Cache warming and optimization:'); + await this.framework.warmupCaches(); + + // Query performance comparison + console.log('\n2. Query performance comparison:'); + const startTime = Date.now(); + + // First query (cold cache) + await Post.where('isPublic', '=', true).limit(5).exec(); + const coldTime = Date.now() - startTime; + + const warmStartTime = Date.now(); + // Second query (warm cache) + await Post.where('isPublic', '=', true).limit(5).exec(); + const warmTime = Date.now() - warmStartTime; + + console.log(`Cold cache query: ${coldTime}ms`); + console.log(`Warm cache query: ${warmTime}ms`); + console.log(`Performance improvement: ${coldTime > 0 ? (coldTime / Math.max(warmTime, 1)).toFixed(2) : 'N/A'}x`); + + // Relationship loading optimization + console.log('\n3. Relationship loading optimization:'); + const relationshipManager = this.framework.getRelationshipManager(); + if (relationshipManager) { + const stats = relationshipManager.getRelationshipCacheStats(); + console.log(`Relationship cache: ${stats.cache.totalEntries} entries`); + console.log(`Cache hit rate: ${(stats.cache.hitRate * 100).toFixed(2)}%`); + } + + console.log(''); + } + + async demonstrateErrorHandling(): Promise { + console.log('⚠️ Demonstrating Error Handling'); + console.log('=================================\n'); + + // Validation errors + console.log('1. Model validation errors:'); + try { + await User.create({ + username: 'x', // Too short + email: 'invalid-email' // Invalid format + }); + } catch (error: any) { + console.log(`✅ Caught validation error: ${error.message}`); + } + + // Query errors + console.log('\n2. Query timeout handling:'); + try { + // Simulate slow query + const result = await Post.where('nonExistentField', '=', 'value').exec(); + console.log(`Query result: ${result.length} items`); + } catch (error: any) { + console.log(`✅ Handled query error gracefully: ${error.message}`); + } + + // Migration rollback + console.log('\n3. Migration error recovery:'); + const migrationManager = this.framework.getMigrationManager(); + if (migrationManager) { + try { + const riskyMigration = createMigration( + 'risky_migration', + '99.0.0', + 'Intentionally failing migration' + ) + .customOperation('Post', async () => { + throw new Error('Simulated migration failure'); + }) + .build(); + + await migrationManager.registerMigration(riskyMigration); + await migrationManager.runMigration(riskyMigration.id); + } catch (error: any) { + console.log(`✅ Migration failed as expected and rolled back: ${error.message}`); + } + } + + console.log(''); + } + + async showFrameworkStatistics(): Promise { + console.log('📊 Framework Statistics'); + console.log('=======================\n'); + + const status = this.framework.getStatus(); + const metrics = this.framework.getMetrics(); + + console.log('Status:'); + console.log(`- Initialized: ${status.initialized}`); + console.log(`- Healthy: ${status.healthy}`); + console.log(`- Version: ${status.version}`); + console.log(`- Environment: ${status.environment}`); + console.log(`- Services: ${Object.entries(status.services).map(([name, status]) => `${name}:${status}`).join(', ')}`); + + console.log('\nMetrics:'); + console.log(`- Uptime: ${(metrics.uptime / 1000).toFixed(2)} seconds`); + console.log(`- Total models: ${metrics.totalModels}`); + console.log(`- Queries executed: ${metrics.queriesExecuted}`); + console.log(`- Migrations run: ${metrics.migrationsRun}`); + console.log(`- Cache hit rate: ${(metrics.cacheHitRate * 100).toFixed(2)}%`); + console.log(`- Average query time: ${metrics.averageQueryTime.toFixed(2)}ms`); + + console.log('\nMemory Usage:'); + console.log(`- Query cache: ${(metrics.memoryUsage.queryCache / 1024).toFixed(2)} KB`); + console.log(`- Relationship cache: ${(metrics.memoryUsage.relationshipCache / 1024).toFixed(2)} KB`); + console.log(`- Total: ${(metrics.memoryUsage.total / 1024).toFixed(2)} KB`); + + console.log(''); + } + + async cleanup(): Promise { + console.log('🧹 Cleaning up framework...'); + await this.framework.stop(); + console.log('✅ Framework stopped and cleaned up'); + } + + // Mock service creation (in real usage, these would be actual services) + private createMockOrbitDB(): any { + return { + create: async () => ({ add: async () => {}, get: async () => [], all: async () => [] }), + open: async () => ({ add: async () => {}, get: async () => [], all: async () => [] }), + disconnect: async () => {}, + stores: {} + }; + } + + private createMockIPFS(): any { + return { + add: async () => ({ cid: 'mock-cid' }), + cat: async () => Buffer.from('mock data'), + pin: { add: async () => {}, rm: async () => {} }, + pubsub: { + subscribe: async () => {}, + unsubscribe: async () => {}, + publish: async () => {} + }, + object: { stat: async () => ({ CumulativeSize: 1024 }) } + }; + } +} + +// Usage function +export async function runCompleteFrameworkExample(): Promise { + const example = new CompleteFrameworkExample(); + await example.runCompleteExample(); +} + +// Run if called directly +if (require.main === module) { + runCompleteFrameworkExample().catch(console.error); +} \ No newline at end of file diff --git a/examples/framework-integration.ts b/examples/framework-integration.ts new file mode 100644 index 0000000..ecc306e --- /dev/null +++ b/examples/framework-integration.ts @@ -0,0 +1,524 @@ +/** + * Example: Integrating DebrosFramework with existing OrbitDB/IPFS services + * + * This example shows how to: + * 1. Initialize the framework with your existing services + * 2. Create models with different scopes and configurations + * 3. Use the framework for CRUD operations + * 4. Handle user-scoped vs global data + */ + +import { + BaseModel, + Model, + Field, + BelongsTo, + HasMany, + ModelRegistry, + DatabaseManager, + ShardManager, + FrameworkOrbitDBService, + FrameworkIPFSService, + ConfigManager, + QueryCache, + RelationshipManager +} from '../src/framework'; + +// Example models for a social platform +@Model({ + scope: 'global', + type: 'docstore', + pinning: { strategy: 'fixed', factor: 3 } +}) +export class User extends BaseModel { + @Field({ type: 'string', required: true, unique: true }) + username!: string; + + @Field({ type: 'string', required: true }) + email!: string; + + @Field({ type: 'string', required: false }) + bio?: string; + + @Field({ type: 'number', default: 0 }) + followerCount!: number; + + @HasMany(Post, 'userId') + posts!: Post[]; + + @HasMany(Follow, 'followerId') + following!: Follow[]; +} + +@Model({ + scope: 'user', + type: 'docstore', + pinning: { strategy: 'popularity', factor: 2 }, + sharding: { strategy: 'hash', count: 4, key: 'id' } +}) +export class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title!: string; + + @Field({ type: 'string', required: true }) + content!: string; + + @Field({ type: 'string', required: true }) + userId!: string; + + @Field({ type: 'boolean', default: true }) + isPublic!: boolean; + + @Field({ type: 'array', default: [] }) + tags!: string[]; + + @Field({ type: 'number', default: 0 }) + likeCount!: number; + + @BelongsTo(User, 'userId') + author!: User; + + @HasMany(Comment, 'postId') + comments!: Comment[]; +} + +@Model({ + scope: 'user', + type: 'docstore' +}) +export class Comment extends BaseModel { + @Field({ type: 'string', required: true }) + content!: string; + + @Field({ type: 'string', required: true }) + userId!: string; + + @Field({ type: 'string', required: true }) + postId!: string; + + @BelongsTo(User, 'userId') + author!: User; + + @BelongsTo(Post, 'postId') + post!: Post; +} + +@Model({ + scope: 'global', + type: 'keyvalue' +}) +export class Follow extends BaseModel { + @Field({ type: 'string', required: true }) + followerId!: string; + + @Field({ type: 'string', required: true }) + followingId!: string; + + @BelongsTo(User, 'followerId') + follower!: User; + + @BelongsTo(User, 'followingId') + following!: User; +} + +// Framework Integration Class +export class SocialPlatformFramework { + private databaseManager!: DatabaseManager; + private shardManager!: ShardManager; + private configManager!: ConfigManager; + private queryCache!: QueryCache; + private relationshipManager!: RelationshipManager; + private initialized: boolean = false; + + async initialize( + existingOrbitDBService: any, + existingIPFSService: any, + environment: 'development' | 'production' | 'test' = 'development' + ): Promise { + console.log('🚀 Initializing Social Platform Framework...'); + + // Create configuration based on environment + let config; + switch (environment) { + case 'production': + config = ConfigManager.productionConfig(); + break; + case 'test': + config = ConfigManager.testConfig(); + break; + default: + config = ConfigManager.developmentConfig(); + } + + this.configManager = new ConfigManager(config); + + // Wrap existing services + const frameworkOrbitDB = new FrameworkOrbitDBService(existingOrbitDBService); + const frameworkIPFS = new FrameworkIPFSService(existingIPFSService); + + // Initialize services + await frameworkOrbitDB.init(); + await frameworkIPFS.init(); + + // Create framework components + this.databaseManager = new DatabaseManager(frameworkOrbitDB); + this.shardManager = new ShardManager(); + this.shardManager.setOrbitDBService(frameworkOrbitDB); + + // Initialize databases for all registered models + await this.databaseManager.initializeAllDatabases(); + + // Create shards for global models that need them + const globalModels = ModelRegistry.getGlobalModels(); + for (const model of globalModels) { + if (model.sharding) { + await this.shardManager.createShards( + model.modelName, + model.sharding, + model.dbType + ); + } + } + + // Create global indexes for user-scoped models + const userModels = ModelRegistry.getUserScopedModels(); + for (const model of userModels) { + const indexName = `${model.modelName}GlobalIndex`; + await this.shardManager.createGlobalIndex(model.modelName, indexName); + } + + // Initialize query cache + const cacheConfig = this.configManager.cacheConfig; + this.queryCache = new QueryCache( + cacheConfig?.maxSize || 1000, + cacheConfig?.ttl || 300000 + ); + + // Initialize relationship manager + this.relationshipManager = new RelationshipManager({ + databaseManager: this.databaseManager, + shardManager: this.shardManager, + queryCache: this.queryCache + }); + + // Store framework instance globally for BaseModel access + (globalThis as any).__debrosFramework = { + databaseManager: this.databaseManager, + shardManager: this.shardManager, + configManager: this.configManager, + queryCache: this.queryCache, + relationshipManager: this.relationshipManager + }; + + this.initialized = true; + console.log('✅ Social Platform Framework initialized successfully!'); + } + + async createUser(userData: { username: string; email: string; bio?: string }): Promise { + if (!this.initialized) { + throw new Error('Framework not initialized'); + } + + // Create user in global database + const user = new User(userData); + await user.save(); + + // Create user-specific databases + await this.databaseManager.createUserDatabases(user.id); + + console.log(`👤 Created user: ${user.username} (${user.id})`); + return user; + } + + async createPost( + userId: string, + postData: { title: string; content: string; tags?: string[]; isPublic?: boolean } + ): Promise { + if (!this.initialized) { + throw new Error('Framework not initialized'); + } + + const post = new Post({ + ...postData, + userId + }); + + await post.save(); + + // Add to global index for cross-user queries + const globalIndexName = 'PostGlobalIndex'; + await this.shardManager.addToGlobalIndex(globalIndexName, post.id, { + id: post.id, + userId: post.userId, + title: post.title, + isPublic: post.isPublic, + createdAt: post.createdAt, + tags: post.tags + }); + + console.log(`📝 Created post: ${post.title} by user ${userId}`); + return post; + } + + async createComment( + userId: string, + postId: string, + content: string + ): Promise { + if (!this.initialized) { + throw new Error('Framework not initialized'); + } + + const comment = new Comment({ + content, + userId, + postId + }); + + await comment.save(); + + console.log(`💬 Created comment on post ${postId} by user ${userId}`); + return comment; + } + + async followUser(followerId: string, followingId: string): Promise { + if (!this.initialized) { + throw new Error('Framework not initialized'); + } + + const follow = new Follow({ + followerId, + followingId + }); + + await follow.save(); + + console.log(`👥 User ${followerId} followed user ${followingId}`); + return follow; + } + + // Fully functional query methods + async getPublicPosts(limit: number = 10): Promise { + console.log(`🔍 Querying for ${limit} public posts...`); + + return await Post + .where('isPublic', '=', true) + .orderBy('createdAt', 'desc') + .limit(limit) + .exec(); + } + + async getUserPosts(userId: string, limit: number = 20): Promise { + console.log(`🔍 Getting posts for user ${userId}...`); + + return await Post + .whereUser(userId) + .orderBy('createdAt', 'desc') + .limit(limit) + .exec(); + } + + async searchPosts(searchTerm: string, limit: number = 50): Promise { + console.log(`🔍 Searching posts for: ${searchTerm}`); + + return await Post + .where('isPublic', '=', true) + .orWhere(query => { + query.whereLike('title', searchTerm) + .whereLike('content', searchTerm); + }) + .orderBy('createdAt', 'desc') + .limit(limit) + .exec(); + } + + async getPostsWithComments(userId: string, limit: number = 10): Promise { + console.log(`🔍 Getting posts with comments for user ${userId}...`); + + const posts = await Post + .whereUser(userId) + .orderBy('createdAt', 'desc') + .limit(limit) + .exec(); + + // Load relationships for all posts + await this.relationshipManager.eagerLoadRelationships(posts, ['comments', 'author']); + + return posts; + } + + async getPostsWithFilteredComments(userId: string, minCommentLength: number = 10): Promise { + console.log(`🔍 Getting posts with filtered comments for user ${userId}...`); + + const posts = await Post + .whereUser(userId) + .orderBy('createdAt', 'desc') + .limit(10) + .exec(); + + // Load comments with constraints + for (const post of posts) { + await post.loadRelationWithConstraints('comments', (query) => + query.where('content', '>', minCommentLength) + .orderBy('createdAt', 'desc') + .limit(5) + ); + + // Also load the author + await post.loadRelation('author'); + } + + return posts; + } + + async getUserStats(userId: string): Promise { + console.log(`📊 Getting stats for user ${userId}...`); + + const [postCount, totalLikes] = await Promise.all([ + Post.whereUser(userId).count(), + Post.whereUser(userId).sum('likeCount') + ]); + + return { + userId, + postCount, + totalLikes, + averageLikes: postCount > 0 ? totalLikes / postCount : 0 + }; + } + + async getFrameworkStats(): Promise { + if (!this.initialized) { + throw new Error('Framework not initialized'); + } + + const stats = { + initialized: this.initialized, + registeredModels: ModelRegistry.getModelNames(), + globalModels: ModelRegistry.getGlobalModels().map(m => m.name), + userScopedModels: ModelRegistry.getUserScopedModels().map(m => m.name), + shardsInfo: this.shardManager.getAllModelsWithShards().map(modelName => + this.shardManager.getShardStatistics(modelName) + ), + config: this.configManager.getConfig(), + cache: { + query: { + stats: this.queryCache.getStats(), + usage: this.queryCache.analyzeUsage(), + popular: this.queryCache.getPopularEntries(5) + }, + relationships: this.relationshipManager.getRelationshipCacheStats() + } + }; + + return stats; + } + + async explainQuery(query: any): Promise { + console.log(`📊 Analyzing query...`); + return query.explain(); + } + + async warmupCache(): Promise { + console.log(`🔥 Warming up caches...`); + + // Warm up query cache + const commonQueries = [ + Post.where('isPublic', '=', true).orderBy('createdAt', 'desc').limit(10), + User.orderBy('followerCount', 'desc').limit(20), + Follow.limit(100) + ]; + + await this.queryCache.warmup(commonQueries); + + // Warm up relationship cache + const users = await User.limit(5).exec(); + const posts = await Post.where('isPublic', '=', true).limit(10).exec(); + + if (users.length > 0) { + await this.relationshipManager.warmupRelationshipCache(users, ['posts', 'following']); + } + + if (posts.length > 0) { + await this.relationshipManager.warmupRelationshipCache(posts, ['author', 'comments']); + } + } + + async stop(): Promise { + if (!this.initialized) { + return; + } + + console.log('🛑 Stopping Social Platform Framework...'); + + await this.databaseManager.stop(); + await this.shardManager.stop(); + this.queryCache.clear(); + this.relationshipManager.clearRelationshipCache(); + + // Clear global reference + delete (globalThis as any).__debrosFramework; + + this.initialized = false; + console.log('✅ Framework stopped successfully'); + } +} + +// Example usage function +export async function exampleUsage(orbitDBService: any, ipfsService: any) { + const framework = new SocialPlatformFramework(); + + try { + // Initialize framework with existing services + await framework.initialize(orbitDBService, ipfsService, 'development'); + + // Create some users + const alice = await framework.createUser({ + username: 'alice', + email: 'alice@example.com', + bio: 'Love decentralized tech!' + }); + + const bob = await framework.createUser({ + username: 'bob', + email: 'bob@example.com', + bio: 'Building the future' + }); + + // Create posts + const post1 = await framework.createPost(alice.id, { + title: 'Welcome to the Decentralized Web', + content: 'This is my first post using the DebrosFramework!', + tags: ['web3', 'decentralized', 'orbitdb'], + isPublic: true + }); + + const post2 = await framework.createPost(bob.id, { + title: 'Framework Architecture', + content: 'The new framework handles database partitioning automatically.', + tags: ['framework', 'architecture'], + isPublic: true + }); + + // Create comments + await framework.createComment(bob.id, post1.id, 'Great post Alice!'); + await framework.createComment(alice.id, post2.id, 'Thanks for building this!'); + + // Follow users + await framework.followUser(alice.id, bob.id); + + // Get framework statistics + const stats = await framework.getFrameworkStats(); + console.log('📊 Framework Statistics:', JSON.stringify(stats, null, 2)); + + console.log('✅ Example usage completed successfully!'); + + return { framework, users: { alice, bob }, posts: { post1, post2 } }; + } catch (error) { + console.error('❌ Example usage failed:', error); + await framework.stop(); + throw error; + } +} + +export { SocialPlatformFramework }; \ No newline at end of file diff --git a/examples/migration-examples.ts b/examples/migration-examples.ts new file mode 100644 index 0000000..f0d3daf --- /dev/null +++ b/examples/migration-examples.ts @@ -0,0 +1,932 @@ +/** + * Comprehensive Migration Examples for DebrosFramework + * + * This file demonstrates the migration system capabilities: + * - Schema evolution with field additions and modifications + * - Data transformation and migration + * - Rollback scenarios and recovery + * - Cross-model relationship changes + * - Performance optimization migrations + * - Version management and dependency handling + */ + +import { MigrationManager, Migration } from '../src/framework/migrations/MigrationManager'; +import { MigrationBuilder, createMigration } from '../src/framework/migrations/MigrationBuilder'; +import { SocialPlatformFramework } from './framework-integration'; + +export class MigrationExamples { + private migrationManager: MigrationManager; + private framework: SocialPlatformFramework; + + constructor(framework: SocialPlatformFramework) { + this.framework = framework; + this.migrationManager = new MigrationManager( + (framework as any).databaseManager, + (framework as any).shardManager + ); + } + + async runAllExamples(): Promise { + console.log('🔄 Running comprehensive migration examples...\n'); + + await this.createExampleMigrations(); + await this.basicMigrationExamples(); + await this.complexDataTransformationExamples(); + await this.rollbackAndRecoveryExamples(); + await this.performanceOptimizationExamples(); + await this.crossModelMigrationExamples(); + await this.versionManagementExamples(); + + console.log('✅ All migration examples completed!\n'); + } + + async createExampleMigrations(): Promise { + console.log('📝 Creating Example Migrations'); + console.log('==============================\n'); + + // Migration 1: Add timestamps to User model + const addTimestampsMigration = createMigration( + 'add_user_timestamps', + '1.0.1', + 'Add timestamps to User model' + ) + .description('Add createdAt and updatedAt timestamps to User model for better tracking') + .author('Framework Team') + .tags('schema', 'timestamps', 'user') + .addTimestamps('User') + .addValidator( + 'validate_timestamp_format', + 'Ensure timestamp fields are valid numbers', + async (context) => { + const errors: string[] = []; + const warnings: string[] = []; + + // Validate that all timestamps are valid + return { valid: errors.length === 0, errors, warnings }; + } + ) + .build(); + + // Migration 2: Add user profile enhancements + const userProfileEnhancement = createMigration( + 'enhance_user_profile', + '1.1.0', + 'Enhance User profile with additional fields' + ) + .description('Add profile picture, location, and social links to User model') + .dependencies('add_user_timestamps') + .addField('User', 'profilePicture', { + type: 'string', + required: false, + validate: (value) => !value || value.startsWith('http') + }) + .addField('User', 'location', { + type: 'string', + required: false + }) + .addField('User', 'socialLinks', { + type: 'array', + required: false, + default: [] + }) + .addField('User', 'isVerified', { + type: 'boolean', + required: false, + default: false + }) + .build(); + + // Migration 3: Restructure Post content + const postContentRestructure = createMigration( + 'restructure_post_content', + '1.2.0', + 'Restructure Post content with rich metadata' + ) + .description('Transform Post content from plain text to rich content structure') + .addField('Post', 'contentType', { + type: 'string', + required: false, + default: 'text' + }) + .addField('Post', 'metadata', { + type: 'object', + required: false, + default: {} + }) + .transformData('Post', (post) => { + // Transform existing content to new structure + const wordCount = post.content ? post.content.split(' ').length : 0; + const hasLinks = post.content ? /https?:\/\//.test(post.content) : false; + + return { + ...post, + contentType: hasLinks ? 'rich' : 'text', + metadata: { + wordCount, + hasLinks, + transformedAt: Date.now() + } + }; + }) + .build(); + + // Migration 4: Add Comment threading + const commentThreading = createMigration( + 'add_comment_threading', + '1.3.0', + 'Add threading support to Comments' + ) + .description('Add parent-child relationships to comments for threading') + .addField('Comment', 'parentId', { + type: 'string', + required: false, + default: null + }) + .addField('Comment', 'threadDepth', { + type: 'number', + required: false, + default: 0 + }) + .addField('Comment', 'childCount', { + type: 'number', + required: false, + default: 0 + }) + .transformData('Comment', (comment) => { + // All existing comments become root-level comments + return { + ...comment, + parentId: null, + threadDepth: 0, + childCount: 0 + }; + }) + .build(); + + // Migration 5: Performance optimization + const performanceOptimization = createMigration( + 'optimize_post_indexing', + '1.4.0', + 'Optimize Post model for better query performance' + ) + .description('Add computed fields and indexes for better query performance') + .addField('Post', 'searchText', { + type: 'string', + required: false, + default: '' + }) + .addField('Post', 'popularityScore', { + type: 'number', + required: false, + default: 0 + }) + .transformData('Post', (post) => { + // Create searchable text and calculate popularity + const searchText = `${post.title || ''} ${post.content || ''}`.toLowerCase(); + const popularityScore = (post.likeCount || 0) * 2 + (post.commentCount || 0); + + return { + ...post, + searchText, + popularityScore + }; + }) + .createIndex('Post', ['searchText']) + .createIndex('Post', ['popularityScore'], { name: 'popularity_index' }) + .build(); + + // Register all migrations + const migrations = [ + addTimestampsMigration, + userProfileEnhancement, + postContentRestructure, + commentThreading, + performanceOptimization + ]; + + for (const migration of migrations) { + this.migrationManager.registerMigration(migration); + console.log(`✅ Registered migration: ${migration.name} (v${migration.version})`); + } + + console.log(`\nRegistered ${migrations.length} example migrations\n`); + } + + async basicMigrationExamples(): Promise { + console.log('🔄 Basic Migration Examples'); + console.log('===========================\n'); + + // Get pending migrations + const pendingMigrations = this.migrationManager.getPendingMigrations(); + console.log(`Found ${pendingMigrations.length} pending migrations:`); + + pendingMigrations.forEach(migration => { + console.log(`- ${migration.name} (v${migration.version})`); + }); + + // Run a single migration with dry run first + if (pendingMigrations.length > 0) { + const firstMigration = pendingMigrations[0]; + + console.log(`\nRunning dry run for: ${firstMigration.name}`); + const dryRunResult = await this.migrationManager.runMigration(firstMigration.id, { + dryRun: true + }); + + console.log('Dry run results:'); + console.log(`- Success: ${dryRunResult.success}`); + console.log(`- Estimated records: ${dryRunResult.recordsProcessed}`); + console.log(`- Duration: ${dryRunResult.duration}ms`); + console.log(`- Warnings: ${dryRunResult.warnings.length}`); + + // Run the actual migration + console.log(`\nRunning actual migration: ${firstMigration.name}`); + try { + const result = await this.migrationManager.runMigration(firstMigration.id, { + batchSize: 50 + }); + + console.log('Migration results:'); + console.log(`- Success: ${result.success}`); + console.log(`- Records processed: ${result.recordsProcessed}`); + console.log(`- Records modified: ${result.recordsModified}`); + console.log(`- Duration: ${result.duration}ms`); + console.log(`- Rollback available: ${result.rollbackAvailable}`); + + if (result.warnings.length > 0) { + console.log('- Warnings:', result.warnings); + } + + } catch (error) { + console.error(`Migration failed: ${error}`); + } + } + + console.log(''); + } + + async complexDataTransformationExamples(): Promise { + console.log('🔄 Complex Data Transformation Examples'); + console.log('=======================================\n'); + + // Create a complex migration that transforms user data + const userDataNormalization = createMigration( + 'normalize_user_data', + '2.0.0', + 'Normalize and clean user data' + ) + .description('Clean up user data, normalize email formats, and merge duplicate accounts') + .transformData('User', (user) => { + // Normalize email to lowercase + if (user.email) { + user.email = user.email.toLowerCase().trim(); + } + + // Clean up username + if (user.username) { + user.username = user.username.trim().replace(/[^a-zA-Z0-9_]/g, ''); + } + + // Add normalized search fields + user.searchName = (user.username || '').toLowerCase(); + user.displayName = user.username || user.email?.split('@')[0] || 'Anonymous'; + + return user; + }) + .addValidator( + 'validate_email_uniqueness', + 'Ensure email addresses are unique after normalization', + async (context) => { + // Simulation of validation logic + return { + valid: true, + errors: [], + warnings: ['Some duplicate emails may have been found'] + }; + } + ) + .build(); + + this.migrationManager.registerMigration(userDataNormalization); + + // Create a migration that handles relationship data + const postRelationshipMigration = createMigration( + 'update_post_relationships', + '2.1.0', + 'Update Post relationship structure' + ) + .description('Restructure how posts relate to users and add engagement metrics') + .addField('Post', 'engagementScore', { + type: 'number', + required: false, + default: 0 + }) + .addField('Post', 'lastActivityAt', { + type: 'number', + required: false, + default: Date.now() + }) + .customOperation('Post', async (context) => { + context.logger.info('Calculating engagement scores for all posts'); + + // Simulate complex calculation across related models + const posts = await context.databaseManager.getAllRecords('Post'); + + for (const post of posts) { + // Get related comments and likes + const comments = await context.databaseManager.getRelatedRecords('Comment', 'postId', post.id); + const likes = post.likeCount || 0; + + // Calculate engagement score + const engagementScore = (comments.length * 2) + likes; + const lastActivityAt = comments.length > 0 + ? Math.max(...comments.map((c: any) => c.createdAt || 0)) + : post.createdAt || Date.now(); + + post.engagementScore = engagementScore; + post.lastActivityAt = lastActivityAt; + + await context.databaseManager.updateRecord('Post', post); + } + }) + .build(); + + this.migrationManager.registerMigration(postRelationshipMigration); + + console.log('Created complex data transformation migrations'); + console.log('- User data normalization'); + console.log('- Post relationship updates with engagement scoring'); + + console.log(''); + } + + async rollbackAndRecoveryExamples(): Promise { + console.log('↩️ Rollback and Recovery Examples'); + console.log('==================================\n'); + + // Create a migration that might fail + const riskyMigration = createMigration( + 'risky_data_migration', + '2.2.0', + 'Risky data migration (demonstration)' + ) + .description('A migration that demonstrates rollback capabilities') + .addField('User', 'tempField', { + type: 'string', + required: false, + default: 'temp' + }) + .customOperation('User', async (context) => { + context.logger.info('Performing risky operation that might fail'); + + // Simulate a 50% chance of failure for demonstration + if (Math.random() > 0.5) { + throw new Error('Simulated operation failure for rollback demonstration'); + } + + context.logger.info('Risky operation completed successfully'); + }) + .build(); + + this.migrationManager.registerMigration(riskyMigration); + + try { + console.log('Running risky migration (may fail)...'); + const result = await this.migrationManager.runMigration(riskyMigration.id); + console.log(`Migration result: ${result.success ? 'SUCCESS' : 'FAILED'}`); + + if (result.success) { + console.log('Migration succeeded, demonstrating rollback...'); + + // Demonstrate manual rollback + const rollbackResult = await this.migrationManager.rollbackMigration(riskyMigration.id); + console.log(`Rollback result: ${rollbackResult.success ? 'SUCCESS' : 'FAILED'}`); + console.log(`Rollback duration: ${rollbackResult.duration}ms`); + } + + } catch (error) { + console.log(`Migration failed as expected: ${error}`); + + // Check migration history + const history = this.migrationManager.getMigrationHistory(riskyMigration.id); + console.log(`Migration attempts: ${history.length}`); + + if (history.length > 0) { + const lastAttempt = history[history.length - 1]; + console.log(`Last attempt result: ${lastAttempt.success ? 'SUCCESS' : 'FAILED'}`); + console.log(`Rollback available: ${lastAttempt.rollbackAvailable}`); + } + } + + // Demonstrate recovery scenarios + console.log('\nDemonstrating recovery scenarios...'); + + const recoveryMigration = createMigration( + 'recovery_migration', + '2.3.0', + 'Recovery migration with validation' + ) + .description('Migration with comprehensive pre and post validation') + .addValidator( + 'pre_migration_check', + 'Validate system state before migration', + async (context) => { + context.logger.info('Running pre-migration validation'); + return { + valid: true, + errors: [], + warnings: ['System is ready for migration'] + }; + } + ) + .addField('Post', 'recoveryField', { + type: 'string', + required: false, + default: 'recovered' + }) + .addValidator( + 'post_migration_check', + 'Validate migration results', + async (context) => { + context.logger.info('Running post-migration validation'); + return { + valid: true, + errors: [], + warnings: ['Migration completed successfully'] + }; + } + ) + .build(); + + this.migrationManager.registerMigration(recoveryMigration); + console.log('Created recovery migration with validation'); + + console.log(''); + } + + async performanceOptimizationExamples(): Promise { + console.log('🚀 Performance Optimization Migration Examples'); + console.log('===============================================\n'); + + // Create migrations that optimize different aspects + const indexOptimization = createMigration( + 'optimize_search_indexes', + '3.0.0', + 'Optimize search and query performance' + ) + .description('Add indexes and computed fields for better query performance') + .createIndex('User', ['email'], { unique: true, name: 'user_email_unique' }) + .createIndex('User', ['username'], { unique: true, name: 'user_username_unique' }) + .createIndex('Post', ['userId', 'createdAt'], { name: 'user_posts_timeline' }) + .createIndex('Post', ['isPublic', 'popularityScore'], { name: 'public_popular_posts' }) + .createIndex('Comment', ['postId', 'createdAt'], { name: 'post_comments_timeline' }) + .build(); + + const dataArchiving = createMigration( + 'archive_old_data', + '3.1.0', + 'Archive old inactive data' + ) + .description('Move old inactive data to archive tables for better performance') + .addField('Post', 'isArchived', { + type: 'boolean', + required: false, + default: false + }) + .addField('Comment', 'isArchived', { + type: 'boolean', + required: false, + default: false + }) + .customOperation('Post', async (context) => { + context.logger.info('Archiving old posts'); + + const cutoffDate = Date.now() - (365 * 24 * 60 * 60 * 1000); // 1 year ago + const posts = await context.databaseManager.getAllRecords('Post'); + + let archivedCount = 0; + for (const post of posts) { + if ((post.lastActivityAt || post.createdAt || 0) < cutoffDate && + (post.engagementScore || 0) < 5) { + post.isArchived = true; + await context.databaseManager.updateRecord('Post', post); + archivedCount++; + } + } + + context.logger.info(`Archived ${archivedCount} old posts`); + }) + .build(); + + const cacheOptimization = createMigration( + 'optimize_cache_fields', + '3.2.0', + 'Add cache-friendly computed fields' + ) + .description('Add denormalized fields to reduce query complexity') + .addField('User', 'postCount', { + type: 'number', + required: false, + default: 0 + }) + .addField('User', 'totalEngagement', { + type: 'number', + required: false, + default: 0 + }) + .addField('Post', 'commentCount', { + type: 'number', + required: false, + default: 0 + }) + .customOperation('User', async (context) => { + context.logger.info('Computing user statistics'); + + const users = await context.databaseManager.getAllRecords('User'); + + for (const user of users) { + const posts = await context.databaseManager.getRelatedRecords('Post', 'userId', user.id); + const totalEngagement = posts.reduce((sum: number, post: any) => + sum + (post.engagementScore || 0), 0); + + user.postCount = posts.length; + user.totalEngagement = totalEngagement; + + await context.databaseManager.updateRecord('User', user); + } + }) + .build(); + + // Register performance migrations + [indexOptimization, dataArchiving, cacheOptimization].forEach(migration => { + this.migrationManager.registerMigration(migration); + console.log(`✅ Registered: ${migration.name}`); + }); + + console.log('\nPerformance optimization migrations created:'); + console.log('- Search index optimization'); + console.log('- Data archiving for old content'); + console.log('- Cache-friendly denormalized fields'); + + console.log(''); + } + + async crossModelMigrationExamples(): Promise { + console.log('🔗 Cross-Model Migration Examples'); + console.log('=================================\n'); + + // Migration that affects multiple models and their relationships + const relationshipRestructure = createMigration( + 'restructure_follow_system', + '4.0.0', + 'Restructure follow system with categories' + ) + .description('Add follow categories and mutual follow detection') + .addField('Follow', 'category', { + type: 'string', + required: false, + default: 'general' + }) + .addField('Follow', 'isMutual', { + type: 'boolean', + required: false, + default: false + }) + .addField('Follow', 'strength', { + type: 'number', + required: false, + default: 1 + }) + .customOperation('Follow', async (context) => { + context.logger.info('Analyzing follow relationships'); + + const follows = await context.databaseManager.getAllRecords('Follow'); + const mutualMap = new Map>(); + + // Build mutual follow map + follows.forEach((follow: any) => { + if (!mutualMap.has(follow.followerId)) { + mutualMap.set(follow.followerId, new Set()); + } + mutualMap.get(follow.followerId)!.add(follow.followingId); + }); + + // Update mutual status + for (const follow of follows) { + const reverseExists = mutualMap.get(follow.followingId)?.has(follow.followerId); + follow.isMutual = Boolean(reverseExists); + + // Calculate relationship strength based on mutual status and activity + follow.strength = follow.isMutual ? 2 : 1; + + await context.databaseManager.updateRecord('Follow', follow); + } + }) + .build(); + + const contentCategorization = createMigration( + 'add_content_categories', + '4.1.0', + 'Add content categorization system' + ) + .description('Add categories and tags to posts and improve content discovery') + .addField('Post', 'category', { + type: 'string', + required: false, + default: 'general' + }) + .addField('Post', 'subcategory', { + type: 'string', + required: false + }) + .addField('Post', 'autoTags', { + type: 'array', + required: false, + default: [] + }) + .transformData('Post', (post) => { + // Auto-categorize posts based on content + const content = (post.content || '').toLowerCase(); + let category = 'general'; + let autoTags: string[] = []; + + if (content.includes('tech') || content.includes('programming')) { + category = 'technology'; + autoTags.push('tech'); + } else if (content.includes('art') || content.includes('design')) { + category = 'creative'; + autoTags.push('art'); + } else if (content.includes('news') || content.includes('update')) { + category = 'news'; + autoTags.push('news'); + } + + // Extract hashtags as auto tags + const hashtags = content.match(/#\w+/g) || []; + autoTags.push(...hashtags.map(tag => tag.slice(1))); + + return { + ...post, + category, + autoTags: [...new Set(autoTags)] // Remove duplicates + }; + }) + .build(); + + // Register cross-model migrations + [relationshipRestructure, contentCategorization].forEach(migration => { + this.migrationManager.registerMigration(migration); + console.log(`✅ Registered: ${migration.name}`); + }); + + console.log('\nCross-model migrations demonstrate:'); + console.log('- Complex relationship analysis and updates'); + console.log('- Multi-model data transformation'); + console.log('- Automatic content categorization'); + + console.log(''); + } + + async versionManagementExamples(): Promise { + console.log('📋 Version Management Examples'); + console.log('==============================\n'); + + // Demonstrate migration ordering and dependencies + const allMigrations = this.migrationManager.getMigrations(); + + console.log('Migration dependency chain:'); + allMigrations.forEach(migration => { + const deps = migration.dependencies?.join(', ') || 'None'; + console.log(`- ${migration.name} (v${migration.version}) depends on: ${deps}`); + }); + + // Show pending migrations in order + const pendingMigrations = this.migrationManager.getPendingMigrations(); + console.log(`\nPending migrations (${pendingMigrations.length}):`); + pendingMigrations.forEach((migration, index) => { + console.log(`${index + 1}. ${migration.name} (v${migration.version})`); + }); + + // Demonstrate batch migration with different strategies + console.log('\nRunning pending migrations with different strategies:'); + + if (pendingMigrations.length > 0) { + console.log('\n1. Dry run all pending migrations:'); + try { + const dryRunResults = await this.migrationManager.runPendingMigrations({ + dryRun: true, + stopOnError: false + }); + + console.log(`Dry run completed: ${dryRunResults.length} migrations processed`); + dryRunResults.forEach(result => { + console.log(`- ${result.migrationId}: ${result.success ? 'SUCCESS' : 'FAILED'}`); + }); + + } catch (error) { + console.error(`Dry run failed: ${error}`); + } + + console.log('\n2. Run migrations with stop-on-error:'); + try { + const results = await this.migrationManager.runPendingMigrations({ + stopOnError: true, + batchSize: 25 + }); + + console.log(`Migration batch completed: ${results.length} migrations`); + + } catch (error) { + console.error(`Migration batch stopped due to error: ${error}`); + } + } + + // Show migration history and statistics + const history = this.migrationManager.getMigrationHistory(); + console.log(`\nMigration history (${history.length} total runs):`); + + history.slice(0, 5).forEach(result => { + console.log(`- ${result.migrationId}: ${result.success ? 'SUCCESS' : 'FAILED'} ` + + `(${result.duration}ms, ${result.recordsProcessed} records)`); + }); + + // Show active migrations (should be empty in examples) + const activeMigrations = this.migrationManager.getActiveMigrations(); + console.log(`\nActive migrations: ${activeMigrations.length}`); + + console.log(''); + } + + async demonstrateAdvancedFeatures(): Promise { + console.log('🔬 Advanced Migration Features'); + console.log('==============================\n'); + + // Create a migration with complex validation + const complexValidation = createMigration( + 'complex_validation_example', + '5.0.0', + 'Migration with complex validation' + ) + .description('Demonstrates advanced validation and error handling') + .addValidator( + 'check_data_consistency', + 'Verify data consistency across models', + async (context) => { + const errors: string[] = []; + const warnings: string[] = []; + + // Simulate complex validation + const users = await context.databaseManager.getAllRecords('User'); + const posts = await context.databaseManager.getAllRecords('Post'); + + // Check for orphaned posts + const userIds = new Set(users.map((u: any) => u.id)); + const orphanedPosts = posts.filter((p: any) => !userIds.has(p.userId)); + + if (orphanedPosts.length > 0) { + warnings.push(`Found ${orphanedPosts.length} orphaned posts`); + } + + return { valid: errors.length === 0, errors, warnings }; + } + ) + .addField('User', 'validationField', { + type: 'string', + required: false, + default: 'validated' + }) + .build(); + + // Create a migration that handles large datasets + const largeMigration = createMigration( + 'large_dataset_migration', + '5.1.0', + 'Migration optimized for large datasets' + ) + .description('Demonstrates batch processing and progress tracking') + .customOperation('Post', async (context) => { + context.logger.info('Processing large dataset with progress tracking'); + + const totalRecords = 10000; // Simulate large dataset + const batchSize = 100; + + for (let i = 0; i < totalRecords; i += batchSize) { + const progress = ((i / totalRecords) * 100).toFixed(1); + context.logger.info(`Processing batch ${i / batchSize + 1}, Progress: ${progress}%`); + + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + context.progress.processedRecords = i + batchSize; + context.progress.estimatedTimeRemaining = + ((totalRecords - i) / batchSize) * 10; // Rough estimate + } + }) + .build(); + + console.log('Created advanced feature demonstrations:'); + console.log('- Complex multi-model validation'); + console.log('- Large dataset processing with progress tracking'); + console.log('- Error handling and recovery strategies'); + + console.log(''); + } +} + +// Usage function +export async function runMigrationExamples( + orbitDBService: any, + ipfsService: any +): Promise { + const framework = new SocialPlatformFramework(); + + try { + await framework.initialize(orbitDBService, ipfsService, 'development'); + + // Create sample data first + await createSampleDataForMigrations(framework); + + // Run migration examples + const examples = new MigrationExamples(framework); + await examples.runAllExamples(); + await examples.demonstrateAdvancedFeatures(); + + // Show final migration statistics + const migrationManager = (examples as any).migrationManager; + const allMigrations = migrationManager.getMigrations(); + const history = migrationManager.getMigrationHistory(); + + console.log('📊 Final Migration Statistics:'); + console.log('============================='); + console.log(`Total migrations registered: ${allMigrations.length}`); + console.log(`Total migration runs: ${history.length}`); + console.log(`Successful runs: ${history.filter((h: any) => h.success).length}`); + console.log(`Failed runs: ${history.filter((h: any) => !h.success).length}`); + + const totalDuration = history.reduce((sum: number, h: any) => sum + h.duration, 0); + console.log(`Total migration time: ${totalDuration}ms`); + + const totalRecords = history.reduce((sum: number, h: any) => sum + h.recordsProcessed, 0); + console.log(`Total records processed: ${totalRecords}`); + + } catch (error) { + console.error('❌ Migration examples failed:', error); + } finally { + await framework.stop(); + } +} + +async function createSampleDataForMigrations(framework: SocialPlatformFramework): Promise { + console.log('🗄️ Creating sample data for migration testing...\n'); + + try { + // Create users without timestamps (to demonstrate migration) + const users = []; + for (let i = 0; i < 5; i++) { + const user = await framework.createUser({ + username: `migrationuser${i}`, + email: `migration${i}@example.com`, + bio: `Migration test user ${i}` + }); + users.push(user); + } + + // Create posts with basic structure + const posts = []; + for (let i = 0; i < 10; i++) { + const user = users[i % users.length]; + const post = await framework.createPost(user.id, { + title: `Migration Test Post ${i}`, + content: `This is test content for migration testing. Post ${i} with various content types.`, + tags: ['migration', 'test'], + isPublic: true + }); + posts.push(post); + } + + // Create comments + for (let i = 0; i < 15; i++) { + const user = users[i % users.length]; + const post = posts[i % posts.length]; + await framework.createComment( + user.id, + post.id, + `Migration test comment ${i}` + ); + } + + // Create follow relationships + for (let i = 0; i < users.length; i++) { + for (let j = 0; j < users.length; j++) { + if (i !== j && Math.random() > 0.6) { + await framework.followUser(users[i].id, users[j].id); + } + } + } + + console.log(`✅ Created sample data: ${users.length} users, ${posts.length} posts, 15 comments\n`); + + } catch (error) { + console.warn('⚠️ Some sample data creation failed:', error); + } +} \ No newline at end of file diff --git a/examples/query-examples.ts b/examples/query-examples.ts new file mode 100644 index 0000000..1e14de8 --- /dev/null +++ b/examples/query-examples.ts @@ -0,0 +1,475 @@ +/** + * Comprehensive Query Examples for DebrosFramework + * + * This file demonstrates all the query capabilities implemented in Phase 3: + * - Basic and advanced filtering + * - User-scoped vs global queries + * - Relationship loading + * - Aggregations and analytics + * - Query optimization and caching + * - Pagination and chunked processing + */ + +import { SocialPlatformFramework, User, Post, Comment, Follow } from './framework-integration'; + +export class QueryExamples { + private framework: SocialPlatformFramework; + + constructor(framework: SocialPlatformFramework) { + this.framework = framework; + } + + async runAllExamples(): Promise { + console.log('🚀 Running comprehensive query examples...\n'); + + await this.basicQueries(); + await this.userScopedQueries(); + await this.relationshipQueries(); + await this.aggregationQueries(); + await this.advancedFiltering(); + await this.paginationExamples(); + await this.cacheExamples(); + await this.optimizationExamples(); + + console.log('✅ All query examples completed!\n'); + } + + async basicQueries(): Promise { + console.log('📊 Basic Query Examples'); + console.log('========================\n'); + + // Simple equality + const publicPosts = await Post + .where('isPublic', '=', true) + .limit(5) + .exec(); + console.log(`Found ${publicPosts.length} public posts`); + + // Multiple conditions + const recentPublicPosts = await Post + .where('isPublic', '=', true) + .where('createdAt', '>', Date.now() - 86400000) // Last 24 hours + .orderBy('createdAt', 'desc') + .limit(10) + .exec(); + console.log(`Found ${recentPublicPosts.length} recent public posts`); + + // Using whereIn + const specificUsers = await User + .whereIn('username', ['alice', 'bob', 'charlie']) + .exec(); + console.log(`Found ${specificUsers.length} specific users`); + + // Find by ID + if (publicPosts.length > 0) { + const singlePost = await Post.find(publicPosts[0].id); + console.log(`Found post: ${singlePost?.title || 'Not found'}`); + } + + console.log(''); + } + + async userScopedQueries(): Promise { + console.log('👤 User-Scoped Query Examples'); + console.log('==============================\n'); + + // Get all users first + const users = await User.limit(3).exec(); + if (users.length === 0) { + console.log('No users found for user-scoped examples'); + return; + } + + const userId = users[0].id; + + // Single user query (efficient - direct database access) + const userPosts = await Post + .whereUser(userId) + .orderBy('createdAt', 'desc') + .limit(10) + .exec(); + console.log(`Found ${userPosts.length} posts for user ${userId}`); + + // Multiple users query + const multiUserPosts = await Post + .whereUserIn(users.map(u => u.id)) + .where('isPublic', '=', true) + .limit(20) + .exec(); + console.log(`Found ${multiUserPosts.length} posts from ${users.length} users`); + + // Global query on user-scoped data (uses global index) + const allPublicPosts = await Post + .where('isPublic', '=', true) + .orderBy('createdAt', 'desc') + .limit(15) + .exec(); + console.log(`Found ${allPublicPosts.length} public posts across all users`); + + console.log(''); + } + + async relationshipQueries(): Promise { + console.log('🔗 Relationship Query Examples'); + console.log('===============================\n'); + + // Load posts with their authors + const postsWithAuthors = await Post + .where('isPublic', '=', true) + .load(['author']) + .limit(5) + .exec(); + console.log(`Loaded ${postsWithAuthors.length} posts with authors`); + + // Load posts with comments and authors + const postsWithComments = await Post + .where('isPublic', '=', true) + .load(['comments', 'author']) + .limit(3) + .exec(); + console.log(`Loaded ${postsWithComments.length} posts with comments and authors`); + + // Load user with their posts + const users = await User.limit(2).exec(); + if (users.length > 0) { + const userWithPosts = await User + .where('id', '=', users[0].id) + .load(['posts']) + .first(); + + if (userWithPosts) { + console.log(`User ${userWithPosts.username} has posts loaded`); + } + } + + console.log(''); + } + + async aggregationQueries(): Promise { + console.log('📈 Aggregation Query Examples'); + console.log('==============================\n'); + + // Count queries + const totalPosts = await Post.count(); + const publicPostCount = await Post.where('isPublic', '=', true).count(); + console.log(`Total posts: ${totalPosts}, Public: ${publicPostCount}`); + + // Sum and average + const totalLikes = await Post.sum('likeCount'); + const averageLikes = await Post.avg('likeCount'); + console.log(`Total likes: ${totalLikes}, Average: ${averageLikes.toFixed(2)}`); + + // Min and max + const oldestPost = await Post.min('createdAt'); + const newestPost = await Post.max('createdAt'); + console.log(`Oldest post: ${new Date(oldestPost).toISOString()}`); + console.log(`Newest post: ${new Date(newestPost).toISOString()}`); + + // User-specific aggregations + const users = await User.limit(1).exec(); + if (users.length > 0) { + const userId = users[0].id; + const userPostCount = await Post.whereUser(userId).count(); + const userTotalLikes = await Post.whereUser(userId).sum('likeCount'); + console.log(`User ${userId}: ${userPostCount} posts, ${userTotalLikes} total likes`); + } + + console.log(''); + } + + async advancedFiltering(): Promise { + console.log('🔍 Advanced Filtering Examples'); + console.log('===============================\n'); + + // Date filtering + const lastWeek = Date.now() - (7 * 24 * 60 * 60 * 1000); + const recentPosts = await Post + .whereDate('createdAt', '>', lastWeek) + .where('isPublic', '=', true) + .exec(); + console.log(`Found ${recentPosts.length} posts from last week`); + + // Range filtering + const popularPosts = await Post + .whereBetween('likeCount', 5, 100) + .where('isPublic', '=', true) + .orderBy('likeCount', 'desc') + .limit(10) + .exec(); + console.log(`Found ${popularPosts.length} moderately popular posts`); + + // Array filtering + const techPosts = await Post + .whereArrayContains('tags', 'tech') + .where('isPublic', '=', true) + .exec(); + console.log(`Found ${techPosts.length} tech-related posts`); + + // Text search + const searchResults = await Post + .where('isPublic', '=', true) + .orWhere(query => { + query.whereLike('title', 'framework') + .whereLike('content', 'orbitdb'); + }) + .limit(10) + .exec(); + console.log(`Found ${searchResults.length} posts matching search terms`); + + // Null checks + const postsWithBio = await User + .whereNotNull('bio') + .limit(5) + .exec(); + console.log(`Found ${postsWithBio.length} users with bios`); + + console.log(''); + } + + async paginationExamples(): Promise { + console.log('📄 Pagination Examples'); + console.log('=======================\n'); + + // Basic pagination + const page1 = await Post + .where('isPublic', '=', true) + .orderBy('createdAt', 'desc') + .page(1, 5) + .exec(); + console.log(`Page 1: ${page1.length} posts`); + + // Pagination with metadata + const paginatedResult = await Post + .where('isPublic', '=', true) + .orderBy('createdAt', 'desc') + .paginate(1, 5); + + console.log(`Pagination: ${paginatedResult.currentPage}/${paginatedResult.lastPage}`); + console.log(`Total: ${paginatedResult.total}, Per page: ${paginatedResult.perPage}`); + console.log(`Has next: ${paginatedResult.hasNextPage}, Has prev: ${paginatedResult.hasPrevPage}`); + + // Chunked processing + let processedCount = 0; + await Post + .where('isPublic', '=', true) + .chunk(3, async (posts, page) => { + processedCount += posts.length; + console.log(`Processed chunk ${page}: ${posts.length} posts`); + + // Stop after processing 2 chunks for demo + if (page >= 2) return false; + }); + console.log(`Total processed in chunks: ${processedCount}`); + + console.log(''); + } + + async cacheExamples(): Promise { + console.log('⚡ Cache Examples'); + console.log('=================\n'); + + // First execution (cache miss) + console.log('First query execution (cache miss):'); + const start1 = Date.now(); + const posts1 = await Post + .where('isPublic', '=', true) + .orderBy('createdAt', 'desc') + .limit(10) + .exec(); + const duration1 = Date.now() - start1; + console.log(`Returned ${posts1.length} posts in ${duration1}ms`); + + // Second execution (cache hit) + console.log('Second query execution (cache hit):'); + const start2 = Date.now(); + const posts2 = await Post + .where('isPublic', '=', true) + .orderBy('createdAt', 'desc') + .limit(10) + .exec(); + const duration2 = Date.now() - start2; + console.log(`Returned ${posts2.length} posts in ${duration2}ms`); + + // Cache statistics + const stats = await this.framework.getFrameworkStats(); + console.log('Cache statistics:', stats.cache.stats); + + console.log(''); + } + + async optimizationExamples(): Promise { + console.log('🚀 Query Optimization Examples'); + console.log('===============================\n'); + + // Query explanation + const query = Post + .where('isPublic', '=', true) + .where('likeCount', '>', 10) + .orderBy('createdAt', 'desc') + .limit(20); + + const explanation = await this.framework.explainQuery(query); + console.log('Query explanation:'); + console.log('- Strategy:', explanation.plan.strategy); + console.log('- Estimated cost:', explanation.plan.estimatedCost); + console.log('- Optimizations:', explanation.plan.optimizations); + console.log('- Suggestions:', explanation.suggestions); + + // Query with index hint + const optimizedQuery = Post + .where('isPublic', '=', true) + .useIndex('post_public_idx') + .orderBy('createdAt', 'desc') + .limit(10); + + const optimizedResults = await optimizedQuery.exec(); + console.log(`Optimized query returned ${optimizedResults.length} results`); + + // Disable cache for specific query + const nonCachedQuery = Post + .where('isPublic', '=', true) + .limit(5); + + // Note: This would work with QueryExecutor integration + // const nonCachedResults = await nonCachedQuery.exec().disableCache(); + + console.log(''); + } + + async demonstrateQueryBuilder(): Promise { + console.log('🔧 QueryBuilder Method Demonstration'); + console.log('=====================================\n'); + + // Show various QueryBuilder methods + const complexQuery = Post + .where('isPublic', '=', true) + .whereNotNull('title') + .whereDateBetween('createdAt', Date.now() - 86400000 * 7, Date.now()) + .whereArrayLength('tags', '>', 0) + .orderByMultiple([ + { field: 'likeCount', direction: 'desc' }, + { field: 'createdAt', direction: 'desc' } + ]) + .distinct('userId') + .limit(15); + + console.log('Complex query SQL representation:'); + console.log(complexQuery.toSQL()); + + console.log('\nQuery explanation:'); + console.log(complexQuery.explain()); + + // Clone and modify query + const modifiedQuery = complexQuery.clone() + .where('likeCount', '>', 5) + .limit(10); + + console.log('\nModified query SQL:'); + console.log(modifiedQuery.toSQL()); + + const results = await modifiedQuery.exec(); + console.log(`\nExecuted complex query, got ${results.length} results`); + + console.log(''); + } +} + +// Usage example +export async function runQueryExamples( + orbitDBService: any, + ipfsService: any +): Promise { + const framework = new SocialPlatformFramework(); + + try { + await framework.initialize(orbitDBService, ipfsService, 'development'); + + // Create some sample data if needed + await createSampleData(framework); + + // Run query examples + const examples = new QueryExamples(framework); + await examples.runAllExamples(); + await examples.demonstrateQueryBuilder(); + + // Show final framework stats + const stats = await framework.getFrameworkStats(); + console.log('📊 Final Framework Statistics:'); + console.log(JSON.stringify(stats, null, 2)); + + } catch (error) { + console.error('❌ Query examples failed:', error); + } finally { + await framework.stop(); + } +} + +async function createSampleData(framework: SocialPlatformFramework): Promise { + console.log('🗄️ Creating sample data for query examples...\n'); + + try { + // Create users + const alice = await framework.createUser({ + username: 'alice', + email: 'alice@example.com', + bio: 'Tech enthusiast and framework developer' + }); + + const bob = await framework.createUser({ + username: 'bob', + email: 'bob@example.com', + bio: 'Building decentralized applications' + }); + + const charlie = await framework.createUser({ + username: 'charlie', + email: 'charlie@example.com' + }); + + // Create posts + await framework.createPost(alice.id, { + title: 'Introduction to DebrosFramework', + content: 'The DebrosFramework makes OrbitDB development much easier...', + tags: ['framework', 'orbitdb', 'tech'], + isPublic: true + }); + + await framework.createPost(alice.id, { + title: 'Advanced Query Patterns', + content: 'Here are some advanced patterns for querying decentralized data...', + tags: ['queries', 'patterns', 'tech'], + isPublic: true + }); + + await framework.createPost(bob.id, { + title: 'Building Scalable dApps', + content: 'Scalability is crucial for decentralized applications...', + tags: ['scalability', 'dapps'], + isPublic: true + }); + + await framework.createPost(bob.id, { + title: 'Private Development Notes', + content: 'Some private thoughts on the framework architecture...', + tags: ['private', 'notes'], + isPublic: false + }); + + await framework.createPost(charlie.id, { + title: 'Getting Started Guide', + content: 'A comprehensive guide to getting started with the framework...', + tags: ['guide', 'beginner'], + isPublic: true + }); + + // Create some follows + await framework.followUser(alice.id, bob.id); + await framework.followUser(bob.id, charlie.id); + await framework.followUser(charlie.id, alice.id); + + console.log('✅ Sample data created successfully!\n'); + + } catch (error) { + console.warn('⚠️ Some sample data creation failed:', error); + } +} \ No newline at end of file diff --git a/examples/relationship-examples.ts b/examples/relationship-examples.ts new file mode 100644 index 0000000..c283f0b --- /dev/null +++ b/examples/relationship-examples.ts @@ -0,0 +1,511 @@ +/** + * Comprehensive Relationship Examples for DebrosFramework + * + * This file demonstrates all the relationship loading capabilities implemented in Phase 4: + * - Lazy and eager loading + * - Relationship caching + * - Cross-database relationship resolution + * - Advanced loading with constraints + * - Performance optimization techniques + */ + +import { SocialPlatformFramework, User, Post, Comment, Follow } from './framework-integration'; + +export class RelationshipExamples { + private framework: SocialPlatformFramework; + + constructor(framework: SocialPlatformFramework) { + this.framework = framework; + } + + async runAllExamples(): Promise { + console.log('🔗 Running comprehensive relationship examples...\n'); + + await this.basicRelationshipLoading(); + await this.eagerLoadingExamples(); + await this.lazyLoadingExamples(); + await this.constrainedLoadingExamples(); + await this.cacheOptimizationExamples(); + await this.crossDatabaseRelationships(); + await this.performanceExamples(); + + console.log('✅ All relationship examples completed!\n'); + } + + async basicRelationshipLoading(): Promise { + console.log('🔗 Basic Relationship Loading'); + console.log('==============================\n'); + + // Get a post and load its author (BelongsTo) + const posts = await Post.where('isPublic', '=', true).limit(3).exec(); + + if (posts.length > 0) { + const post = posts[0]; + console.log(`Loading author for post: ${post.title}`); + + const author = await post.loadRelation('author'); + console.log(`Author loaded: ${author?.username || 'Unknown'}`); + + // Load comments for the post (HasMany) + console.log(`Loading comments for post: ${post.title}`); + const comments = await post.loadRelation('comments'); + console.log(`Comments loaded: ${Array.isArray(comments) ? comments.length : 0} comment(s)`); + + // Check what relationships are loaded + console.log(`Loaded relationships: ${post.getLoadedRelations().join(', ')}`); + } + + // Get a user and load their posts (HasMany) + const users = await User.limit(2).exec(); + if (users.length > 0) { + const user = users[0]; + console.log(`\nLoading posts for user: ${user.username}`); + + const userPosts = await user.loadRelation('posts'); + console.log(`Posts loaded: ${Array.isArray(userPosts) ? userPosts.length : 0} post(s)`); + } + + console.log(''); + } + + async eagerLoadingExamples(): Promise { + console.log('⚡ Eager Loading Examples'); + console.log('==========================\n'); + + // Load multiple posts with their authors and comments in one go + console.log('Loading posts with authors and comments (eager loading):'); + const posts = await Post + .where('isPublic', '=', true) + .limit(5) + .exec(); + + if (posts.length > 0) { + // Eager load relationships for all posts at once + const startTime = Date.now(); + await posts[0].load(['author', 'comments']); + const singleLoadTime = Date.now() - startTime; + + // Now eager load for all posts + const eagerStartTime = Date.now(); + await this.framework.relationshipManager.eagerLoadRelationships( + posts, + ['author', 'comments'] + ); + const eagerLoadTime = Date.now() - eagerStartTime; + + console.log(`Single post relationship loading: ${singleLoadTime}ms`); + console.log(`Eager loading for ${posts.length} posts: ${eagerLoadTime}ms`); + console.log(`Efficiency gain: ${((singleLoadTime * posts.length) / eagerLoadTime).toFixed(2)}x faster`); + + // Verify relationships are loaded + let loadedCount = 0; + for (const post of posts) { + if (post.isRelationLoaded('author') && post.isRelationLoaded('comments')) { + loadedCount++; + } + } + console.log(`Successfully loaded relationships for ${loadedCount}/${posts.length} posts`); + } + + // Load users with their posts + console.log('\nLoading users with their posts (eager loading):'); + const users = await User.limit(3).exec(); + + if (users.length > 0) { + await this.framework.relationshipManager.eagerLoadRelationships( + users, + ['posts', 'following'] + ); + + for (const user of users) { + const posts = user.getRelation('posts') || []; + const following = user.getRelation('following') || []; + console.log(`User ${user.username}: ${posts.length} posts, ${following.length} following`); + } + } + + console.log(''); + } + + async lazyLoadingExamples(): Promise { + console.log('💤 Lazy Loading Examples'); + console.log('=========================\n'); + + const posts = await Post.where('isPublic', '=', true).limit(2).exec(); + + if (posts.length > 0) { + const post = posts[0]; + + console.log('Demonstrating lazy loading behavior:'); + console.log(`Post title: ${post.title}`); + console.log(`Author loaded initially: ${post.isRelationLoaded('author')}`); + + // First access triggers loading + console.log('Accessing author (triggers lazy loading)...'); + const author = await post.loadRelation('author'); + console.log(`Author: ${author?.username || 'Unknown'}`); + console.log(`Author loaded after access: ${post.isRelationLoaded('author')}`); + + // Second access uses cached value + console.log('Accessing author again (uses cache)...'); + const authorAgain = post.getRelation('author'); + console.log(`Author (cached): ${authorAgain?.username || 'Unknown'}`); + + // Reload relationship (clears cache and reloads) + console.log('Reloading author relationship...'); + const reloadedAuthor = await post.reloadRelation('author'); + console.log(`Reloaded author: ${reloadedAuthor?.username || 'Unknown'}`); + } + + console.log(''); + } + + async constrainedLoadingExamples(): Promise { + console.log('🎯 Constrained Loading Examples'); + console.log('=================================\n'); + + const posts = await Post.where('isPublic', '=', true).limit(3).exec(); + + if (posts.length > 0) { + const post = posts[0]; + + // Load only recent comments + console.log(`Loading recent comments for post: ${post.title}`); + const recentComments = await post.loadRelationWithConstraints('comments', (query) => + query.where('createdAt', '>', Date.now() - 86400000) // Last 24 hours + .orderBy('createdAt', 'desc') + .limit(5) + ); + console.log(`Recent comments loaded: ${Array.isArray(recentComments) ? recentComments.length : 0}`); + + // Load comments with minimum length + console.log(`Loading substantive comments (>50 chars):`); + const substantiveComments = await post.loadRelationWithConstraints('comments', (query) => + query.whereRaw('LENGTH(content) > ?', [50]) + .orderBy('createdAt', 'desc') + .limit(3) + ); + console.log(`Substantive comments: ${Array.isArray(substantiveComments) ? substantiveComments.length : 0}`); + } + + // Load user posts with constraints + const users = await User.limit(2).exec(); + if (users.length > 0) { + const user = users[0]; + + console.log(`\nLoading popular posts for user: ${user.username}`); + const popularPosts = await user.loadRelationWithConstraints('posts', (query) => + query.where('likeCount', '>', 5) + .where('isPublic', '=', true) + .orderBy('likeCount', 'desc') + .limit(10) + ); + console.log(`Popular posts: ${Array.isArray(popularPosts) ? popularPosts.length : 0}`); + } + + console.log(''); + } + + async cacheOptimizationExamples(): Promise { + console.log('🚀 Cache Optimization Examples'); + console.log('===============================\n'); + + // Get cache stats before + const statsBefore = this.framework.relationshipManager.getRelationshipCacheStats(); + console.log('Relationship cache stats before:'); + console.log(`- Total entries: ${statsBefore.cache.totalEntries}`); + console.log(`- Hit rate: ${(statsBefore.cache.hitRate * 100).toFixed(2)}%`); + + // Load relationships multiple times to demonstrate caching + const posts = await Post.where('isPublic', '=', true).limit(3).exec(); + + if (posts.length > 0) { + console.log('\nLoading relationships multiple times (should hit cache):'); + + for (let i = 0; i < 3; i++) { + const startTime = Date.now(); + await posts[0].loadRelation('author'); + await posts[0].loadRelation('comments'); + const duration = Date.now() - startTime; + console.log(`Iteration ${i + 1}: ${duration}ms`); + } + } + + // Warm up cache + console.log('\nWarming up relationship cache:'); + const allPosts = await Post.limit(5).exec(); + const allUsers = await User.limit(3).exec(); + + await this.framework.relationshipManager.warmupRelationshipCache( + allPosts, + ['author', 'comments'] + ); + + await this.framework.relationshipManager.warmupRelationshipCache( + allUsers, + ['posts', 'following'] + ); + + // Get cache stats after + const statsAfter = this.framework.relationshipManager.getRelationshipCacheStats(); + console.log('\nRelationship cache stats after warmup:'); + console.log(`- Total entries: ${statsAfter.cache.totalEntries}`); + console.log(`- Hit rate: ${(statsAfter.cache.hitRate * 100).toFixed(2)}%`); + console.log(`- Memory usage: ${(statsAfter.cache.memoryUsage / 1024).toFixed(2)} KB`); + + // Show cache performance analysis + const performance = statsAfter.performance; + console.log('\nCache performance analysis:'); + console.log(`- Average age: ${(performance.averageAge / 1000).toFixed(2)} seconds`); + console.log(`- Relationship types in cache:`); + performance.relationshipTypes.forEach((count, type) => { + console.log(` * ${type}: ${count} entries`); + }); + + console.log(''); + } + + async crossDatabaseRelationships(): Promise { + console.log('🌐 Cross-Database Relationship Examples'); + console.log('=========================================\n'); + + // This demonstrates relationships that span across user databases and global databases + + // Get users (stored in global database) + const users = await User.limit(2).exec(); + + if (users.length >= 2) { + const user1 = users[0]; + const user2 = users[1]; + + console.log(`Loading cross-database relationships:`); + console.log(`User 1: ${user1.username} (global DB)`); + console.log(`User 2: ${user2.username} (global DB)`); + + // Load posts for user1 (stored in user1's database) + const user1Posts = await user1.loadRelation('posts'); + console.log(`User 1 posts (from user DB): ${Array.isArray(user1Posts) ? user1Posts.length : 0}`); + + // Load posts for user2 (stored in user2's database) + const user2Posts = await user2.loadRelation('posts'); + console.log(`User 2 posts (from user DB): ${Array.isArray(user2Posts) ? user2Posts.length : 0}`); + + // Load followers relationship (stored in global database) + const user1Following = await user1.loadRelation('following'); + console.log(`User 1 following (from global DB): ${Array.isArray(user1Following) ? user1Following.length : 0}`); + + // Demonstrate the complexity: Post (user DB) -> Author (global DB) -> Posts (back to user DB) + if (Array.isArray(user1Posts) && user1Posts.length > 0) { + const post = user1Posts[0]; + console.log(`\nDemonstrating complex cross-DB relationship chain:`); + console.log(`Post: "${post.title}" (from user DB)`); + + const author = await post.loadRelation('author'); + console.log(`-> Author: ${author?.username || 'Unknown'} (from global DB)`); + + if (author) { + const authorPosts = await author.loadRelation('posts'); + console.log(`-> Author's posts: ${Array.isArray(authorPosts) ? authorPosts.length : 0} (back to user DB)`); + } + } + } + + console.log(''); + } + + async performanceExamples(): Promise { + console.log('📈 Performance Examples'); + console.log('========================\n'); + + // Compare different loading strategies + const posts = await Post.where('isPublic', '=', true).limit(10).exec(); + + if (posts.length > 0) { + console.log(`Performance comparison for ${posts.length} posts:\n`); + + // Strategy 1: Sequential loading (N+1 problem) + console.log('1. Sequential loading (N+1 queries):'); + const sequentialStart = Date.now(); + for (const post of posts) { + await post.loadRelation('author'); + } + const sequentialTime = Date.now() - sequentialStart; + console.log(` Time: ${sequentialTime}ms (${(sequentialTime / posts.length).toFixed(2)}ms per post)`); + + // Clear loaded relationships for fair comparison + posts.forEach(post => { + post._loadedRelations.clear(); + }); + + // Strategy 2: Eager loading (optimal) + console.log('\n2. Eager loading (optimized):'); + const eagerStart = Date.now(); + await this.framework.relationshipManager.eagerLoadRelationships(posts, ['author']); + const eagerTime = Date.now() - eagerStart; + console.log(` Time: ${eagerTime}ms (${(eagerTime / posts.length).toFixed(2)}ms per post)`); + console.log(` Performance improvement: ${(sequentialTime / eagerTime).toFixed(2)}x faster`); + + // Strategy 3: Cached loading (fastest for repeated access) + console.log('\n3. Cached loading (repeated access):'); + const cachedStart = Date.now(); + await this.framework.relationshipManager.eagerLoadRelationships(posts, ['author']); + const cachedTime = Date.now() - cachedStart; + console.log(` Time: ${cachedTime}ms (cache hit)`); + console.log(` Cache efficiency: ${(eagerTime / Math.max(cachedTime, 1)).toFixed(2)}x faster than first load`); + } + + // Memory usage demonstration + console.log('\nMemory usage analysis:'); + const memoryStats = this.framework.relationshipManager.getRelationshipCacheStats(); + console.log(`- Cache entries: ${memoryStats.cache.totalEntries}`); + console.log(`- Memory usage: ${(memoryStats.cache.memoryUsage / 1024).toFixed(2)} KB`); + console.log(`- Average per entry: ${memoryStats.cache.totalEntries > 0 ? (memoryStats.cache.memoryUsage / memoryStats.cache.totalEntries).toFixed(2) : 0} bytes`); + + // Cache cleanup demonstration + console.log('\nCache cleanup:'); + const expiredCount = this.framework.relationshipManager.cleanupExpiredCache(); + console.log(`- Cleaned up ${expiredCount} expired entries`); + + // Model-based invalidation + const invalidatedCount = this.framework.relationshipManager.invalidateModelCache('User'); + console.log(`- Invalidated ${invalidatedCount} User-related cache entries`); + + console.log(''); + } + + async demonstrateAdvancedFeatures(): Promise { + console.log('🔬 Advanced Relationship Features'); + console.log('==================================\n'); + + const posts = await Post.where('isPublic', '=', true).limit(3).exec(); + + if (posts.length > 0) { + const post = posts[0]; + + // Demonstrate conditional loading + console.log('Conditional relationship loading:'); + if (!post.isRelationLoaded('author')) { + console.log('- Author not loaded, loading now...'); + await post.loadRelation('author'); + } else { + console.log('- Author already loaded, using cached version'); + } + + // Demonstrate partial loading with pagination + console.log('\nPaginated relationship loading:'); + const page1Comments = await post.loadRelationWithConstraints('comments', (query) => + query.orderBy('createdAt', 'desc').limit(5).offset(0) + ); + console.log(`- Page 1: ${Array.isArray(page1Comments) ? page1Comments.length : 0} comments`); + + const page2Comments = await post.loadRelationWithConstraints('comments', (query) => + query.orderBy('createdAt', 'desc').limit(5).offset(5) + ); + console.log(`- Page 2: ${Array.isArray(page2Comments) ? page2Comments.length : 0} comments`); + + // Demonstrate relationship statistics + console.log('\nRelationship loading statistics:'); + const modelClass = post.constructor as any; + const relationships = Array.from(modelClass.relationships?.keys() || []); + console.log(`- Available relationships: ${relationships.join(', ')}`); + console.log(`- Currently loaded: ${post.getLoadedRelations().join(', ')}`); + } + + console.log(''); + } +} + +// Usage example +export async function runRelationshipExamples( + orbitDBService: any, + ipfsService: any +): Promise { + const framework = new SocialPlatformFramework(); + + try { + await framework.initialize(orbitDBService, ipfsService, 'development'); + + // Ensure we have sample data + await createSampleDataForRelationships(framework); + + // Run relationship examples + const examples = new RelationshipExamples(framework); + await examples.runAllExamples(); + await examples.demonstrateAdvancedFeatures(); + + // Show final relationship cache statistics + const finalStats = framework.relationshipManager.getRelationshipCacheStats(); + console.log('📊 Final Relationship Cache Statistics:'); + console.log(JSON.stringify(finalStats, null, 2)); + + } catch (error) { + console.error('❌ Relationship examples failed:', error); + } finally { + await framework.stop(); + } +} + +async function createSampleDataForRelationships(framework: SocialPlatformFramework): Promise { + console.log('🗄️ Creating sample data for relationship examples...\n'); + + try { + // Create users + const alice = await framework.createUser({ + username: 'alice', + email: 'alice@example.com', + bio: 'Framework developer and relationship expert' + }); + + const bob = await framework.createUser({ + username: 'bob', + email: 'bob@example.com', + bio: 'Database architect' + }); + + const charlie = await framework.createUser({ + username: 'charlie', + email: 'charlie@example.com', + bio: 'Performance optimization specialist' + }); + + // Create posts with relationships + const post1 = await framework.createPost(alice.id, { + title: 'Understanding Relationships in Distributed Databases', + content: 'Relationships across distributed databases present unique challenges...', + tags: ['relationships', 'distributed', 'databases'], + isPublic: true + }); + + const post2 = await framework.createPost(bob.id, { + title: 'Optimizing Cross-Database Queries', + content: 'When data spans multiple databases, query optimization becomes crucial...', + tags: ['optimization', 'queries', 'performance'], + isPublic: true + }); + + const post3 = await framework.createPost(alice.id, { + title: 'Caching Strategies for Relationships', + content: 'Effective caching can dramatically improve relationship loading performance...', + tags: ['caching', 'performance', 'relationships'], + isPublic: true + }); + + // Create comments to establish relationships + await framework.createComment(bob.id, post1.id, 'Great explanation of the distributed relationship challenges!'); + await framework.createComment(charlie.id, post1.id, 'This helped me understand the complexity involved.'); + await framework.createComment(alice.id, post2.id, 'Excellent optimization techniques, Bob!'); + await framework.createComment(charlie.id, post2.id, 'These optimizations improved our app performance by 3x.'); + await framework.createComment(bob.id, post3.id, 'Caching relationships was a game-changer for our system.'); + + // Create follow relationships + await framework.followUser(alice.id, bob.id); + await framework.followUser(bob.id, charlie.id); + await framework.followUser(charlie.id, alice.id); + await framework.followUser(alice.id, charlie.id); + + console.log('✅ Sample relationship data created successfully!\n'); + + } catch (error) { + console.warn('⚠️ Some sample data creation failed:', error); + } +} \ No newline at end of file diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 0000000..f36bf1e --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,19 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/tests'], + testMatch: ['**/unit/**/*.test.ts'], + setupFilesAfterEnv: ['/tests/setup.ts'], + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + isolatedModules: true, + }, + ], + }, + collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/index.ts'], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testTimeout: 30000 +}; diff --git a/package.json b/package.json index 83e4943..b4b84cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@debros/network", - "version": "0.0.24-alpha", + "version": "0.5.0-beta", "description": "Debros network core functionality for IPFS, libp2p and OrbitDB", "type": "module", "main": "dist/index.js", @@ -18,7 +18,10 @@ "prepare": "husky", "lint": "npx eslint src", "format": "prettier --write \"**/*.{ts,js,json,md}\"", - "lint:fix": "npx eslint src --fix" + "lint:fix": "npx eslint src --fix", + "test:unit": "jest tests/unit", + "test:blog-integration": "tsx tests/real-integration/blog-scenario/scenarios/BlogTestRunner.ts", + "test:real": "docker-compose -f tests/real-integration/blog-scenario/docker/docker-compose.blog.yml up --build --abort-on-container-exit" }, "keywords": [ "ipfs", @@ -47,6 +50,7 @@ "@orbitdb/core": "^2.5.0", "@orbitdb/feed-db": "^1.1.2", "blockstore-fs": "^2.0.2", + "datastore-fs": "^10.0.4", "express": "^5.1.0", "helia": "^5.3.0", "libp2p": "^2.8.2", @@ -58,11 +62,20 @@ "peerDependencies": { "typescript": ">=5.0.0" }, + "pnpm": { + "onlyBuiltDependencies": [ + "@ipshipyard/node-datachannel", + "classic-level" + ] + }, "devDependencies": { "@eslint/js": "^9.24.0", + "@jest/globals": "^30.0.1", "@orbitdb/core-types": "^1.0.14", "@types/express": "^5.0.1", + "@types/jest": "^30.0.0", "@types/node": "^22.13.10", + "@types/node-fetch": "^2.6.7", "@types/node-forge": "^1.3.11", "@typescript-eslint/eslint-plugin": "^8.29.0", "@typescript-eslint/parser": "^8.29.0", @@ -71,20 +84,15 @@ "eslint-plugin-prettier": "^5.2.6", "globals": "^16.0.0", "husky": "^8.0.3", + "jest": "^30.0.1", "lint-staged": "^15.5.0", + "node-fetch": "^2.7.0", "prettier": "^3.5.3", "rimraf": "^5.0.5", + "ts-jest": "^29.4.0", "tsc-esm-fix": "^3.1.2", + "tsx": "^4.20.3", "typescript": "^5.8.2", "typescript-eslint": "^8.29.0" - }, - "compilerOptions": { - "typeRoots": [ - "./node_modules/@types", - "./node_modules/@constl/orbit-db-types" - ], - "types": [ - "@constl/orbit-db-types" - ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5e96da..a004fb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ settings: excludeLinksFromLockfile: false importers: + .: dependencies: '@chainsafe/libp2p-gossipsub': @@ -51,16 +52,19 @@ importers: version: 2.5.0 '@orbitdb/feed-db': specifier: ^1.1.2 - version: 1.1.2(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0)) + version: 1.1.2(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0)) blockstore-fs: specifier: ^2.0.2 version: 2.0.2 + datastore-fs: + specifier: ^10.0.4 + version: 10.0.4 express: specifier: ^5.1.0 version: 5.1.0 helia: specifier: ^5.3.0 - version: 5.3.0(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0)) + version: 5.3.0(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0)) libp2p: specifier: ^2.8.2 version: 2.8.2 @@ -80,15 +84,24 @@ importers: '@eslint/js': specifier: ^9.24.0 version: 9.24.0 + '@jest/globals': + specifier: ^30.0.1 + version: 30.0.1 '@orbitdb/core-types': specifier: ^1.0.14 version: 1.0.14 '@types/express': specifier: ^5.0.1 version: 5.0.1 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 '@types/node': specifier: ^22.13.10 version: 22.13.16 + '@types/node-fetch': + specifier: ^2.6.7 + version: 2.6.12 '@types/node-forge': specifier: ^1.3.11 version: 1.3.11 @@ -113,18 +126,30 @@ importers: husky: specifier: ^8.0.3 version: 8.0.3 + jest: + specifier: ^30.0.1 + version: 30.0.1(@types/node@22.13.16) lint-staged: specifier: ^15.5.0 version: 15.5.0 + node-fetch: + specifier: ^2.7.0 + version: 2.7.0 prettier: specifier: ^3.5.3 version: 3.5.3 rimraf: specifier: ^5.0.5 version: 5.0.10 + ts-jest: + specifier: ^29.4.0 + version: 29.4.0(@babel/core@7.27.4)(@jest/transform@30.0.1)(@jest/types@30.0.1)(babel-jest@30.0.1(@babel/core@7.27.4))(jest-util@30.0.1)(jest@30.0.1(@types/node@22.13.16))(typescript@5.8.2) tsc-esm-fix: specifier: ^3.1.2 version: 3.1.2 + tsx: + specifier: ^4.20.3 + version: 4.20.3 typescript: specifier: ^5.8.2 version: 5.8.2 @@ -133,1808 +158,1404 @@ importers: version: 8.29.0(eslint@9.24.0)(typescript@5.8.2) packages: + '@achingbrain/http-parser-js@0.5.8': - resolution: - { - integrity: sha512-7P8ukzL4jyh8Fho5tSfPBTzWJUZ0D7DxaW7ObObT5HTcljhjq9NN/qFg1yzPxq0VWRI/8qPnSUa1ofzzH/R9eQ==, - } + resolution: {integrity: sha512-7P8ukzL4jyh8Fho5tSfPBTzWJUZ0D7DxaW7ObObT5HTcljhjq9NN/qFg1yzPxq0VWRI/8qPnSUa1ofzzH/R9eQ==} '@achingbrain/nat-port-mapper@4.0.2': - resolution: - { - integrity: sha512-cOV/mPL8ouLko487f37LXl6t76NwksLbyib2Y2T72HK2bm7y2QP0+3+1xxqKqaffoo30CJm8E8IHN9hmJ/LiSA==, - } + resolution: {integrity: sha512-cOV/mPL8ouLko487f37LXl6t76NwksLbyib2Y2T72HK2bm7y2QP0+3+1xxqKqaffoo30CJm8E8IHN9hmJ/LiSA==} '@achingbrain/ssdp@4.2.1': - resolution: - { - integrity: sha512-haY46oYyQWlM3qElCpQ1M5I5pVbPPJ5p3n3gYuMtZsDezT5mQ4e4PuqiIzhfmrURj+WKbSppgNRuczN0S+Xt1Q==, - } + resolution: {integrity: sha512-haY46oYyQWlM3qElCpQ1M5I5pVbPPJ5p3n3gYuMtZsDezT5mQ4e4PuqiIzhfmrURj+WKbSppgNRuczN0S+Xt1Q==} '@ampproject/remapping@2.3.0': - resolution: - { - integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} '@assemblyscript/loader@0.9.4': - resolution: - { - integrity: sha512-HazVq9zwTVwGmqdwYzu7WyQ6FQVZ7SwET0KKQuKm55jD0IfUpZgN0OPIiZG3zV1iSrVYcN0bdwLRXI/VNCYsUA==, - } + resolution: {integrity: sha512-HazVq9zwTVwGmqdwYzu7WyQ6FQVZ7SwET0KKQuKm55jD0IfUpZgN0OPIiZG3zV1iSrVYcN0bdwLRXI/VNCYsUA==} '@babel/code-frame@7.26.2': - resolution: - { - integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} '@babel/compat-data@7.26.8': - resolution: - { - integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.27.5': + resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} + engines: {node: '>=6.9.0'} '@babel/core@7.26.10': - resolution: - { - integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.27.4': + resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + engines: {node: '>=6.9.0'} '@babel/generator@7.27.0': - resolution: - { - integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.5': + resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} + engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.25.9': - resolution: - { - integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.0': - resolution: - { - integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} '@babel/helper-create-class-features-plugin@7.27.0': - resolution: - { - integrity: sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-create-regexp-features-plugin@7.27.0': - resolution: - { - integrity: sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-define-polyfill-provider@0.6.4': - resolution: - { - integrity: sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==, - } + resolution: {integrity: sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 '@babel/helper-member-expression-to-functions@7.25.9': - resolution: - { - integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.25.9': - resolution: - { - integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} '@babel/helper-module-transforms@7.26.0': - resolution: - { - integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-optimise-call-expression@7.25.9': - resolution: - { - integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} '@babel/helper-plugin-utils@7.26.5': - resolution: - { - integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} '@babel/helper-remap-async-to-generator@7.25.9': - resolution: - { - integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-replace-supers@7.26.5': - resolution: - { - integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-skip-transparent-expression-wrappers@7.25.9': - resolution: - { - integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.25.9': - resolution: - { - integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.25.9': - resolution: - { - integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.25.9': - resolution: - { - integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} '@babel/helper-wrap-function@7.25.9': - resolution: - { - integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} '@babel/helpers@7.27.0': - resolution: - { - integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} '@babel/parser@7.27.0': - resolution: - { - integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.27.5': + resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} + engines: {node: '>=6.0.0'} hasBin: true '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': - resolution: - { - integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': - resolution: - { - integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': - resolution: - { - integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': - resolution: - { - integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': - resolution: - { - integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-proposal-export-default-from@7.25.9': - resolution: - { - integrity: sha512-ykqgwNfSnNOB+C8fV5X4mG3AVmvu+WVxcaU9xHHtBb7PCrPeweMmPjGsn8eMaeJg6SJuoUuZENeeSWaarWqonQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-ykqgwNfSnNOB+C8fV5X4mG3AVmvu+WVxcaU9xHHtBb7PCrPeweMmPjGsn8eMaeJg6SJuoUuZENeeSWaarWqonQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': - resolution: - { - integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-async-generators@7.8.4': - resolution: - { - integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==, - } + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-bigint@7.8.3': - resolution: - { - integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==, - } + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-class-properties@7.12.13': - resolution: - { - integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==, - } + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: - { - integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-dynamic-import@7.8.3': - resolution: - { - integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==, - } + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-export-default-from@7.25.9': - resolution: - { - integrity: sha512-9MhJ/SMTsVqsd69GyQg89lYR4o9T+oDGv5F6IsigxxqFVOyR/IflDLYP8WDI1l8fkhNGGktqkvL5qwNCtGEpgQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-9MhJ/SMTsVqsd69GyQg89lYR4o9T+oDGv5F6IsigxxqFVOyR/IflDLYP8WDI1l8fkhNGGktqkvL5qwNCtGEpgQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-flow@7.26.0': - resolution: - { - integrity: sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-assertions@7.26.0': - resolution: - { - integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-attributes@7.26.0': - resolution: - { - integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-meta@7.10.4': - resolution: - { - integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==, - } + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-json-strings@7.8.3': - resolution: - { - integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==, - } + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.25.9': - resolution: - { - integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==, - } - engines: { node: '>=6.9.0' } + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: - { - integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==, - } + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: - { - integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==, - } + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: - { - integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==, - } + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: - { - integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==, - } + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: - { - integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==, - } + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: - { - integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==, - } + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: - { - integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: - { - integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.25.9': - resolution: - { - integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==, - } - engines: { node: '>=6.9.0' } + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-unicode-sets-regex@7.18.6': - resolution: - { - integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-transform-arrow-functions@7.25.9': - resolution: - { - integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-async-generator-functions@7.26.8': - resolution: - { - integrity: sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-async-to-generator@7.25.9': - resolution: - { - integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-block-scoped-functions@7.26.5': - resolution: - { - integrity: sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-block-scoping@7.27.0': - resolution: - { - integrity: sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-class-properties@7.25.9': - resolution: - { - integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-class-static-block@7.26.0': - resolution: - { - integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 '@babel/plugin-transform-classes@7.25.9': - resolution: - { - integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-computed-properties@7.25.9': - resolution: - { - integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-destructuring@7.25.9': - resolution: - { - integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-dotall-regex@7.25.9': - resolution: - { - integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-duplicate-keys@7.25.9': - resolution: - { - integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': - resolution: - { - integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-transform-dynamic-import@7.25.9': - resolution: - { - integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-exponentiation-operator@7.26.3': - resolution: - { - integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-export-namespace-from@7.25.9': - resolution: - { - integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-flow-strip-types@7.26.5': - resolution: - { - integrity: sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-for-of@7.26.9': - resolution: - { - integrity: sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-function-name@7.25.9': - resolution: - { - integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-json-strings@7.25.9': - resolution: - { - integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-literals@7.25.9': - resolution: - { - integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-logical-assignment-operators@7.25.9': - resolution: - { - integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-member-expression-literals@7.25.9': - resolution: - { - integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-amd@7.25.9': - resolution: - { - integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-commonjs@7.26.3': - resolution: - { - integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-systemjs@7.25.9': - resolution: - { - integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-umd@7.25.9': - resolution: - { - integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': - resolution: - { - integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-transform-new-target@7.25.9': - resolution: - { - integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-nullish-coalescing-operator@7.26.6': - resolution: - { - integrity: sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-numeric-separator@7.25.9': - resolution: - { - integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-object-rest-spread@7.25.9': - resolution: - { - integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-object-super@7.25.9': - resolution: - { - integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-optional-catch-binding@7.25.9': - resolution: - { - integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-optional-chaining@7.25.9': - resolution: - { - integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-parameters@7.25.9': - resolution: - { - integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-private-methods@7.25.9': - resolution: - { - integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-private-property-in-object@7.25.9': - resolution: - { - integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-property-literals@7.25.9': - resolution: - { - integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-react-display-name@7.25.9': - resolution: - { - integrity: sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-react-jsx-self@7.25.9': - resolution: - { - integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-react-jsx-source@7.25.9': - resolution: - { - integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-react-jsx@7.25.9': - resolution: - { - integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-regenerator@7.27.0': - resolution: - { - integrity: sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-regexp-modifiers@7.26.0': - resolution: - { - integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-transform-reserved-words@7.25.9': - resolution: - { - integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-runtime@7.26.10': - resolution: - { - integrity: sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-shorthand-properties@7.25.9': - resolution: - { - integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-spread@7.25.9': - resolution: - { - integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-sticky-regex@7.25.9': - resolution: - { - integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-template-literals@7.26.8': - resolution: - { - integrity: sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-typeof-symbol@7.27.0': - resolution: - { - integrity: sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-typescript@7.27.0': - resolution: - { - integrity: sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-escapes@7.25.9': - resolution: - { - integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-property-regex@7.25.9': - resolution: - { - integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-regex@7.25.9': - resolution: - { - integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-sets-regex@7.25.9': - resolution: - { - integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/preset-env@7.26.9': - resolution: - { - integrity: sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/preset-flow@7.25.9': - resolution: - { - integrity: sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/preset-modules@0.1.6-no-external-plugins': - resolution: - { - integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==, - } + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 '@babel/preset-typescript@7.27.0': - resolution: - { - integrity: sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/register@7.25.9': - resolution: - { - integrity: sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/runtime@7.27.0': - resolution: - { - integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + engines: {node: '>=6.9.0'} '@babel/template@7.27.0': - resolution: - { - integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} '@babel/traverse@7.27.0': - resolution: - { - integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.4': + resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + engines: {node: '>=6.9.0'} '@babel/types@7.27.0': - resolution: - { - integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.6': + resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} '@chainsafe/as-chacha20poly1305@0.1.0': - resolution: - { - integrity: sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew==, - } + resolution: {integrity: sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew==} '@chainsafe/as-sha256@1.0.1': - resolution: - { - integrity: sha512-4Y/kQm0LsJ6QRtGcMq6gOdQP+fZhWDfIV2eIqP6oFJZBWYGmdh3wm8YbrXDPLJO87X2Fu6koRLdUS00O3k14Hw==, - } + resolution: {integrity: sha512-4Y/kQm0LsJ6QRtGcMq6gOdQP+fZhWDfIV2eIqP6oFJZBWYGmdh3wm8YbrXDPLJO87X2Fu6koRLdUS00O3k14Hw==} '@chainsafe/is-ip@2.1.0': - resolution: - { - integrity: sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w==, - } + resolution: {integrity: sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w==} '@chainsafe/libp2p-gossipsub@14.1.1': - resolution: - { - integrity: sha512-EUs2C+xHXXbw0pQQF2AN/ih4qB6BBWOGkDhvHz1VN52o2m/827IBEMT8RHdXMNZciQc90to1L57BKmhXkvztDw==, - } - engines: { npm: '>=8.7.0' } + resolution: {integrity: sha512-EUs2C+xHXXbw0pQQF2AN/ih4qB6BBWOGkDhvHz1VN52o2m/827IBEMT8RHdXMNZciQc90to1L57BKmhXkvztDw==} + engines: {npm: '>=8.7.0'} '@chainsafe/libp2p-noise@16.1.0': - resolution: - { - integrity: sha512-GJA/i5pd6VmetxokvnPlEbVCeL7SfLHkSuUHwbJ4w0u7dZUbse4Hr8SA8RYGwNHbZr2TEKFC9WerhvMWbciIrQ==, - } + resolution: {integrity: sha512-GJA/i5pd6VmetxokvnPlEbVCeL7SfLHkSuUHwbJ4w0u7dZUbse4Hr8SA8RYGwNHbZr2TEKFC9WerhvMWbciIrQ==} '@chainsafe/libp2p-yamux@7.0.1': - resolution: - { - integrity: sha512-949MI0Ll0AsYq1gUETZmL/MijwX0jilOQ1i4s8wDEXGiMhuPWWiMsPgEnX6n+VzFmTrfNYyGaaJj5/MqxV9y/g==, - } + resolution: {integrity: sha512-949MI0Ll0AsYq1gUETZmL/MijwX0jilOQ1i4s8wDEXGiMhuPWWiMsPgEnX6n+VzFmTrfNYyGaaJj5/MqxV9y/g==} '@chainsafe/netmask@2.0.0': - resolution: - { - integrity: sha512-I3Z+6SWUoaljh3TBzCnCxjlUyN8tA+NAk5L6m9IxvCf1BENQTePzPMis97CoN/iMW1St3WN+AWCCRp+TTBRiDg==, - } + resolution: {integrity: sha512-I3Z+6SWUoaljh3TBzCnCxjlUyN8tA+NAk5L6m9IxvCf1BENQTePzPMis97CoN/iMW1St3WN+AWCCRp+TTBRiDg==} '@colors/colors@1.6.0': - resolution: - { - integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==, - } - engines: { node: '>=0.1.90' } + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} '@dabh/diagnostics@2.0.3': - resolution: - { - integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==, - } + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] '@eslint-community/eslint-utils@4.5.1': - resolution: - { - integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 '@eslint-community/regexpp@4.12.1': - resolution: - { - integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==, - } - engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/config-array@0.20.0': - resolution: - { - integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/config-helpers@0.2.1': - resolution: - { - integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.12.0': - resolution: - { - integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.13.0': - resolution: - { - integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': - resolution: - { - integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.24.0': - resolution: - { - integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': - resolution: - { - integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/plugin-kit@0.2.8': - resolution: - { - integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@helia/bitswap@2.0.5': - resolution: - { - integrity: sha512-LdvjagmArJ6d67yFKIxU+H29be+u8teP3yQzL8CLPU2J6uG66Pwh0Bb7bU+D1uUyUcfLS4TqDrRh1VKR+EghYw==, - } + resolution: {integrity: sha512-LdvjagmArJ6d67yFKIxU+H29be+u8teP3yQzL8CLPU2J6uG66Pwh0Bb7bU+D1uUyUcfLS4TqDrRh1VKR+EghYw==} '@helia/block-brokers@4.1.0': - resolution: - { - integrity: sha512-pzIhJeDLdF0VFkj9+LeLI6ZZORdH6/FwV7Q+IlIi2m5kAExqaYh8Oob6Dc7J5luu1atf69q4PWNIPYRiySmsmg==, - } + resolution: {integrity: sha512-pzIhJeDLdF0VFkj9+LeLI6ZZORdH6/FwV7Q+IlIi2m5kAExqaYh8Oob6Dc7J5luu1atf69q4PWNIPYRiySmsmg==} '@helia/delegated-routing-v1-http-api-client@4.2.2': - resolution: - { - integrity: sha512-SQuyIZAbfvXUkGiralGI7sWq44Ztd1Cf+3pz/paCzq1J3Jvl7JnofWB0spsZjwSu0jYPdwAL60Nmg1TSTm6ZVg==, - } + resolution: {integrity: sha512-SQuyIZAbfvXUkGiralGI7sWq44Ztd1Cf+3pz/paCzq1J3Jvl7JnofWB0spsZjwSu0jYPdwAL60Nmg1TSTm6ZVg==} '@helia/interface@5.2.1': - resolution: - { - integrity: sha512-8eH3wOoOAHqcux2erXOm33oFBtKdpfHclepzn28bBYEl5wXhrc9JFeo2X3SYJeE0o/jxq0L39BprkYjgSSC91Q==, - } + resolution: {integrity: sha512-8eH3wOoOAHqcux2erXOm33oFBtKdpfHclepzn28bBYEl5wXhrc9JFeo2X3SYJeE0o/jxq0L39BprkYjgSSC91Q==} '@helia/routers@3.0.1': - resolution: - { - integrity: sha512-Eshr/8XJU4c0H8s1m5oBFB2YM0n3HBbxB3ny8DbsRFS8cAQ/L8ujnQomniMjZuuOhcNz8EEGwkUc07HCtAqAFA==, - } + resolution: {integrity: sha512-Eshr/8XJU4c0H8s1m5oBFB2YM0n3HBbxB3ny8DbsRFS8cAQ/L8ujnQomniMjZuuOhcNz8EEGwkUc07HCtAqAFA==} '@helia/unixfs@5.0.0': - resolution: - { - integrity: sha512-wIv9Zf4vM7UN2A7jNiOa5rOfO1Hl/9AarKSFQeV09I1NflclSSu6EHaiNcH1K6LBhRJ36/w2RHXE8DK3+DK8hw==, - } + resolution: {integrity: sha512-wIv9Zf4vM7UN2A7jNiOa5rOfO1Hl/9AarKSFQeV09I1NflclSSu6EHaiNcH1K6LBhRJ36/w2RHXE8DK3+DK8hw==} '@helia/utils@1.2.2': - resolution: - { - integrity: sha512-f8TC+gTQkMTVPaSDB8sSV+8W5/QIMX9XNWY2Xf0Y/WVzGm+Nz5o5wpVTT1kgBpILngXHSs4Xo+6aBQlafL15EA==, - } + resolution: {integrity: sha512-f8TC+gTQkMTVPaSDB8sSV+8W5/QIMX9XNWY2Xf0Y/WVzGm+Nz5o5wpVTT1kgBpILngXHSs4Xo+6aBQlafL15EA==} '@humanfs/core@0.19.1': - resolution: - { - integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, - } - engines: { node: '>=18.18.0' } + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} '@humanfs/node@0.16.6': - resolution: - { - integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==, - } - engines: { node: '>=18.18.0' } + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': - resolution: - { - integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, - } - engines: { node: '>=12.22' } + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} '@humanwhocodes/retry@0.3.1': - resolution: - { - integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} '@humanwhocodes/retry@0.4.2': - resolution: - { - integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + engines: {node: '>=18.18'} '@ipld/dag-cbor@9.2.2': - resolution: - { - integrity: sha512-uIEOuruCqKTP50OBWwgz4Js2+LhiBQaxc57cnP71f45b1mHEAo1OCR1Zn/TbvSW/mV1x+JqhacIktkKyaYqhCw==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-uIEOuruCqKTP50OBWwgz4Js2+LhiBQaxc57cnP71f45b1mHEAo1OCR1Zn/TbvSW/mV1x+JqhacIktkKyaYqhCw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} '@ipld/dag-json@10.2.3': - resolution: - { - integrity: sha512-itacv1j1hvYgLox2B42Msn70QLzcr0MEo5yGIENuw2SM/lQzq9bmBiMky+kDsIrsqqblKTXcHBZnnmK7D4a6ZQ==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-itacv1j1hvYgLox2B42Msn70QLzcr0MEo5yGIENuw2SM/lQzq9bmBiMky+kDsIrsqqblKTXcHBZnnmK7D4a6ZQ==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} '@ipld/dag-pb@4.1.3': - resolution: - { - integrity: sha512-ueULCaaSCcD+dQga6nKiRr+RSeVgdiYiEPKVUu5iQMNYDN+9osd0KpR3UDd9uQQ+6RWuv9L34SchfEwj7YIbOA==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-ueULCaaSCcD+dQga6nKiRr+RSeVgdiYiEPKVUu5iQMNYDN+9osd0KpR3UDd9uQQ+6RWuv9L34SchfEwj7YIbOA==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} '@ipshipyard/libp2p-auto-tls@1.0.0': - resolution: - { - integrity: sha512-wV1smnqbg3xUCHmPB8KWFuP8G9MKlx8KDuiJvhCWPi7B03xJq2FMybMDPI8tM9boa9sHD+5+NFu+Teo3Lz76fw==, - } + resolution: {integrity: sha512-wV1smnqbg3xUCHmPB8KWFuP8G9MKlx8KDuiJvhCWPi7B03xJq2FMybMDPI8tM9boa9sHD+5+NFu+Teo3Lz76fw==} '@ipshipyard/node-datachannel@0.26.5': - resolution: - { - integrity: sha512-GOxqgCI4scLTSFwFO7ClK5eDgSCJQgf7mbmJu0qgPu9zNlRp0VJl6rNJScQBllHP7IhmBf3VXRWVvwWfOrplww==, - } - engines: { node: '>=18.20.0' } + resolution: {integrity: sha512-GOxqgCI4scLTSFwFO7ClK5eDgSCJQgf7mbmJu0qgPu9zNlRp0VJl6rNJScQBllHP7IhmBf3VXRWVvwWfOrplww==} + engines: {node: '>=18.20.0'} '@isaacs/cliui@8.0.2': - resolution: - { - integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} '@isaacs/ttlcache@1.4.1': - resolution: - { - integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} '@istanbuljs/load-nyc-config@1.1.0': - resolution: - { - integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} '@istanbuljs/schema@0.1.3': - resolution: - { - integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@30.0.1': + resolution: {integrity: sha512-ThsJ+1I1/7CSTCmddZWqwkwremh3kmKCEoa7oafYL0A1a4tiXWKHzp8+a4m0EbXfGsYVjaVjjzywOQ1ZCnLlzg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.0.1': + resolution: {integrity: sha512-wImaJH4bFaV8oDJkCureHnnua0dOtgVgogh62gFKjTMXyKRVLjiVOJU9VypxXNqDUAM+W23VHJrJRauW3OLPeQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true '@jest/create-cache-key-function@29.7.0': - resolution: - { - integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/environment@29.7.0': - resolution: - { - integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/environment@30.0.1': + resolution: {integrity: sha512-JFI3qCT4ps9UjQNievPdsmpX+mOcAjOR2aemGUJbNiwpsuSCbiAaXwa2yBCND7OqCxUoiWMh6Lf/cwGxt/m2NA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.0.1': + resolution: {integrity: sha512-txHSNST7ud1V7JVFS5N1qqU+Wf6tiFPxDbjQpklTnckeVecFF8O+LD6efgF5z1dBigp4nMmDIYYxslQJHaS7QA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.0.1': + resolution: {integrity: sha512-mxhK5Zt8z+gOrXkv6RxQoRb1741EkcliTaNAIzrj1w4ch3TruFW+1QbLOTarovxo02EIh+a+JGky3r25p0nhIA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/fake-timers@29.7.0': - resolution: - { - integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@30.0.1': + resolution: {integrity: sha512-H/rYdOcSa+vlux7a3aw6bqQ/nMFMGQqmflAl4qFTThidyakO63ATiHSuhHL1yY39IFBCIbIiUpqr8ognXZA54A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.0.1': + resolution: {integrity: sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.0.1': + resolution: {integrity: sha512-5IdHDqKVayXzBL8sKM5AvPaAnrfO9GXphDLwOg6VWjUiqSrGcj/Hd518QpfDWOeu1aWjBblst3rxeRgbtOEJ8Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.0.1': + resolution: {integrity: sha512-r0vZe9j3J97Luj/qQ4G+nYpcvdhl1JuEeoJ7WgUN6FOUixztDKkqHjVtURmfUCoU7rqd1Hj5g5nKm35jClFhfw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true '@jest/schemas@29.6.3': - resolution: - { - integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/schemas@30.0.1': + resolution: {integrity: sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.0.1': + resolution: {integrity: sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.0.1': + resolution: {integrity: sha512-VpPEdwN+NivPsExCb9FCcIfIIP4x6vzGg4xfaH0URYkZcJixwe2E69uRqp9MPq6A4mWUoQRtjPNocFA/kRoiFg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.0.1': + resolution: {integrity: sha512-2D3F5XSPIfGMvdK+T6z8fExQso3sPnkBJsUM5x3YQ1Aaz+4Qrs4X8eqzMyC0i0ENfhcijidzz5yMTM4PvK+mKg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/transform@29.7.0': - resolution: - { - integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@30.0.1': + resolution: {integrity: sha512-BXZJPGD56+bwIq8EM0X6VqtM+/W4NCMBOxTe4MtfpPVyoZ+rIs6thzdem853vav2jQzpXDsyKir3DRQS5mS9Rw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/types@29.6.3': - resolution: - { - integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@30.0.1': + resolution: {integrity: sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jridgewell/gen-mapping@0.3.8': - resolution: - { - integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} '@jridgewell/resolve-uri@3.1.2': - resolution: - { - integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} '@jridgewell/set-array@1.2.1': - resolution: - { - integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} '@jridgewell/source-map@0.3.6': - resolution: - { - integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==, - } + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} '@jridgewell/sourcemap-codec@1.5.0': - resolution: - { - integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==, - } + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} '@jridgewell/trace-mapping@0.3.25': - resolution: - { - integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==, - } + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} '@leichtgewicht/ip-codec@2.0.5': - resolution: - { - integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==, - } + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} '@libp2p/autonat@2.0.28': - resolution: - { - integrity: sha512-SFVowDdGf+C8l3XpDAqvf6eVuFcxNVuo9AS5afkklZ9RoSgc46qVGBrRFsL/27gPkuFZspx5yMYClJO8/HRleg==, - } + resolution: {integrity: sha512-SFVowDdGf+C8l3XpDAqvf6eVuFcxNVuo9AS5afkklZ9RoSgc46qVGBrRFsL/27gPkuFZspx5yMYClJO8/HRleg==} '@libp2p/bootstrap@11.0.32': - resolution: - { - integrity: sha512-sXpl92PyFTHhq3mA56DDyqHwFKn8JSUdco83f4ur3Jl69Zm8e4Yxli2hdzWNPtOoXy2s0DCagc/bOFuSSYu8fA==, - } + resolution: {integrity: sha512-sXpl92PyFTHhq3mA56DDyqHwFKn8JSUdco83f4ur3Jl69Zm8e4Yxli2hdzWNPtOoXy2s0DCagc/bOFuSSYu8fA==} '@libp2p/circuit-relay-v2@3.2.8': - resolution: - { - integrity: sha512-rPKG0/1odRy4EynULTxTjshupAChZtjVNl/R41JO5KvvLV5XkFnOFyFdwXBxtS8telkpKhCEr+YGse6U0gZ3dA==, - } + resolution: {integrity: sha512-rPKG0/1odRy4EynULTxTjshupAChZtjVNl/R41JO5KvvLV5XkFnOFyFdwXBxtS8telkpKhCEr+YGse6U0gZ3dA==} '@libp2p/config@1.1.4': - resolution: - { - integrity: sha512-telhYtL8OdRXu0GSgCMtWbZpZy+sQxptU6mmUkZnJLLEAR9CVzRK9W2KzctAon9WSIop7ErKggAaiXeTGMEw8g==, - } + resolution: {integrity: sha512-telhYtL8OdRXu0GSgCMtWbZpZy+sQxptU6mmUkZnJLLEAR9CVzRK9W2KzctAon9WSIop7ErKggAaiXeTGMEw8g==} '@libp2p/crypto@5.0.15': - resolution: - { - integrity: sha512-28xYMOn3fs8flsNgCVVxp27gEmDTtZHbz+qEVv3v7cWfGRipaVhNXFV9tQJHWXHQ8mN8v/PQvgcfCcWu5jkrTg==, - } + resolution: {integrity: sha512-28xYMOn3fs8flsNgCVVxp27gEmDTtZHbz+qEVv3v7cWfGRipaVhNXFV9tQJHWXHQ8mN8v/PQvgcfCcWu5jkrTg==} '@libp2p/dcutr@2.0.27': - resolution: - { - integrity: sha512-A4qIN+nxfIJhlA0slqa90xlc2DADz2RT5gE09Edxi21jGY7g28IX98gd19A73P4EOMIH2aUBjW6xQpDXEmDLcw==, - } + resolution: {integrity: sha512-A4qIN+nxfIJhlA0slqa90xlc2DADz2RT5gE09Edxi21jGY7g28IX98gd19A73P4EOMIH2aUBjW6xQpDXEmDLcw==} '@libp2p/http-fetch@2.2.2': - resolution: - { - integrity: sha512-dMPo2pe2h/AHAljgwDEErdiB8JbiJM5b0LzuF/Yq4HcplfJZf33VtzUHN1n8x+3K+F8fntWUKN30SwSisSoVaw==, - } + resolution: {integrity: sha512-dMPo2pe2h/AHAljgwDEErdiB8JbiJM5b0LzuF/Yq4HcplfJZf33VtzUHN1n8x+3K+F8fntWUKN30SwSisSoVaw==} '@libp2p/identify@3.0.27': - resolution: - { - integrity: sha512-HCIT8I3X8CS/v1spocl5PR1qHGySvY5K7EtZSX7opH7wvQCT/WSWIQLSyY8hL0ezc2CGvCRpr6YVcuaYZtMjaA==, - } + resolution: {integrity: sha512-HCIT8I3X8CS/v1spocl5PR1qHGySvY5K7EtZSX7opH7wvQCT/WSWIQLSyY8hL0ezc2CGvCRpr6YVcuaYZtMjaA==} '@libp2p/interface-internal@2.3.9': - resolution: - { - integrity: sha512-1hW/yHktO3txc+r4ASmVA9GbNN6ZoGnH8Bt9VrYwY580BT53TP3eipn3Bo1XyGBDtmV6bpQiKhFK5AYVbhnz0g==, - } + resolution: {integrity: sha512-1hW/yHktO3txc+r4ASmVA9GbNN6ZoGnH8Bt9VrYwY580BT53TP3eipn3Bo1XyGBDtmV6bpQiKhFK5AYVbhnz0g==} '@libp2p/interface@2.7.0': - resolution: - { - integrity: sha512-lWmfIGzbSaw//yoEWWJh8dXNDGSCwUyXwC7P1Q6jCFWNoEtCaB1pvwOGBtri7Db/aNFZryMzN5covoq5ulldnA==, - } + resolution: {integrity: sha512-lWmfIGzbSaw//yoEWWJh8dXNDGSCwUyXwC7P1Q6jCFWNoEtCaB1pvwOGBtri7Db/aNFZryMzN5covoq5ulldnA==} '@libp2p/kad-dht@14.2.15': - resolution: - { - integrity: sha512-iARZsaKrm9LlOE0nRTsqMasYGfWbh+zw1TAMWOY/QHTszFGb9ol7FZoI9WUzoif9ltKLu3BjJpy00b8CVofCBw==, - } + resolution: {integrity: sha512-iARZsaKrm9LlOE0nRTsqMasYGfWbh+zw1TAMWOY/QHTszFGb9ol7FZoI9WUzoif9ltKLu3BjJpy00b8CVofCBw==} '@libp2p/keychain@5.1.4': - resolution: - { - integrity: sha512-0TyZSopKMX7npEEAAwyrm81BKURVx7pCvtlNvyroTd4Biyy6vV6GKwjSyBoNX9OYE7dRu8bDF0KH5sr1oxC0sg==, - } + resolution: {integrity: sha512-0TyZSopKMX7npEEAAwyrm81BKURVx7pCvtlNvyroTd4Biyy6vV6GKwjSyBoNX9OYE7dRu8bDF0KH5sr1oxC0sg==} '@libp2p/logger@5.1.13': - resolution: - { - integrity: sha512-JKyMlySG8T+LpItsj9Vma57yap/A0HqJ8ZdaHvgdoThhSOfqcRs8oRWO/2EG0Q5hUXugw//EAT+Ptj8MyNdbjQ==, - } + resolution: {integrity: sha512-JKyMlySG8T+LpItsj9Vma57yap/A0HqJ8ZdaHvgdoThhSOfqcRs8oRWO/2EG0Q5hUXugw//EAT+Ptj8MyNdbjQ==} '@libp2p/mdns@11.0.32': - resolution: - { - integrity: sha512-gMJePsHTND9o+S7A64tMt/vi5d/pBp7Tk1b1t5qq5JEzdeP64D0sW09pjEQ8b6U0OJxJ2pnKjxyYn0kEnE2eMQ==, - } + resolution: {integrity: sha512-gMJePsHTND9o+S7A64tMt/vi5d/pBp7Tk1b1t5qq5JEzdeP64D0sW09pjEQ8b6U0OJxJ2pnKjxyYn0kEnE2eMQ==} '@libp2p/mplex@11.0.32': - resolution: - { - integrity: sha512-91YZkr7N66pdrUDjX7KhevrafWt1ILY/jG5OlCX3RNNIOeguGxmNgdTF8o41JoK660lnI3OCCAN9mSE5cLLzGg==, - } + resolution: {integrity: sha512-91YZkr7N66pdrUDjX7KhevrafWt1ILY/jG5OlCX3RNNIOeguGxmNgdTF8o41JoK660lnI3OCCAN9mSE5cLLzGg==} '@libp2p/multistream-select@6.0.20': - resolution: - { - integrity: sha512-DHObPodBZXNUFiMzMX0KSnkbDM6am4G8GfkfYPpmx+yuleuutiJrmN95Xt9ximhn9m+YtEZWB2Je8+Lb0bwIYQ==, - } + resolution: {integrity: sha512-DHObPodBZXNUFiMzMX0KSnkbDM6am4G8GfkfYPpmx+yuleuutiJrmN95Xt9ximhn9m+YtEZWB2Je8+Lb0bwIYQ==} '@libp2p/peer-collections@6.0.25': - resolution: - { - integrity: sha512-sU6mjwANQvVPgTgslRZvxZ6cYzQJ66QmNHm6mrM0cx03Yf1heWnvL28N/P781nGsUjo1cJD7xB5ctAGk6A/lXw==, - } + resolution: {integrity: sha512-sU6mjwANQvVPgTgslRZvxZ6cYzQJ66QmNHm6mrM0cx03Yf1heWnvL28N/P781nGsUjo1cJD7xB5ctAGk6A/lXw==} '@libp2p/peer-id@5.1.0': - resolution: - { - integrity: sha512-9Xob9DDg1uBboM2QvJ5nyPbsjxsNS9obmGAYeAtLSx5aHAIC4AweJQFHssUUCfW7mufkzX/s3zyR62XPR4SYyQ==, - } + resolution: {integrity: sha512-9Xob9DDg1uBboM2QvJ5nyPbsjxsNS9obmGAYeAtLSx5aHAIC4AweJQFHssUUCfW7mufkzX/s3zyR62XPR4SYyQ==} '@libp2p/peer-record@8.0.25': - resolution: - { - integrity: sha512-IFuAhxzMS/NlXZS7+vn7tTJY32ODtKN/aFBRd1wekAw5DebGtvqkt9mN3UbeXJPesu9w87e4Q8GSarD0URXRlw==, - } + resolution: {integrity: sha512-IFuAhxzMS/NlXZS7+vn7tTJY32ODtKN/aFBRd1wekAw5DebGtvqkt9mN3UbeXJPesu9w87e4Q8GSarD0URXRlw==} '@libp2p/peer-store@11.1.2': - resolution: - { - integrity: sha512-2egfDs6j+uvreBrzChf5xwNe0kQgYhuaOBx3rVgCAHxuJyXK6/lK+PpEH3Cfgad+if388mII58MU5gzpbawsaw==, - } + resolution: {integrity: sha512-2egfDs6j+uvreBrzChf5xwNe0kQgYhuaOBx3rVgCAHxuJyXK6/lK+PpEH3Cfgad+if388mII58MU5gzpbawsaw==} '@libp2p/ping@2.0.27': - resolution: - { - integrity: sha512-UwkiEJkdKY/4ncwLxMeo1A0DMXMFPTOE39qFovIkEVZI+hLmwMGo9d6kaEVaNL7lyCPFvZmT28RAyK2wOqVZQg==, - } + resolution: {integrity: sha512-UwkiEJkdKY/4ncwLxMeo1A0DMXMFPTOE39qFovIkEVZI+hLmwMGo9d6kaEVaNL7lyCPFvZmT28RAyK2wOqVZQg==} '@libp2p/pubsub@10.1.8': - resolution: - { - integrity: sha512-y73boBSCWhykhAA21Rve6CFZaCw7qoDIUffbXz9elOlnOIr7M+1kS2fO6yamwWe5EWLZzGLq315JHeaOtWrugw==, - } + resolution: {integrity: sha512-y73boBSCWhykhAA21Rve6CFZaCw7qoDIUffbXz9elOlnOIr7M+1kS2fO6yamwWe5EWLZzGLq315JHeaOtWrugw==} '@libp2p/record@4.0.5': - resolution: - { - integrity: sha512-HfKugY+ZKizhxE/hbLqI8zcFLfYly2gakaL0k8wBXCfmOTrAV7UajeJWkWqrKkIEMHASUyapm746KF+i9e7Xmw==, - } + resolution: {integrity: sha512-HfKugY+ZKizhxE/hbLqI8zcFLfYly2gakaL0k8wBXCfmOTrAV7UajeJWkWqrKkIEMHASUyapm746KF+i9e7Xmw==} '@libp2p/tcp@10.1.8': - resolution: - { - integrity: sha512-O146gLsAKDD7Fp6MaaLREmWgp0nP2ju5TGKy5WThi8iKFFQ4mLWWb2QY8BFV1Drk6E/6boSgaDz1swqKypYAvA==, - } + resolution: {integrity: sha512-O146gLsAKDD7Fp6MaaLREmWgp0nP2ju5TGKy5WThi8iKFFQ4mLWWb2QY8BFV1Drk6E/6boSgaDz1swqKypYAvA==} '@libp2p/tls@2.1.1': - resolution: - { - integrity: sha512-BTkjDijVyatb1qjTtTZfYbt9778nwqst+ZayWMWW6B5tJQgVz8cW8DXeVtMPuGWyO79KHmMao43jE866PYsuRg==, - } + resolution: {integrity: sha512-BTkjDijVyatb1qjTtTZfYbt9778nwqst+ZayWMWW6B5tJQgVz8cW8DXeVtMPuGWyO79KHmMao43jE866PYsuRg==} '@libp2p/upnp-nat@3.1.11': - resolution: - { - integrity: sha512-rI1unUh7dz6TSOw7M94tv31f74sMQ+wY974yt0m6RIgMzNOSO0L6YhYCxdFLO39Ybf+Pxw0RKdvZBvokxBZoEw==, - } + resolution: {integrity: sha512-rI1unUh7dz6TSOw7M94tv31f74sMQ+wY974yt0m6RIgMzNOSO0L6YhYCxdFLO39Ybf+Pxw0RKdvZBvokxBZoEw==} '@libp2p/utils@6.6.0': - resolution: - { - integrity: sha512-QjS1+r+jInOxULjdATBc1N/gorUWUoJqEKxpqTcB2wOwCipzB58RYR3n3QPeoRHj1mVMhZujE1dTbmK/Nafhqg==, - } + resolution: {integrity: sha512-QjS1+r+jInOxULjdATBc1N/gorUWUoJqEKxpqTcB2wOwCipzB58RYR3n3QPeoRHj1mVMhZujE1dTbmK/Nafhqg==} '@libp2p/webrtc@5.2.9': - resolution: - { - integrity: sha512-+klNGfjTw2E0AovQl8/TnkMoMtPiSqy9uA7wOrWpwY8xc1NcaG0xo89b4WwvjVGMQdcti/gFGqQIJqU0wBkmew==, - } + resolution: {integrity: sha512-+klNGfjTw2E0AovQl8/TnkMoMtPiSqy9uA7wOrWpwY8xc1NcaG0xo89b4WwvjVGMQdcti/gFGqQIJqU0wBkmew==} '@libp2p/websockets@9.2.8': - resolution: - { - integrity: sha512-G4hOu3KDESFfNEvhLpGA0Dan8wRq7s5MtXfbCBnlSgbsWR3qZhQs6WdGaRd3T6Y038tGjmgKNJ3NS8+CbklwKw==, - } + resolution: {integrity: sha512-G4hOu3KDESFfNEvhLpGA0Dan8wRq7s5MtXfbCBnlSgbsWR3qZhQs6WdGaRd3T6Y038tGjmgKNJ3NS8+CbklwKw==} '@multiformats/dns@1.0.6': - resolution: - { - integrity: sha512-nt/5UqjMPtyvkG9BQYdJ4GfLK3nMqGpFZOzf4hAmIa0sJh2LlS9YKXZ4FgwBDsaHvzZqR/rUFIywIc7pkHNNuw==, - } + resolution: {integrity: sha512-nt/5UqjMPtyvkG9BQYdJ4GfLK3nMqGpFZOzf4hAmIa0sJh2LlS9YKXZ4FgwBDsaHvzZqR/rUFIywIc7pkHNNuw==} '@multiformats/mafmt@12.1.6': - resolution: - { - integrity: sha512-tlJRfL21X+AKn9b5i5VnaTD6bNttpSpcqwKVmDmSHLwxoz97fAHaepqFOk/l1fIu94nImIXneNbhsJx/RQNIww==, - } + resolution: {integrity: sha512-tlJRfL21X+AKn9b5i5VnaTD6bNttpSpcqwKVmDmSHLwxoz97fAHaepqFOk/l1fIu94nImIXneNbhsJx/RQNIww==} '@multiformats/multiaddr-matcher@1.7.0': - resolution: - { - integrity: sha512-WfobrJy7XLaYL7PQ3IcFoXdGN5jmdv5FsuKQkZIIreC1pSR4Q9PSOWu2ULxP/M2JT738Xny0PFoCke0ENbyfww==, - } + resolution: {integrity: sha512-WfobrJy7XLaYL7PQ3IcFoXdGN5jmdv5FsuKQkZIIreC1pSR4Q9PSOWu2ULxP/M2JT738Xny0PFoCke0ENbyfww==} '@multiformats/multiaddr-to-uri@11.0.0': - resolution: - { - integrity: sha512-9RNmlIGwZbBLsHekT50dbt4o4u8Iciw9kGjv+WHiGxQdsJ6xKKjU1+C0Vbas6RilMbaVOAOnEyfNcXbUmTkLxQ==, - } + resolution: {integrity: sha512-9RNmlIGwZbBLsHekT50dbt4o4u8Iciw9kGjv+WHiGxQdsJ6xKKjU1+C0Vbas6RilMbaVOAOnEyfNcXbUmTkLxQ==} '@multiformats/multiaddr@12.4.0': - resolution: - { - integrity: sha512-FL7yBTLijJ5JkO044BGb2msf+uJLrwpD6jD6TkXlbjA9N12+18HT40jvd4o5vL4LOJMc86dPX6tGtk/uI9kYKg==, - } + resolution: {integrity: sha512-FL7yBTLijJ5JkO044BGb2msf+uJLrwpD6jD6TkXlbjA9N12+18HT40jvd4o5vL4LOJMc86dPX6tGtk/uI9kYKg==} '@multiformats/murmur3@2.1.8': - resolution: - { - integrity: sha512-6vId1C46ra3R1sbJUOFCZnsUIveR9oF20yhPmAFxPm0JfrX3/ZRCgP3YDrBzlGoEppOXnA9czHeYc0T9mB6hbA==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-6vId1C46ra3R1sbJUOFCZnsUIveR9oF20yhPmAFxPm0JfrX3/ZRCgP3YDrBzlGoEppOXnA9czHeYc0T9mB6hbA==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} '@multiformats/uri-to-multiaddr@8.1.0': - resolution: - { - integrity: sha512-NHFqdKEwJ0A6JDXzC645Lgyw72zWhbM1QfaaD00ZYRrNvtx64p1bD9aIrWZIhLWZN87/lsV4QkJSNRF3Fd3ryw==, - } + resolution: {integrity: sha512-NHFqdKEwJ0A6JDXzC645Lgyw72zWhbM1QfaaD00ZYRrNvtx64p1bD9aIrWZIhLWZN87/lsV4QkJSNRF3Fd3ryw==} + + '@napi-rs/wasm-runtime@0.2.11': + resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} '@noble/ciphers@1.2.1': - resolution: - { - integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==, - } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} + engines: {node: ^14.21.3 || >=16} '@noble/curves@1.8.1': - resolution: - { - integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==, - } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} + engines: {node: ^14.21.3 || >=16} '@noble/hashes@1.7.1': - resolution: - { - integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==, - } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} '@nodelib/fs.scandir@2.1.5': - resolution: - { - integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} '@nodelib/fs.stat@2.0.5': - resolution: - { - integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} '@nodelib/fs.walk@1.2.8': - resolution: - { - integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} '@orbitdb/core-types@1.0.14': - resolution: - { - integrity: sha512-Mq+o9zMw8n1kD3eMOQZZ/XG9TnQ8HqUInmduBfMidPXFSQVrvVtTAxyFpHk+2+FMf6BTj1xhrAUCEZlcvYebeQ==, - } + resolution: {integrity: sha512-Mq+o9zMw8n1kD3eMOQZZ/XG9TnQ8HqUInmduBfMidPXFSQVrvVtTAxyFpHk+2+FMf6BTj1xhrAUCEZlcvYebeQ==} '@orbitdb/core@2.5.0': - resolution: - { - integrity: sha512-RE+te/z8MCLhkawby2j9J0T0hH8Mi9BgTUATWd99ih/cyU9OTbbXVuY64A64bdAQ4EZkfJ+OjO9vAMhJHwgnew==, - } - engines: { node: '>=20.0.0' } + resolution: {integrity: sha512-RE+te/z8MCLhkawby2j9J0T0hH8Mi9BgTUATWd99ih/cyU9OTbbXVuY64A64bdAQ4EZkfJ+OjO9vAMhJHwgnew==} + engines: {node: '>=20.0.0'} '@orbitdb/feed-db@1.1.2': - resolution: - { - integrity: sha512-o+9SBiVWdbvBWQyGRXUajTq75uDQqRzLafbn2dGMshsU8ibWAllxpOhHXojaDyJRUGXxjt25LkbLFQGQQGiBEQ==, - } + resolution: {integrity: sha512-o+9SBiVWdbvBWQyGRXUajTq75uDQqRzLafbn2dGMshsU8ibWAllxpOhHXojaDyJRUGXxjt25LkbLFQGQQGiBEQ==} '@peculiar/asn1-cms@2.3.15': - resolution: - { - integrity: sha512-B+DoudF+TCrxoJSTjjcY8Mmu+lbv8e7pXGWrhNp2/EGJp9EEcpzjBCar7puU57sGifyzaRVM03oD5L7t7PghQg==, - } + resolution: {integrity: sha512-B+DoudF+TCrxoJSTjjcY8Mmu+lbv8e7pXGWrhNp2/EGJp9EEcpzjBCar7puU57sGifyzaRVM03oD5L7t7PghQg==} '@peculiar/asn1-csr@2.3.15': - resolution: - { - integrity: sha512-caxAOrvw2hUZpxzhz8Kp8iBYKsHbGXZPl2KYRMIPvAfFateRebS3136+orUpcVwHRmpXWX2kzpb6COlIrqCumA==, - } + resolution: {integrity: sha512-caxAOrvw2hUZpxzhz8Kp8iBYKsHbGXZPl2KYRMIPvAfFateRebS3136+orUpcVwHRmpXWX2kzpb6COlIrqCumA==} '@peculiar/asn1-ecc@2.3.15': - resolution: - { - integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==, - } + resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==} '@peculiar/asn1-pfx@2.3.15': - resolution: - { - integrity: sha512-E3kzQe3J2xV9DP6SJS4X6/N1e4cYa2xOAK46VtvpaRk8jlheNri8v0rBezKFVPB1rz/jW8npO+u1xOvpATFMWg==, - } + resolution: {integrity: sha512-E3kzQe3J2xV9DP6SJS4X6/N1e4cYa2xOAK46VtvpaRk8jlheNri8v0rBezKFVPB1rz/jW8npO+u1xOvpATFMWg==} '@peculiar/asn1-pkcs8@2.3.15': - resolution: - { - integrity: sha512-/PuQj2BIAw1/v76DV1LUOA6YOqh/UvptKLJHtec/DQwruXOCFlUo7k6llegn8N5BTeZTWMwz5EXruBw0Q10TMg==, - } + resolution: {integrity: sha512-/PuQj2BIAw1/v76DV1LUOA6YOqh/UvptKLJHtec/DQwruXOCFlUo7k6llegn8N5BTeZTWMwz5EXruBw0Q10TMg==} '@peculiar/asn1-pkcs9@2.3.15': - resolution: - { - integrity: sha512-yiZo/1EGvU1KiQUrbcnaPGWc0C7ElMMskWn7+kHsCFm+/9fU0+V1D/3a5oG0Jpy96iaXggQpA9tzdhnYDgjyFg==, - } + resolution: {integrity: sha512-yiZo/1EGvU1KiQUrbcnaPGWc0C7ElMMskWn7+kHsCFm+/9fU0+V1D/3a5oG0Jpy96iaXggQpA9tzdhnYDgjyFg==} '@peculiar/asn1-rsa@2.3.15': - resolution: - { - integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==, - } + resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==} '@peculiar/asn1-schema@2.3.15': - resolution: - { - integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==, - } + resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==} '@peculiar/asn1-x509-attr@2.3.15': - resolution: - { - integrity: sha512-TWJVJhqc+IS4MTEML3l6W1b0sMowVqdsnI4dnojg96LvTuP8dga9f76fjP07MUuss60uSyT2ckoti/2qHXA10A==, - } + resolution: {integrity: sha512-TWJVJhqc+IS4MTEML3l6W1b0sMowVqdsnI4dnojg96LvTuP8dga9f76fjP07MUuss60uSyT2ckoti/2qHXA10A==} '@peculiar/asn1-x509@2.3.15': - resolution: - { - integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==, - } + resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==} '@peculiar/json-schema@1.1.12': - resolution: - { - integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} '@peculiar/webcrypto@1.5.0': - resolution: - { - integrity: sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==, - } - engines: { node: '>=10.12.0' } + resolution: {integrity: sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==} + engines: {node: '>=10.12.0'} '@peculiar/x509@1.12.3': - resolution: - { - integrity: sha512-+Mzq+W7cNEKfkNZzyLl6A6ffqc3r21HGZUezgfKxpZrkORfOqgRXnS80Zu0IV6a9Ue9QBJeKD7kN0iWfc3bhRQ==, - } + resolution: {integrity: sha512-+Mzq+W7cNEKfkNZzyLl6A6ffqc3r21HGZUezgfKxpZrkORfOqgRXnS80Zu0IV6a9Ue9QBJeKD7kN0iWfc3bhRQ==} '@pkgjs/parseargs@0.11.0': - resolution: - { - integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} '@pkgr/core@0.2.1': - resolution: - { - integrity: sha512-VzgHzGblFmUeBmmrk55zPyrQIArQN4vujc9shWytaPdB3P7qhi0cpaiKIr7tlCmFv2lYUwnLospIqjL9ZSAhhg==, - } - engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-VzgHzGblFmUeBmmrk55zPyrQIArQN4vujc9shWytaPdB3P7qhi0cpaiKIr7tlCmFv2lYUwnLospIqjL9ZSAhhg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@pkgr/core@0.2.7': + resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} '@react-native/assets-registry@0.78.1': - resolution: - { - integrity: sha512-SegfYQFuut05EQIQIVB/6QMGaxJ29jEtPmzFWJdIp/yc2mmhIq7MfWRjwOe6qbONzIdp6Ca8p835hiGiAGyeKQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-SegfYQFuut05EQIQIVB/6QMGaxJ29jEtPmzFWJdIp/yc2mmhIq7MfWRjwOe6qbONzIdp6Ca8p835hiGiAGyeKQ==} + engines: {node: '>=18'} '@react-native/babel-plugin-codegen@0.78.1': - resolution: - { - integrity: sha512-rD0tnct/yPEtoOc8eeFHIf8ZJJJEzLkmqLs8HZWSkt3w9VYWngqLXZxiDGqv0ngXjunAlC/Hpq+ULMVOvOnByw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-rD0tnct/yPEtoOc8eeFHIf8ZJJJEzLkmqLs8HZWSkt3w9VYWngqLXZxiDGqv0ngXjunAlC/Hpq+ULMVOvOnByw==} + engines: {node: '>=18'} '@react-native/babel-preset@0.78.1': - resolution: - { - integrity: sha512-yTVcHmEdNQH4Ju7lhvbiQaGxBpMcalgkBy/IvHowXKk/ex3nY1PolF16/mBG1BrefcUA/rtJpqTtk2Ii+7T/Lw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-yTVcHmEdNQH4Ju7lhvbiQaGxBpMcalgkBy/IvHowXKk/ex3nY1PolF16/mBG1BrefcUA/rtJpqTtk2Ii+7T/Lw==} + engines: {node: '>=18'} peerDependencies: '@babel/core': '*' '@react-native/codegen@0.78.1': - resolution: - { - integrity: sha512-kGG5qAM9JdFtxzUwe7c6CyJbsU2PnaTrtCHA2dF8VEiNX1K3yd9yKPzfkxA7HPvmHoAn3ga1941O79BStWcM3A==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-kGG5qAM9JdFtxzUwe7c6CyJbsU2PnaTrtCHA2dF8VEiNX1K3yd9yKPzfkxA7HPvmHoAn3ga1941O79BStWcM3A==} + engines: {node: '>=18'} peerDependencies: '@babel/preset-env': ^7.1.6 '@react-native/community-cli-plugin@0.78.1': - resolution: - { - integrity: sha512-S6vF4oWpFqThpt/dBLrqLQw5ED2M1kg5mVtiL6ZqpoYIg+/e0vg7LZ8EXNbcdMDH4obRnm2xbOd+qlC7mOzNBg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-S6vF4oWpFqThpt/dBLrqLQw5ED2M1kg5mVtiL6ZqpoYIg+/e0vg7LZ8EXNbcdMDH4obRnm2xbOd+qlC7mOzNBg==} + engines: {node: '>=18'} peerDependencies: '@react-native-community/cli': '*' peerDependenciesMeta: @@ -1942,54 +1563,33 @@ packages: optional: true '@react-native/debugger-frontend@0.78.1': - resolution: - { - integrity: sha512-xev/B++QLxSDpEBWsc74GyCuq9XOHYTBwcGSpsuhOJDUha6WZIbEEvZe3LpVW+OiFso4oGIdnVSQntwippZdWw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-xev/B++QLxSDpEBWsc74GyCuq9XOHYTBwcGSpsuhOJDUha6WZIbEEvZe3LpVW+OiFso4oGIdnVSQntwippZdWw==} + engines: {node: '>=18'} '@react-native/dev-middleware@0.78.1': - resolution: - { - integrity: sha512-l8p7/dXa1vWPOdj0iuACkex8lgbLpYyPZ3QXGkocMcpl0bQ24K7hf3Bj02tfptP5PAm16b2RuEi04sjIGHUzzg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-l8p7/dXa1vWPOdj0iuACkex8lgbLpYyPZ3QXGkocMcpl0bQ24K7hf3Bj02tfptP5PAm16b2RuEi04sjIGHUzzg==} + engines: {node: '>=18'} '@react-native/gradle-plugin@0.78.1': - resolution: - { - integrity: sha512-v8GJU+8DzQDWO3iuTFI1nbuQ/kzuqbXv07VVtSIMLbdofHzuuQT14DGBacBkrIDKBDTVaBGAc/baDNsyxCghng==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-v8GJU+8DzQDWO3iuTFI1nbuQ/kzuqbXv07VVtSIMLbdofHzuuQT14DGBacBkrIDKBDTVaBGAc/baDNsyxCghng==} + engines: {node: '>=18'} '@react-native/js-polyfills@0.78.1': - resolution: - { - integrity: sha512-Ogcv4QOA1o3IyErrf/i4cDnP+nfNcIfGTgw6iNQyAPry1xjPOz4ziajskLpWG/3ADeneIZuyZppKB4A28rZSvg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-Ogcv4QOA1o3IyErrf/i4cDnP+nfNcIfGTgw6iNQyAPry1xjPOz4ziajskLpWG/3ADeneIZuyZppKB4A28rZSvg==} + engines: {node: '>=18'} '@react-native/metro-babel-transformer@0.78.1': - resolution: - { - integrity: sha512-jQWf69D+QTMvSZSWLR+cr3VUF16rGB6sbD+bItD8Czdfn3hajzfMoHJTkVFP7991cjK5sIVekNiQIObou8JSQw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-jQWf69D+QTMvSZSWLR+cr3VUF16rGB6sbD+bItD8Czdfn3hajzfMoHJTkVFP7991cjK5sIVekNiQIObou8JSQw==} + engines: {node: '>=18'} peerDependencies: '@babel/core': '*' '@react-native/normalize-colors@0.78.1': - resolution: - { - integrity: sha512-h4wARnY4iBFgigN1NjnaKFtcegWwQyE9+CEBVG4nHmwMtr8lZBmc7ZKIM6hUc6lxqY/ugHg48aSQSynss7mJUg==, - } + resolution: {integrity: sha512-h4wARnY4iBFgigN1NjnaKFtcegWwQyE9+CEBVG4nHmwMtr8lZBmc7ZKIM6hUc6lxqY/ugHg48aSQSynss7mJUg==} '@react-native/virtualized-lists@0.78.1': - resolution: - { - integrity: sha512-v0jqDNMFXpnRnSlkDVvwNxXgPhifzzTFlxTSnHj9erKJsKpE26gSU5qB4hmJkEsscLG/ygdJ1c88aqinSh/wRA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-v0jqDNMFXpnRnSlkDVvwNxXgPhifzzTFlxTSnHj9erKJsKpE26gSU5qB4hmJkEsscLG/ygdJ1c88aqinSh/wRA==} + engines: {node: '>=18'} peerDependencies: '@types/react': ^19.0.0 react: '*' @@ -1999,1022 +1599,725 @@ packages: optional: true '@sinclair/typebox@0.27.8': - resolution: - { - integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==, - } + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinclair/typebox@0.34.35': + resolution: {integrity: sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==} '@sindresorhus/fnv1a@3.1.0': - resolution: - { - integrity: sha512-KV321z5m/0nuAg83W1dPLy85HpHDk7Sdi4fJbwvacWsEhAh+rZUW4ZfGcXmUIvjZg4ss2bcwNlRhJ7GBEUG08w==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-KV321z5m/0nuAg83W1dPLy85HpHDk7Sdi4fJbwvacWsEhAh+rZUW4ZfGcXmUIvjZg4ss2bcwNlRhJ7GBEUG08w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} '@sinonjs/commons@3.0.1': - resolution: - { - integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==, - } + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} '@sinonjs/fake-timers@10.3.0': - resolution: - { - integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==, - } + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@sinonjs/fake-timers@13.0.5': + resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} '@topoconfig/extends@0.16.2': - resolution: - { - integrity: sha512-sTF+qpWakr5jf1Hn/kkFSi833xPW15s/loMAiKSYSSVv4vDonxf6hwCGzMXjLq+7HZoaK6BgaV72wXr1eY7FcQ==, - } + resolution: {integrity: sha512-sTF+qpWakr5jf1Hn/kkFSi833xPW15s/loMAiKSYSSVv4vDonxf6hwCGzMXjLq+7HZoaK6BgaV72wXr1eY7FcQ==} hasBin: true + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/babel__core@7.20.5': - resolution: - { - integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, - } + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} '@types/babel__generator@7.27.0': - resolution: - { - integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, - } + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} '@types/babel__template@7.4.4': - resolution: - { - integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, - } + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} '@types/babel__traverse@7.20.7': - resolution: - { - integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==, - } + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} '@types/body-parser@1.19.5': - resolution: - { - integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==, - } + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} '@types/connect@3.4.38': - resolution: - { - integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==, - } + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} '@types/dns-packet@5.6.5': - resolution: - { - integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==, - } + resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==} '@types/estree@1.0.7': - resolution: - { - integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==, - } + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} '@types/express-serve-static-core@5.0.6': - resolution: - { - integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==, - } + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} '@types/express@5.0.1': - resolution: - { - integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==, - } + resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} '@types/graceful-fs@4.1.9': - resolution: - { - integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==, - } + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} '@types/http-errors@2.0.4': - resolution: - { - integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==, - } + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} '@types/istanbul-lib-coverage@2.0.6': - resolution: - { - integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==, - } + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} '@types/istanbul-lib-report@3.0.3': - resolution: - { - integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==, - } + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} '@types/istanbul-reports@3.0.4': - resolution: - { - integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==, - } + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} '@types/json-schema@7.0.15': - resolution: - { - integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, - } + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/mime@1.3.5': - resolution: - { - integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==, - } + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} '@types/multicast-dns@7.2.4': - resolution: - { - integrity: sha512-ib5K4cIDR4Ro5SR3Sx/LROkMDa0BHz0OPaCBL/OSPDsAXEGZ3/KQeS6poBKYVN7BfjXDL9lWNwzyHVgt/wkyCw==, - } + resolution: {integrity: sha512-ib5K4cIDR4Ro5SR3Sx/LROkMDa0BHz0OPaCBL/OSPDsAXEGZ3/KQeS6poBKYVN7BfjXDL9lWNwzyHVgt/wkyCw==} + + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} '@types/node-forge@1.3.11': - resolution: - { - integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==, - } + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} '@types/node@22.13.16': - resolution: - { - integrity: sha512-15tM+qA4Ypml/N7kyRdvfRjBQT2RL461uF1Bldn06K0Nzn1lY3nAPgHlsVrJxdZ9WhZiW0Fmc1lOYMtDsAuB3w==, - } + resolution: {integrity: sha512-15tM+qA4Ypml/N7kyRdvfRjBQT2RL461uF1Bldn06K0Nzn1lY3nAPgHlsVrJxdZ9WhZiW0Fmc1lOYMtDsAuB3w==} '@types/node@22.14.0': - resolution: - { - integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==, - } + resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==} '@types/qs@6.9.18': - resolution: - { - integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==, - } + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} '@types/range-parser@1.2.7': - resolution: - { - integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==, - } + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} '@types/retry@0.12.2': - resolution: - { - integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==, - } + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} '@types/send@0.17.4': - resolution: - { - integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==, - } + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} '@types/serve-static@1.15.7': - resolution: - { - integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==, - } + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} '@types/sinon@17.0.4': - resolution: - { - integrity: sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==, - } + resolution: {integrity: sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==} '@types/sinonjs__fake-timers@8.1.5': - resolution: - { - integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==, - } + resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} '@types/stack-utils@2.0.3': - resolution: - { - integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==, - } + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} '@types/triple-beam@1.3.5': - resolution: - { - integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==, - } + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} '@types/ws@8.18.1': - resolution: - { - integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==, - } + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} '@types/yargs-parser@21.0.3': - resolution: - { - integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==, - } + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} '@types/yargs@17.0.33': - resolution: - { - integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==, - } + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} '@typescript-eslint/eslint-plugin@8.29.0': - resolution: - { - integrity: sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/parser@8.29.0': - resolution: - { - integrity: sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/scope-manager@8.29.0': - resolution: - { - integrity: sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/type-utils@8.29.0': - resolution: - { - integrity: sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/types@8.29.0': - resolution: - { - integrity: sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.29.0': - resolution: - { - integrity: sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/utils@8.29.0': - resolution: - { - integrity: sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/visitor-keys@8.29.0': - resolution: - { - integrity: sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.9.0': + resolution: {integrity: sha512-h1T2c2Di49ekF2TE8ZCoJkb+jwETKUIPDJ/nO3tJBKlLFPu+fyd93f0rGP/BvArKx2k2HlRM4kqkNarj3dvZlg==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.9.0': + resolution: {integrity: sha512-sG1NHtgXtX8owEkJ11yn34vt0Xqzi3k9TJ8zppDmyG8GZV4kVWw44FHwKwHeEFl07uKPeC4ZoyuQaGh5ruJYPA==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.9.0': + resolution: {integrity: sha512-nJ9z47kfFnCxN1z/oYZS7HSNsFh43y2asePzTEZpEvK7kGyuShSl3RRXnm/1QaqFL+iP+BjMwuB+DYUymOkA5A==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.9.0': + resolution: {integrity: sha512-TK+UA1TTa0qS53rjWn7cVlEKVGz2B6JYe0C++TdQjvWYIyx83ruwh0wd4LRxYBM5HeuAzXcylA9BH2trARXJTw==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.9.0': + resolution: {integrity: sha512-6uZwzMRFcD7CcCd0vz3Hp+9qIL2jseE/bx3ZjaLwn8t714nYGwiE84WpaMCYjU+IQET8Vu/+BNAGtYD7BG/0yA==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.9.0': + resolution: {integrity: sha512-bPUBksQfrgcfv2+mm+AZinaKq8LCFvt5PThYqRotqSuuZK1TVKkhbVMS/jvSRfYl7jr3AoZLYbDkItxgqMKRkg==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.9.0': + resolution: {integrity: sha512-uT6E7UBIrTdCsFQ+y0tQd3g5oudmrS/hds5pbU3h4s2t/1vsGWbbSKhBSCD9mcqaqkBwoqlECpUrRJCmldl8PA==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.9.0': + resolution: {integrity: sha512-vdqBh911wc5awE2bX2zx3eflbyv8U9xbE/jVKAm425eRoOVv/VseGZsqi3A3SykckSpF4wSROkbQPvbQFn8EsA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.9.0': + resolution: {integrity: sha512-/8JFZ/SnuDr1lLEVsxsuVwrsGquTvT51RZGvyDB/dOK3oYK2UqeXzgeyq6Otp8FZXQcEYqJwxb9v+gtdXn03eQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.9.0': + resolution: {integrity: sha512-FkJjybtrl+rajTw4loI3L6YqSOpeZfDls4SstL/5lsP2bka9TiHUjgMBjygeZEis1oC8LfJTS8FSgpKPaQx2tQ==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.9.0': + resolution: {integrity: sha512-w/NZfHNeDusbqSZ8r/hp8iL4S39h4+vQMc9/vvzuIKMWKppyUGKm3IST0Qv0aOZ1rzIbl9SrDeIqK86ZpUK37w==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.9.0': + resolution: {integrity: sha512-bEPBosut8/8KQbUixPry8zg/fOzVOWyvwzOfz0C0Rw6dp+wIBseyiHKjkcSyZKv/98edrbMknBaMNJfA/UEdqw==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.9.0': + resolution: {integrity: sha512-LDtMT7moE3gK753gG4pc31AAqGUC86j3AplaFusc717EUGF9ZFJ356sdQzzZzkBk1XzMdxFyZ4f/i35NKM/lFA==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.9.0': + resolution: {integrity: sha512-WmFd5KINHIXj8o1mPaT8QRjA9HgSXhN1gl9Da4IZihARihEnOylu4co7i/yeaIpcfsI6sYs33cNZKyHYDh0lrA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.9.0': + resolution: {integrity: sha512-CYuXbANW+WgzVRIl8/QvZmDaZxrqvOldOwlbUjIM4pQ46FJ0W5cinJ/Ghwa/Ng1ZPMJMk1VFdsD/XwmCGIXBWg==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.9.0': + resolution: {integrity: sha512-6Rp2WH0OoitMYR57Z6VE8Y6corX8C6QEMWLgOV6qXiJIeZ1F9WGXY/yQ8yDC4iTraotyLOeJ2Asea0urWj2fKQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.9.0': + resolution: {integrity: sha512-rknkrTRuvujprrbPmGeHi8wYWxmNVlBoNW8+4XF2hXUnASOjmuC9FNF1tGbDiRQWn264q9U/oGtixyO3BT8adQ==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.9.0': + resolution: {integrity: sha512-Ceymm+iBl+bgAICtgiHyMLz6hjxmLJKqBim8tDzpX61wpZOx2bPK6Gjuor7I2RiUynVjvvkoRIkrPyMwzBzF3A==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.9.0': + resolution: {integrity: sha512-k59o9ZyeyS0hAlcaKFezYSH2agQeRFEB7KoQLXl3Nb3rgkqT1NY9Vwy+SqODiLmYnEjxWJVRE/yq2jFVqdIxZw==} + cpu: [x64] + os: [win32] abort-controller@3.0.0: - resolution: - { - integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, - } - engines: { node: '>=6.5' } + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} abort-error@1.0.1: - resolution: - { - integrity: sha512-fxqCblJiIPdSXIUrxI0PL+eJG49QdP9SQ70qtB65MVAoMr2rASlOyAbJFOylfB467F/f+5BCLJJq58RYi7mGfg==, - } + resolution: {integrity: sha512-fxqCblJiIPdSXIUrxI0PL+eJG49QdP9SQ70qtB65MVAoMr2rASlOyAbJFOylfB467F/f+5BCLJJq58RYi7mGfg==} abstract-level@1.0.4: - resolution: - { - integrity: sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==} + engines: {node: '>=12'} accepts@1.3.8: - resolution: - { - integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} accepts@2.0.0: - resolution: - { - integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} acme-client@5.4.0: - resolution: - { - integrity: sha512-mORqg60S8iML6XSmVjqjGHJkINrCGLMj2QvDmFzI9vIlv1RGlyjmw3nrzaINJjkNsYXC41XhhD5pfy7CtuGcbA==, - } - engines: { node: '>= 16' } + resolution: {integrity: sha512-mORqg60S8iML6XSmVjqjGHJkINrCGLMj2QvDmFzI9vIlv1RGlyjmw3nrzaINJjkNsYXC41XhhD5pfy7CtuGcbA==} + engines: {node: '>= 16'} acorn-jsx@5.3.2: - resolution: - { - integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, - } + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn@8.14.1: - resolution: - { - integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==, - } - engines: { node: '>=0.4.0' } + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} hasBin: true agent-base@7.1.3: - resolution: - { - integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==, - } - engines: { node: '>= 14' } + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} ajv@6.12.6: - resolution: - { - integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, - } + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} anser@1.4.10: - resolution: - { - integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==, - } + resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} ansi-escapes@7.0.0: - resolution: - { - integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} ansi-regex@5.0.1: - resolution: - { - integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} ansi-regex@6.1.0: - resolution: - { - integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} ansi-styles@4.3.0: - resolution: - { - integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} ansi-styles@5.2.0: - resolution: - { - integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} ansi-styles@6.2.1: - resolution: - { - integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} any-signal@4.1.1: - resolution: - { - integrity: sha512-iADenERppdC+A2YKbOXXB2WUeABLaM6qnpZ70kZbPZ1cZMMJ7eF+3CaYm+/PhBizgkzlvssC7QuHS30oOiQYWA==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-iADenERppdC+A2YKbOXXB2WUeABLaM6qnpZ70kZbPZ1cZMMJ7eF+3CaYm+/PhBizgkzlvssC7QuHS30oOiQYWA==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} anymatch@3.1.3: - resolution: - { - integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} argparse@1.0.10: - resolution: - { - integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==, - } + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: - resolution: - { - integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, - } + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} asap@2.0.6: - resolution: - { - integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==, - } + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} asn1js@3.0.6: - resolution: - { - integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==, - } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} + engines: {node: '>=12.0.0'} ast-types@0.16.1: - resolution: - { - integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} async-limiter@1.0.1: - resolution: - { - integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==, - } + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} async@3.2.6: - resolution: - { - integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==, - } + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} asynckit@0.4.0: - resolution: - { - integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, - } + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} axios@1.8.4: - resolution: - { - integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==, - } + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} babel-jest@29.7.0: - resolution: - { - integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 + babel-jest@30.0.1: + resolution: {integrity: sha512-JlqAR53kHcRkLUpxvLYzUdo/Zn5HYPtheVMpSh+JQQppC9TYjkXoEt/PGUT86L3t7lNZLH83Wa+wziYVARYWXQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + babel-plugin-istanbul@6.1.1: - resolution: - { - integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-istanbul@7.0.0: + resolution: {integrity: sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==} + engines: {node: '>=12'} babel-plugin-jest-hoist@29.6.3: - resolution: - { - integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-jest-hoist@30.0.1: + resolution: {integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} babel-plugin-polyfill-corejs2@0.4.13: - resolution: - { - integrity: sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==, - } + resolution: {integrity: sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-polyfill-corejs3@0.11.1: - resolution: - { - integrity: sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==, - } + resolution: {integrity: sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-polyfill-regenerator@0.6.4: - resolution: - { - integrity: sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==, - } + resolution: {integrity: sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-syntax-hermes-parser@0.25.1: - resolution: - { - integrity: sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==, - } + resolution: {integrity: sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==} babel-plugin-transform-flow-enums@0.0.2: - resolution: - { - integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==, - } + resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==} babel-preset-current-node-syntax@1.1.0: - resolution: - { - integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==, - } + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} peerDependencies: '@babel/core': ^7.0.0 babel-preset-jest@29.6.3: - resolution: - { - integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 + babel-preset-jest@30.0.1: + resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + balanced-match@1.0.2: - resolution: - { - integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, - } + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} base64-js@1.5.1: - resolution: - { - integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, - } + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} bl@4.1.0: - resolution: - { - integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==, - } + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} bl@5.1.0: - resolution: - { - integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==, - } + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} blockstore-core@5.0.2: - resolution: - { - integrity: sha512-y7/BHdYLO3YCpJMg6Ue7b4Oz4FT1HWSZoHHdlsaJTsvoE8XieXb6kUCB9UkkUBDw2x4neRDwlgYBpyK77+Ro2Q==, - } + resolution: {integrity: sha512-y7/BHdYLO3YCpJMg6Ue7b4Oz4FT1HWSZoHHdlsaJTsvoE8XieXb6kUCB9UkkUBDw2x4neRDwlgYBpyK77+Ro2Q==} blockstore-fs@2.0.2: - resolution: - { - integrity: sha512-g4l4cJZqcLGPD+iOSb9DYWClAiSSGKsN7V13PTZYqQFHeg96phG15jNi9ql3urrlVC/OTzPB95FXK+GP0TX8Tw==, - } + resolution: {integrity: sha512-g4l4cJZqcLGPD+iOSb9DYWClAiSSGKsN7V13PTZYqQFHeg96phG15jNi9ql3urrlVC/OTzPB95FXK+GP0TX8Tw==} body-parser@2.2.0: - resolution: - { - integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} brace-expansion@1.1.11: - resolution: - { - integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==, - } + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} brace-expansion@2.0.1: - resolution: - { - integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==, - } + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} braces@3.0.3: - resolution: - { - integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} browser-level@1.0.1: - resolution: - { - integrity: sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ==, - } + resolution: {integrity: sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ==} browser-readablestream-to-it@2.0.8: - resolution: - { - integrity: sha512-+aDq+8QoTxIklc9m21oVg96Bm18EpeVke4/8vWPNu+9Ktd+G4PYavitE4gv/pjIndw1q+vxE/Rcnv1zYHrEQbQ==, - } + resolution: {integrity: sha512-+aDq+8QoTxIklc9m21oVg96Bm18EpeVke4/8vWPNu+9Ktd+G4PYavitE4gv/pjIndw1q+vxE/Rcnv1zYHrEQbQ==} browserslist@4.24.4: - resolution: - { - integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==, - } - engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + bser@2.1.1: - resolution: - { - integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==, - } + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} buffer-from@1.1.2: - resolution: - { - integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, - } + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: - resolution: - { - integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==, - } + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} buffer@6.0.3: - resolution: - { - integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, - } + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} bytes@3.1.2: - resolution: - { - integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} call-bind-apply-helpers@1.0.2: - resolution: - { - integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} call-bound@1.0.4: - resolution: - { - integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} caller-callsite@2.0.0: - resolution: - { - integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} + engines: {node: '>=4'} caller-path@2.0.0: - resolution: - { - integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} + engines: {node: '>=4'} callsites@2.0.0: - resolution: - { - integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} + engines: {node: '>=4'} callsites@3.1.0: - resolution: - { - integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} camelcase@5.3.1: - resolution: - { - integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} camelcase@6.3.0: - resolution: - { - integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} caniuse-lite@1.0.30001712: - resolution: - { - integrity: sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==, - } + resolution: {integrity: sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==} catering@2.1.1: - resolution: - { - integrity: sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==} + engines: {node: '>=6'} cborg@4.2.9: - resolution: - { - integrity: sha512-HG8GprGhfzkbzDAIQApqYcN1BJAyf8vDQbzclAwaqrm3ATFnB7ygiWLr+YID+GBdfTJ+yHtzPi06218xULpZrg==, - } + resolution: {integrity: sha512-HG8GprGhfzkbzDAIQApqYcN1BJAyf8vDQbzclAwaqrm3ATFnB7ygiWLr+YID+GBdfTJ+yHtzPi06218xULpZrg==} hasBin: true chalk@4.1.2: - resolution: - { - integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} chalk@5.4.1: - resolution: - { - integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==, - } - engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 } + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} chownr@1.1.4: - resolution: - { - integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==, - } + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} chrome-launcher@0.15.2: - resolution: - { - integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==, - } - engines: { node: '>=12.13.0' } + resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} + engines: {node: '>=12.13.0'} hasBin: true chromium-edge-launcher@0.2.0: - resolution: - { - integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==, - } + resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==} ci-info@2.0.0: - resolution: - { - integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==, - } + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} ci-info@3.9.0: - resolution: - { - integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + ci-info@4.2.0: + resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} + engines: {node: '>=8'} + + cjs-module-lexer@2.1.0: + resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} classic-level@1.4.1: - resolution: - { - integrity: sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==} + engines: {node: '>=12'} cli-cursor@5.0.0: - resolution: - { - integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} cli-truncate@4.0.0: - resolution: - { - integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} cliui@8.0.1: - resolution: - { - integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} clone-deep@4.0.1: - resolution: - { - integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} clone-regexp@3.0.0: - resolution: - { - integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==} + engines: {node: '>=12'} clone@2.1.2: - resolution: - { - integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==, - } - engines: { node: '>=0.8' } + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} color-convert@1.9.3: - resolution: - { - integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==, - } + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} color-convert@2.0.1: - resolution: - { - integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, - } - engines: { node: '>=7.0.0' } + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} color-name@1.1.3: - resolution: - { - integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==, - } + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} color-name@1.1.4: - resolution: - { - integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, - } + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} color-string@1.9.1: - resolution: - { - integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==, - } + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} color@3.2.1: - resolution: - { - integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==, - } + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} colorette@2.0.20: - resolution: - { - integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, - } + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} colorspace@1.1.4: - resolution: - { - integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==, - } + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} combined-stream@1.0.8: - resolution: - { - integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} commander@12.1.0: - resolution: - { - integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} commander@13.1.0: - resolution: - { - integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} commander@2.20.3: - resolution: - { - integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, - } + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} commondir@1.0.1: - resolution: - { - integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==, - } + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} concat-map@0.0.1: - resolution: - { - integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, - } + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} connect@3.7.0: - resolution: - { - integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==, - } - engines: { node: '>= 0.10.0' } + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} content-disposition@1.0.0: - resolution: - { - integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} content-type@1.0.5: - resolution: - { - integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} convert-hrtime@5.0.0: - resolution: - { - integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} + engines: {node: '>=12'} convert-source-map@2.0.0: - resolution: - { - integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, - } + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} cookie-signature@1.2.2: - resolution: - { - integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==, - } - engines: { node: '>=6.6.0' } + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} cookie@0.7.2: - resolution: - { - integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} core-js-compat@3.41.0: - resolution: - { - integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==, - } + resolution: {integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==} cosmiconfig@5.2.1: - resolution: - { - integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} + engines: {node: '>=4'} cross-spawn@7.0.6: - resolution: - { - integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} datastore-core@10.0.2: - resolution: - { - integrity: sha512-B3WXxI54VxJkpXxnYibiF17si3bLXE1XOjrJB7wM5co9fx2KOEkiePDGiCCEtnapFHTnmAnYCPdA7WZTIpdn/A==, - } + resolution: {integrity: sha512-B3WXxI54VxJkpXxnYibiF17si3bLXE1XOjrJB7wM5co9fx2KOEkiePDGiCCEtnapFHTnmAnYCPdA7WZTIpdn/A==} + + datastore-fs@10.0.4: + resolution: {integrity: sha512-zo3smcRFZaeKubtiOwWxzsf04G6384/wbUMJpyryW0i42Ih6hpp2zIbbbSJEa1xeUr/G4fxWGUnLqEcabGqcFg==} debug@2.6.9: - resolution: - { - integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==, - } + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -3022,11 +2325,8 @@ packages: optional: true debug@4.3.4: - resolution: - { - integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==, - } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -3034,11 +2334,8 @@ packages: optional: true debug@4.4.0: - resolution: - { - integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==, - } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -3046,250 +2343,169 @@ packages: optional: true decompress-response@6.0.0: - resolution: - { - integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + dedent@1.6.0: + resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true deep-extend@0.6.0: - resolution: - { - integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==, - } - engines: { node: '>=4.0.0' } + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} deep-is@0.1.4: - resolution: - { - integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, - } + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} delay@6.0.0: - resolution: - { - integrity: sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==} + engines: {node: '>=16'} delayed-stream@1.0.0: - resolution: - { - integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, - } - engines: { node: '>=0.4.0' } + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} denque@2.1.0: - resolution: - { - integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} depd@2.0.0: - resolution: - { - integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} depseek@0.4.1: - resolution: - { - integrity: sha512-YYfPPajzH9s2qnEva411VJzCMWtArBTfluI9USiKQ+T6xBWFh3C7yPxhaa1KVgJa17v9aRKc+LcRhgxS5/9mOA==, - } + resolution: {integrity: sha512-YYfPPajzH9s2qnEva411VJzCMWtArBTfluI9USiKQ+T6xBWFh3C7yPxhaa1KVgJa17v9aRKc+LcRhgxS5/9mOA==} destroy@1.2.0: - resolution: - { - integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==, - } - engines: { node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16 } + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} detect-browser@5.3.0: - resolution: - { - integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==, - } + resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} detect-libc@2.0.3: - resolution: - { - integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} dns-packet@5.6.1: - resolution: - { - integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} dunder-proto@1.0.1: - resolution: - { - integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} eastasianwidth@0.2.0: - resolution: - { - integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, - } + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} ee-first@1.1.1: - resolution: - { - integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, - } + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true electron-to-chromium@1.5.134: - resolution: - { - integrity: sha512-zSwzrLg3jNP3bwsLqWHmS5z2nIOQ5ngMnfMZOWWtXnqqQkPVyOipxK98w+1beLw1TB+EImPNcG8wVP/cLVs2Og==, - } + resolution: {integrity: sha512-zSwzrLg3jNP3bwsLqWHmS5z2nIOQ5ngMnfMZOWWtXnqqQkPVyOipxK98w+1beLw1TB+EImPNcG8wVP/cLVs2Og==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} emoji-regex@10.4.0: - resolution: - { - integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==, - } + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} emoji-regex@8.0.0: - resolution: - { - integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, - } + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: - resolution: - { - integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, - } + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} enabled@2.0.0: - resolution: - { - integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==, - } + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} encodeurl@1.0.2: - resolution: - { - integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} encodeurl@2.0.0: - resolution: - { - integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} end-of-stream@1.4.4: - resolution: - { - integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==, - } + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} environment@1.1.0: - resolution: - { - integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} err-code@3.0.1: - resolution: - { - integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==, - } + resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} error-ex@1.3.2: - resolution: - { - integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==, - } + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} error-stack-parser@2.1.4: - resolution: - { - integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==, - } + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} es-define-property@1.0.1: - resolution: - { - integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} es-errors@1.3.0: - resolution: - { - integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} es-object-atoms@1.1.1: - resolution: - { - integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: - resolution: - { - integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true escalade@3.2.0: - resolution: - { - integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} escape-html@1.0.3: - resolution: - { - integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, - } + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} escape-string-regexp@2.0.0: - resolution: - { - integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} escape-string-regexp@4.0.0: - resolution: - { - integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} eslint-config-prettier@10.1.1: - resolution: - { - integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==, - } + resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==} hasBin: true peerDependencies: eslint: '>=7.0.0' eslint-plugin-prettier@5.2.6: - resolution: - { - integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==, - } - engines: { node: ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==} + engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' eslint: '>=8.0.0' @@ -3302,32 +2518,20 @@ packages: optional: true eslint-scope@8.3.0: - resolution: - { - integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: - resolution: - { - integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} eslint-visitor-keys@4.2.0: - resolution: - { - integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint@9.24.0: - resolution: - { - integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: jiti: '*' @@ -3336,251 +2540,155 @@ packages: optional: true espree@10.3.0: - resolution: - { - integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: - resolution: - { - integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} hasBin: true esquery@1.6.0: - resolution: - { - integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} esrecurse@4.3.0: - resolution: - { - integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, - } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} estraverse@5.3.0: - resolution: - { - integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, - } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} esutils@2.0.3: - resolution: - { - integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} etag@1.8.1: - resolution: - { - integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} event-iterator@2.0.0: - resolution: - { - integrity: sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ==, - } + resolution: {integrity: sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ==} event-target-shim@5.0.1: - resolution: - { - integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} event-target-shim@6.0.2: - resolution: - { - integrity: sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==} + engines: {node: '>=10.13.0'} eventemitter3@5.0.1: - resolution: - { - integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==, - } + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} execa@8.0.1: - resolution: - { - integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==, - } - engines: { node: '>=16.17' } + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} expand-template@2.0.3: - resolution: - { - integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect@30.0.1: + resolution: {integrity: sha512-FLzSqyMY397aV5awKVGWOKrfrzQRxoGAofdTt9ucJ6dSVY+1c6yEfcw/JZ1oqfLnL78FONo9GfVaEb8VJ5irGw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} exponential-backoff@3.1.2: - resolution: - { - integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==, - } + resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} express@5.1.0: - resolution: - { - integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==, - } - engines: { node: '>= 18' } + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} fast-deep-equal@3.1.3: - resolution: - { - integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, - } + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} fast-diff@1.3.0: - resolution: - { - integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==, - } + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} fast-glob@3.3.3: - resolution: - { - integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, - } - engines: { node: '>=8.6.0' } + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} fast-json-stable-stringify@2.1.0: - resolution: - { - integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, - } + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: - resolution: - { - integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, - } + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} fastq@1.19.1: - resolution: - { - integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==, - } + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} fb-watchman@2.0.2: - resolution: - { - integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==, - } + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} fecha@4.2.3: - resolution: - { - integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==, - } + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} file-entry-cache@8.0.0: - resolution: - { - integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, - } - engines: { node: '>=16.0.0' } + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} fill-range@7.1.1: - resolution: - { - integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} finalhandler@1.1.2: - resolution: - { - integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} finalhandler@2.1.0: - resolution: - { - integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} find-cache-dir@2.1.0: - resolution: - { - integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} find-up@3.0.0: - resolution: - { - integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} find-up@4.1.0: - resolution: - { - integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} find-up@5.0.0: - resolution: - { - integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} flat-cache@4.0.1: - resolution: - { - integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.3.3: - resolution: - { - integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==, - } + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} flow-enums-runtime@0.0.6: - resolution: - { - integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==, - } + resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} flow-parser@0.266.1: - resolution: - { - integrity: sha512-dON6h+yO7FGa/FO5NQCZuZHN0o3I23Ev6VYOJf9d8LpdrArHPt39wE++LLmueNV/hNY5hgWGIIrgnrDkRcXkPg==, - } - engines: { node: '>=0.4.0' } + resolution: {integrity: sha512-dON6h+yO7FGa/FO5NQCZuZHN0o3I23Ev6VYOJf9d8LpdrArHPt39wE++LLmueNV/hNY5hgWGIIrgnrDkRcXkPg==} + engines: {node: '>=0.4.0'} fn.name@1.1.0: - resolution: - { - integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==, - } + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} follow-redirects@1.15.9: - resolution: - { - integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==, - } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} peerDependencies: debug: '*' peerDependenciesMeta: @@ -3588,287 +2696,171 @@ packages: optional: true foreground-child@3.3.1: - resolution: - { - integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} form-data@4.0.2: - resolution: - { - integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} forwarded@0.2.0: - resolution: - { - integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} freeport-promise@2.0.0: - resolution: - { - integrity: sha512-dwWpT1DdQcwrhmRwnDnPM/ZFny+FtzU+k50qF2eid3KxaQDsMiBrwo1i0G3qSugkN5db6Cb0zgfc68QeTOpEFg==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-dwWpT1DdQcwrhmRwnDnPM/ZFny+FtzU+k50qF2eid3KxaQDsMiBrwo1i0G3qSugkN5db6Cb0zgfc68QeTOpEFg==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} fresh@0.5.2: - resolution: - { - integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} fresh@2.0.0: - resolution: - { - integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} fs-constants@1.0.0: - resolution: - { - integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==, - } + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} fs-extra@11.3.0: - resolution: - { - integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==, - } - engines: { node: '>=14.14' } + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} fs.realpath@1.0.0: - resolution: - { - integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, - } + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] function-bind@1.1.2: - resolution: - { - integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, - } + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} function-timeout@0.1.1: - resolution: - { - integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==, - } - engines: { node: '>=14.16' } + resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==} + engines: {node: '>=14.16'} gensync@1.0.0-beta.2: - resolution: - { - integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} get-caller-file@2.0.5: - resolution: - { - integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, - } - engines: { node: 6.* || 8.* || >= 10.* } + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} get-east-asian-width@1.3.0: - resolution: - { - integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} get-intrinsic@1.3.0: - resolution: - { - integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} get-iterator@2.0.1: - resolution: - { - integrity: sha512-7HuY/hebu4gryTDT7O/XY/fvY9wRByEGdK6QOa4of8npTcv0+NS6frFKABcf6S9EBAsveTuKTsZQQBFMMNILIg==, - } + resolution: {integrity: sha512-7HuY/hebu4gryTDT7O/XY/fvY9wRByEGdK6QOa4of8npTcv0+NS6frFKABcf6S9EBAsveTuKTsZQQBFMMNILIg==} get-package-type@0.1.0: - resolution: - { - integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} get-port@7.1.0: - resolution: - { - integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} get-proto@1.0.1: - resolution: - { - integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} get-stream@8.0.1: - resolution: - { - integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} github-from-package@0.0.0: - resolution: - { - integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==, - } + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} glob-parent@5.1.2: - resolution: - { - integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} glob-parent@6.0.2: - resolution: - { - integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} glob@10.4.5: - resolution: - { - integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==, - } + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true glob@7.2.3: - resolution: - { - integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, - } + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported globals@11.12.0: - resolution: - { - integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} globals@14.0.0: - resolution: - { - integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} globals@16.0.0: - resolution: - { - integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} + engines: {node: '>=18'} gopd@1.2.0: - resolution: - { - integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} graceful-fs@4.2.11: - resolution: - { - integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, - } + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} graphemer@1.4.0: - resolution: - { - integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==, - } + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} hamt-sharding@3.0.6: - resolution: - { - integrity: sha512-nZeamxfymIWLpVcAN0CRrb7uVq3hCOGj9IcL6NMA6VVCVWqj+h9Jo/SmaWuS92AEDf1thmHsM5D5c70hM3j2Tg==, - } + resolution: {integrity: sha512-nZeamxfymIWLpVcAN0CRrb7uVq3hCOGj9IcL6NMA6VVCVWqj+h9Jo/SmaWuS92AEDf1thmHsM5D5c70hM3j2Tg==} has-flag@4.0.0: - resolution: - { - integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} has-symbols@1.1.0: - resolution: - { - integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} has-tostringtag@1.0.2: - resolution: - { - integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} hashlru@2.3.0: - resolution: - { - integrity: sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==, - } + resolution: {integrity: sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==} hasown@2.0.2: - resolution: - { - integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} helia@5.3.0: - resolution: - { - integrity: sha512-mSM/zQqdoQWUicf90NEcVO1MPSjrzPro5vMe90cKdU0mv10BDk6aJDEImgQlN2x7EsmHCf+hOgmA9K3gZizK4w==, - } + resolution: {integrity: sha512-mSM/zQqdoQWUicf90NEcVO1MPSjrzPro5vMe90cKdU0mv10BDk6aJDEImgQlN2x7EsmHCf+hOgmA9K3gZizK4w==} hermes-estree@0.25.1: - resolution: - { - integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==, - } + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: - resolution: - { - integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==, - } + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} http-cookie-agent@6.0.8: - resolution: - { - integrity: sha512-qnYh3yLSr2jBsTYkw11elq+T361uKAJaZ2dR4cfYZChw1dt9uL5t3zSUwehoqqVb4oldk1BpkXKm2oat8zV+oA==, - } - engines: { node: '>=18.0.0' } + resolution: {integrity: sha512-qnYh3yLSr2jBsTYkw11elq+T361uKAJaZ2dR4cfYZChw1dt9uL5t3zSUwehoqqVb4oldk1BpkXKm2oat8zV+oA==} + engines: {node: '>=18.0.0'} peerDependencies: tough-cookie: ^4.0.0 || ^5.0.0 undici: ^5.11.0 || ^6.0.0 @@ -3877,628 +2869,514 @@ packages: optional: true http-errors@2.0.0: - resolution: - { - integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} human-signals@5.0.0: - resolution: - { - integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==, - } - engines: { node: '>=16.17.0' } + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} husky@8.0.3: - resolution: - { - integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} + engines: {node: '>=14'} hasBin: true iconv-lite@0.6.3: - resolution: - { - integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} ieee754@1.2.1: - resolution: - { - integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, - } + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} ignore@5.3.2: - resolution: - { - integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} image-size@1.2.1: - resolution: - { - integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==, - } - engines: { node: '>=16.x' } + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} hasBin: true import-fresh@2.0.0: - resolution: - { - integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} + engines: {node: '>=4'} import-fresh@3.3.1: - resolution: - { - integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true imurmurhash@0.1.4: - resolution: - { - integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, - } - engines: { node: '>=0.8.19' } + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} inflight@1.0.6: - resolution: - { - integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, - } + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: - resolution: - { - integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, - } + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: - resolution: - { - integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==, - } + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} interface-blockstore@5.3.1: - resolution: - { - integrity: sha512-nhgrQnz6yUQEqxTFLhlOBurQOy5lWlwCpgFmZ3GTObTVTQS9RZjK/JTozY6ty9uz2lZs7VFJSqwjWAltorJ4Vw==, - } + resolution: {integrity: sha512-nhgrQnz6yUQEqxTFLhlOBurQOy5lWlwCpgFmZ3GTObTVTQS9RZjK/JTozY6ty9uz2lZs7VFJSqwjWAltorJ4Vw==} interface-datastore@8.3.1: - resolution: - { - integrity: sha512-3r0ETmHIi6HmvM5sc09QQiCD3gUfwtEM/AAChOyAd/UAKT69uk8LXfTSUBufbUIO/dU65Vj8nb9O6QjwW8vDSQ==, - } + resolution: {integrity: sha512-3r0ETmHIi6HmvM5sc09QQiCD3gUfwtEM/AAChOyAd/UAKT69uk8LXfTSUBufbUIO/dU65Vj8nb9O6QjwW8vDSQ==} interface-store@6.0.2: - resolution: - { - integrity: sha512-KSFCXtBlNoG0hzwNa0RmhHtrdhzexp+S+UY2s0rWTBJyfdEIgn6i6Zl9otVqrcFYbYrneBT7hbmHQ8gE0C3umA==, - } + resolution: {integrity: sha512-KSFCXtBlNoG0hzwNa0RmhHtrdhzexp+S+UY2s0rWTBJyfdEIgn6i6Zl9otVqrcFYbYrneBT7hbmHQ8gE0C3umA==} invariant@2.2.4: - resolution: - { - integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==, - } + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} ip-regex@5.0.0: - resolution: - { - integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} ipaddr.js@1.9.1: - resolution: - { - integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, - } - engines: { node: '>= 0.10' } + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} ipfs-unixfs-exporter@13.6.2: - resolution: - { - integrity: sha512-U3NkQHvQn5XzxtjSo1/GfoFIoXYY4hPgOlZG5RUrV5ScBI222b3jAHbHksXZuMy7sqPkA9ieeWdOmnG1+0nxyw==, - } + resolution: {integrity: sha512-U3NkQHvQn5XzxtjSo1/GfoFIoXYY4hPgOlZG5RUrV5ScBI222b3jAHbHksXZuMy7sqPkA9ieeWdOmnG1+0nxyw==} ipfs-unixfs-importer@15.3.2: - resolution: - { - integrity: sha512-12FqAAAE3YC6AHtYxZ944nDCabmvbNLdhNCVIN5RJIOri82ss62XdX4lsLpex9VvPzDIJyTAsrKJPcwM6hXGdQ==, - } + resolution: {integrity: sha512-12FqAAAE3YC6AHtYxZ944nDCabmvbNLdhNCVIN5RJIOri82ss62XdX4lsLpex9VvPzDIJyTAsrKJPcwM6hXGdQ==} ipfs-unixfs@11.2.1: - resolution: - { - integrity: sha512-gUeeX63EFgiaMgcs0cUs2ZUPvlOeEZ38okjK8twdWGZX2jYd2rCk8k/TJ3DSRIDZ2t/aZMv6I23guxHaofZE3w==, - } + resolution: {integrity: sha512-gUeeX63EFgiaMgcs0cUs2ZUPvlOeEZ38okjK8twdWGZX2jYd2rCk8k/TJ3DSRIDZ2t/aZMv6I23guxHaofZE3w==} ipns@10.0.2: - resolution: - { - integrity: sha512-tokCgz9X678zvHnAabVG91K64X7HnHdWOrop0ghUcXkzH5XNsmxHwVpqVATNqq/w62h7fRDhWURHU/WOfYmCpA==, - } + resolution: {integrity: sha512-tokCgz9X678zvHnAabVG91K64X7HnHdWOrop0ghUcXkzH5XNsmxHwVpqVATNqq/w62h7fRDhWURHU/WOfYmCpA==} is-arrayish@0.2.1: - resolution: - { - integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==, - } + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-arrayish@0.3.2: - resolution: - { - integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==, - } + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} is-buffer@2.0.5: - resolution: - { - integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} is-core-module@2.16.1: - resolution: - { - integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} is-directory@0.3.1: - resolution: - { - integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} + engines: {node: '>=0.10.0'} is-docker@2.2.1: - resolution: - { - integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} hasBin: true is-electron@2.2.2: - resolution: - { - integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==, - } + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} is-extglob@2.1.1: - resolution: - { - integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} is-fullwidth-code-point@3.0.0: - resolution: - { - integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} is-fullwidth-code-point@4.0.0: - resolution: - { - integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} is-fullwidth-code-point@5.0.0: - resolution: - { - integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} is-glob@4.0.3: - resolution: - { - integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} is-ip@5.0.1: - resolution: - { - integrity: sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==, - } - engines: { node: '>=14.16' } + resolution: {integrity: sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==} + engines: {node: '>=14.16'} is-loopback-addr@2.0.2: - resolution: - { - integrity: sha512-26POf2KRCno/KTNL5Q0b/9TYnL00xEsSaLfiFRmjM7m7Lw7ZMmFybzzuX4CcsLAluZGd+niLUiMRxEooVE3aqg==, - } + resolution: {integrity: sha512-26POf2KRCno/KTNL5Q0b/9TYnL00xEsSaLfiFRmjM7m7Lw7ZMmFybzzuX4CcsLAluZGd+niLUiMRxEooVE3aqg==} is-network-error@1.1.0: - resolution: - { - integrity: sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==} + engines: {node: '>=16'} is-number@7.0.0: - resolution: - { - integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, - } - engines: { node: '>=0.12.0' } + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} is-plain-obj@2.1.0: - resolution: - { - integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} is-plain-object@2.0.4: - resolution: - { - integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} is-promise@4.0.0: - resolution: - { - integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==, - } + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} is-regexp@3.1.0: - resolution: - { - integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} is-stream@2.0.1: - resolution: - { - integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} is-stream@3.0.0: - resolution: - { - integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} is-wsl@2.2.0: - resolution: - { - integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} isexe@2.0.0: - resolution: - { - integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, - } + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} isobject@3.0.1: - resolution: - { - integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} istanbul-lib-coverage@3.2.2: - resolution: - { - integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} istanbul-lib-instrument@5.2.1: - resolution: - { - integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} it-all@3.0.7: - resolution: - { - integrity: sha512-PkuYtu6XhJzuPTKXImd6y0qE6H91MUPV/b9xotXMAI6GjmD2v3NoHj2g5L0lS2qZ0EzyGWZU1kp0UxW8POvNBQ==, - } + resolution: {integrity: sha512-PkuYtu6XhJzuPTKXImd6y0qE6H91MUPV/b9xotXMAI6GjmD2v3NoHj2g5L0lS2qZ0EzyGWZU1kp0UxW8POvNBQ==} it-batch@3.0.7: - resolution: - { - integrity: sha512-tcAW8+OAnhC3WqO5ggInfndL/jJsL3i++JLBADKs7LSSzfVVOXicufAuY5Sv4RbCkulRuk/ClSZhS0fu9B9SJA==, - } + resolution: {integrity: sha512-tcAW8+OAnhC3WqO5ggInfndL/jJsL3i++JLBADKs7LSSzfVVOXicufAuY5Sv4RbCkulRuk/ClSZhS0fu9B9SJA==} it-byte-stream@1.1.1: - resolution: - { - integrity: sha512-OIOb8PvK9ZV7MHvyxIDNyN3jmrxrJdx99G0RIYYb3Tzo1OWv+O1C6mfg7nnlDuuTQz2POYFXe87AShKAEl+POw==, - } + resolution: {integrity: sha512-OIOb8PvK9ZV7MHvyxIDNyN3jmrxrJdx99G0RIYYb3Tzo1OWv+O1C6mfg7nnlDuuTQz2POYFXe87AShKAEl+POw==} it-drain@3.0.8: - resolution: - { - integrity: sha512-eeOz+WwKc11ou1UuqZympcXPLCjpTn5ALcYFJiHeTEiYEZ2py/J1vq41XWYj88huCUiqp9iNHfObOKrbIk5Izw==, - } + resolution: {integrity: sha512-eeOz+WwKc11ou1UuqZympcXPLCjpTn5ALcYFJiHeTEiYEZ2py/J1vq41XWYj88huCUiqp9iNHfObOKrbIk5Izw==} it-filter@3.1.2: - resolution: - { - integrity: sha512-2AozaGjIvBBiB7t7MpVNug9kwofqmKSpvgW7zhuyvCs6xxDd6FrfvqyfYtlQTKLNP+Io1WeXko1UQhdlK4M0gg==, - } + resolution: {integrity: sha512-2AozaGjIvBBiB7t7MpVNug9kwofqmKSpvgW7zhuyvCs6xxDd6FrfvqyfYtlQTKLNP+Io1WeXko1UQhdlK4M0gg==} it-first@3.0.7: - resolution: - { - integrity: sha512-e2dVSlOP+pAxPYPVJBF4fX7au8cvGfvLhIrGCMc5aWDnCvwgOo94xHbi3Da6eXQ2jPL5FGEM8sJMn5uE8Seu+g==, - } + resolution: {integrity: sha512-e2dVSlOP+pAxPYPVJBF4fX7au8cvGfvLhIrGCMc5aWDnCvwgOo94xHbi3Da6eXQ2jPL5FGEM8sJMn5uE8Seu+g==} it-foreach@2.1.2: - resolution: - { - integrity: sha512-PvXs3v1FaeWDhWzRxnwB4vSKJngxdLgi0PddkfurCvIFBmKTBfWONLeyDk5dxrvtCzdE4y96KzEQynk4/bbI5A==, - } + resolution: {integrity: sha512-PvXs3v1FaeWDhWzRxnwB4vSKJngxdLgi0PddkfurCvIFBmKTBfWONLeyDk5dxrvtCzdE4y96KzEQynk4/bbI5A==} it-glob@3.0.2: - resolution: - { - integrity: sha512-yw6am0buc9W6HThDhlf/0k9LpwK31p9Y3c0hpaoth9Iaha4Kog2oRlVanLGSrPPoh9yGwHJbs+KfBJt020N6/g==, - } + resolution: {integrity: sha512-yw6am0buc9W6HThDhlf/0k9LpwK31p9Y3c0hpaoth9Iaha4Kog2oRlVanLGSrPPoh9yGwHJbs+KfBJt020N6/g==} + + it-glob@3.0.4: + resolution: {integrity: sha512-73PbGBTK/dHp5PX4l8pkQH1ozCONP0U+PB3qMqltxPonRJQNomINE3Hn9p02m2GOu95VoeVvSZdHI2N+qub0pw==} it-last@3.0.7: - resolution: - { - integrity: sha512-qG4BTveE6Wzsz5voqaOtZAfZgXTJT+yiaj45vp5S0Vi8oOdgKlRqUeolfvWoMCJ9vwSc/z9pAaNYIza7gA851w==, - } + resolution: {integrity: sha512-qG4BTveE6Wzsz5voqaOtZAfZgXTJT+yiaj45vp5S0Vi8oOdgKlRqUeolfvWoMCJ9vwSc/z9pAaNYIza7gA851w==} it-length-prefixed-stream@1.2.1: - resolution: - { - integrity: sha512-FYqlxc2toUoK+aPO5r3KDBIUG1mOvk2DzmjQcsfLUTHRWMJP4Va9855tVzg/22Bj+VUUaT7gxBg7HmbiCxTK4w==, - } + resolution: {integrity: sha512-FYqlxc2toUoK+aPO5r3KDBIUG1mOvk2DzmjQcsfLUTHRWMJP4Va9855tVzg/22Bj+VUUaT7gxBg7HmbiCxTK4w==} it-length-prefixed@10.0.1: - resolution: - { - integrity: sha512-BhyluvGps26u9a7eQIpOI1YN7mFgi8lFwmiPi07whewbBARKAG9LE09Odc8s1Wtbt2MB6rNUrl7j9vvfXTJwdQ==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-BhyluvGps26u9a7eQIpOI1YN7mFgi8lFwmiPi07whewbBARKAG9LE09Odc8s1Wtbt2MB6rNUrl7j9vvfXTJwdQ==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} it-length-prefixed@9.1.1: - resolution: - { - integrity: sha512-O88nBweT6M9ozsmok68/auKH7ik/slNM4pYbM9lrfy2z5QnpokW5SlrepHZDKtN71llhG2sZvd6uY4SAl+lAQg==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-O88nBweT6M9ozsmok68/auKH7ik/slNM4pYbM9lrfy2z5QnpokW5SlrepHZDKtN71llhG2sZvd6uY4SAl+lAQg==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} it-length@3.0.7: - resolution: - { - integrity: sha512-URrszwrzPrUn6PtsSFcixG4NwHydaARmPubO0UUnFH+NSNylBaGtair1fnxX7Zf2qVJQltPBVs3PZvcmFPTLXA==, - } + resolution: {integrity: sha512-URrszwrzPrUn6PtsSFcixG4NwHydaARmPubO0UUnFH+NSNylBaGtair1fnxX7Zf2qVJQltPBVs3PZvcmFPTLXA==} it-map@3.1.2: - resolution: - { - integrity: sha512-G3dzFUjTYHKumJJ8wa9dSDS3yKm8L7qDUnAgzemOD0UMztwm54Qc2v97SuUCiAgbOz/aibkSLImfoFK09RlSFQ==, - } + resolution: {integrity: sha512-G3dzFUjTYHKumJJ8wa9dSDS3yKm8L7qDUnAgzemOD0UMztwm54Qc2v97SuUCiAgbOz/aibkSLImfoFK09RlSFQ==} + + it-map@3.1.4: + resolution: {integrity: sha512-QB9PYQdE9fUfpVFYfSxBIyvKynUCgblb143c+ktTK6ZuKSKkp7iH58uYFzagqcJ5HcqIfn1xbfaralHWam+3fg==} it-merge@3.0.9: - resolution: - { - integrity: sha512-TjY4WTiwe4ONmaKScNvHDAJj6Tw0UeQFp4JrtC/3Mq7DTyhytes7mnv5OpZV4gItpZcs0AgRntpT2vAy2cnXUw==, - } + resolution: {integrity: sha512-TjY4WTiwe4ONmaKScNvHDAJj6Tw0UeQFp4JrtC/3Mq7DTyhytes7mnv5OpZV4gItpZcs0AgRntpT2vAy2cnXUw==} it-ndjson@1.1.2: - resolution: - { - integrity: sha512-TPKpdYSNKjDdroCPnLamM5Up6XnPQ7F1KgNP3Ib5y5O4ayOVP+DHac/pzjUigcg9Kf9gkGVXDz8+FFKpWwoB3w==, - } + resolution: {integrity: sha512-TPKpdYSNKjDdroCPnLamM5Up6XnPQ7F1KgNP3Ib5y5O4ayOVP+DHac/pzjUigcg9Kf9gkGVXDz8+FFKpWwoB3w==} it-pair@2.0.6: - resolution: - { - integrity: sha512-5M0t5RAcYEQYNG5BV7d7cqbdwbCAp5yLdzvkxsZmkuZsLbTdZzah6MQySYfaAQjNDCq6PUnDt0hqBZ4NwMfW6g==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-5M0t5RAcYEQYNG5BV7d7cqbdwbCAp5yLdzvkxsZmkuZsLbTdZzah6MQySYfaAQjNDCq6PUnDt0hqBZ4NwMfW6g==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} it-parallel-batch@3.0.7: - resolution: - { - integrity: sha512-R/YKQMefUwLYfJ2UxMaxQUf+Zu9TM+X1KFDe4UaSQlcNog6AbMNMBt3w1suvLEjDDMrI9FNrlopVumfBIboeOg==, - } + resolution: {integrity: sha512-R/YKQMefUwLYfJ2UxMaxQUf+Zu9TM+X1KFDe4UaSQlcNog6AbMNMBt3w1suvLEjDDMrI9FNrlopVumfBIboeOg==} + + it-parallel-batch@3.0.9: + resolution: {integrity: sha512-TszXWqqLG8IG5DUEnC4cgH9aZI6CsGS7sdkXTiiacMIj913bFy7+ohU3IqsFURCcZkpnXtNLNzrYnXISsKBhbQ==} it-parallel@3.0.9: - resolution: - { - integrity: sha512-FSg8T+pr7Z1VUuBxEzAAp/K1j8r1e9mOcyzpWMxN3mt33WFhroFjWXV1oYSSjNqcdYwxD/XgydMVMktJvKiDog==, - } + resolution: {integrity: sha512-FSg8T+pr7Z1VUuBxEzAAp/K1j8r1e9mOcyzpWMxN3mt33WFhroFjWXV1oYSSjNqcdYwxD/XgydMVMktJvKiDog==} it-peekable@3.0.6: - resolution: - { - integrity: sha512-odk9wn8AwFQipy8+tFaZNRCM62riraKZJRysfbmOett9wgJumCwgZFzWUBUwMoiQapEcEVGwjDpMChZIi+zLuQ==, - } + resolution: {integrity: sha512-odk9wn8AwFQipy8+tFaZNRCM62riraKZJRysfbmOett9wgJumCwgZFzWUBUwMoiQapEcEVGwjDpMChZIi+zLuQ==} it-pipe@3.0.1: - resolution: - { - integrity: sha512-sIoNrQl1qSRg2seYSBH/3QxWhJFn9PKYvOf/bHdtCBF0bnghey44VyASsWzn5dAx0DCDDABq1hZIuzKmtBZmKA==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-sIoNrQl1qSRg2seYSBH/3QxWhJFn9PKYvOf/bHdtCBF0bnghey44VyASsWzn5dAx0DCDDABq1hZIuzKmtBZmKA==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} it-protobuf-stream@1.1.6: - resolution: - { - integrity: sha512-TxqgDHXTBt1XkYhrGKP8ubNsYD4zuTClSg6S1M0xTPsskGKA4nPFOGM60zrkh4NMB1Wt3EnsqM5U7kXkx60EXQ==, - } + resolution: {integrity: sha512-TxqgDHXTBt1XkYhrGKP8ubNsYD4zuTClSg6S1M0xTPsskGKA4nPFOGM60zrkh4NMB1Wt3EnsqM5U7kXkx60EXQ==} it-pushable@3.2.3: - resolution: - { - integrity: sha512-gzYnXYK8Y5t5b/BnJUr7glfQLO4U5vyb05gPx/TyTw+4Bv1zM9gFk4YsOrnulWefMewlphCjKkakFvj1y99Tcg==, - } + resolution: {integrity: sha512-gzYnXYK8Y5t5b/BnJUr7glfQLO4U5vyb05gPx/TyTw+4Bv1zM9gFk4YsOrnulWefMewlphCjKkakFvj1y99Tcg==} it-queueless-pushable@1.0.2: - resolution: - { - integrity: sha512-BFIm48C4O8+i+oVEPQpZ70+CaAsVUircvZtZCrpG2Q64933aLp+tDmas1mTBwqVBfIUUlg09d+e6SWW1CBuykQ==, - } + resolution: {integrity: sha512-BFIm48C4O8+i+oVEPQpZ70+CaAsVUircvZtZCrpG2Q64933aLp+tDmas1mTBwqVBfIUUlg09d+e6SWW1CBuykQ==} it-queueless-pushable@2.0.0: - resolution: - { - integrity: sha512-MlNnefWT/ntv5fesrHpxwVIu6ZdtlkN0A4aaJiE5wnmPMBv9ttiwX3UEMf78dFwIj5ZNaU9usYXg4swMEpUNJQ==, - } + resolution: {integrity: sha512-MlNnefWT/ntv5fesrHpxwVIu6ZdtlkN0A4aaJiE5wnmPMBv9ttiwX3UEMf78dFwIj5ZNaU9usYXg4swMEpUNJQ==} it-reader@6.0.4: - resolution: - { - integrity: sha512-XCWifEcNFFjjBHtor4Sfaj8rcpt+FkY0L6WdhD578SCDhV4VUm7fCkF3dv5a+fTcfQqvN9BsxBTvWbYO6iCjTg==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-XCWifEcNFFjjBHtor4Sfaj8rcpt+FkY0L6WdhD578SCDhV4VUm7fCkF3dv5a+fTcfQqvN9BsxBTvWbYO6iCjTg==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} it-sort@3.0.7: - resolution: - { - integrity: sha512-PsaKSd2Z0uhq8Mq5htdfsE/UagmdLCLWdBXPwi3FZGR4BTG180pFamhK+O+luFtBCNGRoqKAdtbZGTyGwA9uzw==, - } + resolution: {integrity: sha512-PsaKSd2Z0uhq8Mq5htdfsE/UagmdLCLWdBXPwi3FZGR4BTG180pFamhK+O+luFtBCNGRoqKAdtbZGTyGwA9uzw==} it-stream-types@2.0.2: - resolution: - { - integrity: sha512-Rz/DEZ6Byn/r9+/SBCuJhpPATDF9D+dz5pbgSUyBsCDtza6wtNATrz/jz1gDyNanC3XdLboriHnOC925bZRBww==, - } + resolution: {integrity: sha512-Rz/DEZ6Byn/r9+/SBCuJhpPATDF9D+dz5pbgSUyBsCDtza6wtNATrz/jz1gDyNanC3XdLboriHnOC925bZRBww==} it-take@3.0.7: - resolution: - { - integrity: sha512-0+EbsTvH1XCpwhhFkjWdqJTjzS5XP3KL69woBqwANNhMLKn0j39jk/WHIlvbg9XW2vEm7cZz4p8w5DkBZR8LoA==, - } + resolution: {integrity: sha512-0+EbsTvH1XCpwhhFkjWdqJTjzS5XP3KL69woBqwANNhMLKn0j39jk/WHIlvbg9XW2vEm7cZz4p8w5DkBZR8LoA==} it-ws@6.1.5: - resolution: - { - integrity: sha512-uWjMtpy5HqhSd/LlrlP3fhYrr7rUfJFFMABv0F5d6n13Q+0glhZthwUKpEAVhDrXY95Tb1RB5lLqqef+QbVNaw==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-uWjMtpy5HqhSd/LlrlP3fhYrr7rUfJFFMABv0F5d6n13Q+0glhZthwUKpEAVhDrXY95Tb1RB5lLqqef+QbVNaw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} jackspeak@3.4.3: - resolution: - { - integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, - } + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jest-changed-files@30.0.1: + resolution: {integrity: sha512-5F/T4oaUdWPE6Ttms/hq5M4YVJA1+s1lWqmUK27xfnj1MBy6HmtnRpXXD2KuKZbD5ntwCxTDVAaRrDyIh+HkBg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.0.1: + resolution: {integrity: sha512-gJl83BUlAgtIx7UkLjIbsTwuQI+PE/959AE+/NbJaUuAgh23LGXWAGQqLdIlXU6TvLEEAmDR4caEI6pfW2PGBg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.0.1: + resolution: {integrity: sha512-jULGjC6PV7vA7oB2gFh3h6lZBWo0XvGnLA9d9Ct2PyM7hmr7DTApStl3beqR0aglUIxCOTHIwmQsnWlbJbGCtg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.0.1: + resolution: {integrity: sha512-5BGh/41Pe1p/aWj9HlEEjbi5JzTFZXYAszGS1cw19//jaPr4Usb16qPGkznzyJLL8ud/7jCplbmF7msTkzqYoA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.0.1: + resolution: {integrity: sha512-9uJGfS2tBBFTvn3ZjfPjrw0r7KtAcutTMs3k39+ur2xD0/MTdmz8SrTzuy1dMlGxmbSet1k79UFSJ2+U7dNEvQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.0.1: + resolution: {integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.0.1: + resolution: {integrity: sha512-zQIKhGrSq6NudJ6SKUBv7wsgRZ3iVe9TXfJ0UNWmrAxaFlsxyVDVq5WkTTWVvCCTCs99fy0s3y62Jx7lLHVJPg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-environment-node@29.7.0: - resolution: - { - integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@30.0.1: + resolution: {integrity: sha512-3MnzhHa1pGH8NgkYp0AjBqFplAW2LECRSpNjM4iA4MBbnyuMf0sBiZG7pzd66smSgilF7hnJr3qVLnlHRsRdIA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-get-type@29.6.3: - resolution: - { - integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} jest-haste-map@29.7.0: - resolution: - { - integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@30.0.1: + resolution: {integrity: sha512-NnvtwP+HmTZQ5blCTjigGlmqHktvGSXk8fqh9qvtbPI04CXX9Qf3hEE8FjtAZiSAkPgYZopZm8jTezvXNStDGA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.0.1: + resolution: {integrity: sha512-67NTiVwvaI5K35oEy2Z3Xo6z4WIzSgcw08AEUXTcgNxhu8D8A7jOol/9YqA6ZJMVXC0QttsU7fxMOJYee08n0A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.0.1: + resolution: {integrity: sha512-4R9ct2D3kZTtRTjPVqWbuQpRgG4lVQ5ifI+Ni52OhEeT4XWnNaPe0AtixpkueMKUJDdh96r6xE7V1+imN2hhHQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-message-util@29.7.0: - resolution: - { - integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@30.0.1: + resolution: {integrity: sha512-/TZhT/tMqBVHhOOYY/VdCBoFN66f7rTAQ0TTh4igilDDd6y0SRP8OW7Fm+IV5bYW8MmdEstDQMZkBivmzDPy8A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-mock@29.7.0: - resolution: - { - integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@30.0.1: + resolution: {integrity: sha512-t57+MErWxWWCrhy4JyQHkgELFHv83u9MqO4XVNP9qAsrknDeX031hG1dEPPwDx77obsciQjXptN2nq1Y83T3CQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true jest-regex-util@29.6.3: - resolution: - { - integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.0.1: + resolution: {integrity: sha512-9lTOL/lsSs1o39/urF1J7eiN+w432Hf2EBVH6V6bzDoxJcr0juRJoWNH0fwDkF/725IjyU5JDEzUUZ/MATXzNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.0.1: + resolution: {integrity: sha512-VWbbfmQVqEjwRZKo/UgBdUE8RbPCZMEDeR3KLLZe+GaGeCmyUraTdSdfDa8WfmyK/JSHxF/zM7OtGoBr5KXiMw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.0.1: + resolution: {integrity: sha512-ntEAnH2AtpAi34j/5mEJTczXMjpVnw5jOKParWM0A0POrelfzJT+WEucIQWIonwlHo96T42B3lHzEUggZfaDNw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.0.1: + resolution: {integrity: sha512-lseQgeKgA9B2BYbGQUrd/XF22wB/Sic6MOCLz7VZ2M159Etzl3dO337foInA68f+f2exmmK0cDxq1lbMToBIVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.0.1: + resolution: {integrity: sha512-Ap2g2X9dkA9Dd9a79DIBkAsE7jsMBydT/xjNGfj8V5ng1kuxpPTqOYHAlHjBZM+cppmCzHSbWn89BVQ9Qh9ibw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-util@29.7.0: - resolution: - { - integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@30.0.1: + resolution: {integrity: sha512-yKUK3Pq+9NtL2XbGhMW0O5PnHYPjvu3kpplm3j08fyqH6lsa/wLg1SCcNJAI4p8LTtfUMj71MnF3L4PKrlIcJg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-validate@29.7.0: - resolution: - { - integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@30.0.1: + resolution: {integrity: sha512-Wy5a3L0wNncZiVeEe8g0uL9ZkHqjXBuDYzl4+SVQ9y5VShSpSi+INSfWipDRX57EG0KCa4k+1N1qAj1s+gDBdg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.0.1: + resolution: {integrity: sha512-TZUy0f9VypPGse7ObbKyfUo7fhVtzLmmDhX84dv4KMvu2j27Nj49L06hBjAiGwi9m3jZruQuUEtQlctaVLSRZg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-worker@29.7.0: - resolution: - { - integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@30.0.1: + resolution: {integrity: sha512-W3zW27LH1+DYwvz5pw4Xw/t83JcWJv24WWp/CtjA2RvQse0k1OViFqUXBAGlUGM6/zTSek/K7EQea+h+SPUKNw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.0.1: + resolution: {integrity: sha512-T+zDYAoEa8+mZuLlRO6VzvHi/D+CtXSvLAPhmVdEYa7mUV7yshs9kvc/6wespnQx0FUHxnhIP7GuZGiIe/BWcg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true js-tokens@4.0.0: - resolution: - { - integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, - } + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} js-yaml@3.14.1: - resolution: - { - integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==, - } + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true js-yaml@4.1.0: - resolution: - { - integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, - } + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true jsc-safe-url@0.2.4: - resolution: - { - integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==, - } + resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} jscodeshift@17.3.0: - resolution: - { - integrity: sha512-LjFrGOIORqXBU+jwfC9nbkjmQfFldtMIoS6d9z2LG/lkmyNXsJAySPT+2SWXJEoE68/bCWcxKpXH37npftgmow==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-LjFrGOIORqXBU+jwfC9nbkjmQfFldtMIoS6d9z2LG/lkmyNXsJAySPT+2SWXJEoE68/bCWcxKpXH37npftgmow==} + engines: {node: '>=16'} hasBin: true peerDependencies: '@babel/preset-env': ^7.1.6 @@ -4507,634 +3385,384 @@ packages: optional: true jsesc@3.0.2: - resolution: - { - integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} hasBin: true jsesc@3.1.0: - resolution: - { - integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} hasBin: true json-buffer@3.0.1: - resolution: - { - integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, - } + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-parse-better-errors@1.0.2: - resolution: - { - integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==, - } + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} json-schema-traverse@0.4.1: - resolution: - { - integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, - } + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} json-stable-stringify-without-jsonify@1.0.1: - resolution: - { - integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, - } + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} json5@2.2.3: - resolution: - { - integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} hasBin: true jsonfile@6.1.0: - resolution: - { - integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==, - } + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} keyv@4.5.4: - resolution: - { - integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, - } + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} kind-of@6.0.3: - resolution: - { - integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} kuler@2.0.0: - resolution: - { - integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==, - } + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} level-supports@4.0.1: - resolution: - { - integrity: sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==} + engines: {node: '>=12'} level-transcoder@1.0.1: - resolution: - { - integrity: sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==} + engines: {node: '>=12'} level@8.0.1: - resolution: - { - integrity: sha512-oPBGkheysuw7DmzFQYyFe8NAia5jFLAgEnkgWnK3OXAuJr8qFT+xBQIwokAZPME2bhPFzS8hlYcL16m8UZrtwQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-oPBGkheysuw7DmzFQYyFe8NAia5jFLAgEnkgWnK3OXAuJr8qFT+xBQIwokAZPME2bhPFzS8hlYcL16m8UZrtwQ==} + engines: {node: '>=12'} leven@3.1.0: - resolution: - { - integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} levn@0.4.1: - resolution: - { - integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} libp2p@2.8.2: - resolution: - { - integrity: sha512-LYZUWXcL5kQ+5VIiWhUWURLR7tgNBfwqGTLtZukMqjb33U/YAdd9lqW2MXjvaJLXPuGgRAatisDTOEP/ckfhWA==, - } + resolution: {integrity: sha512-LYZUWXcL5kQ+5VIiWhUWURLR7tgNBfwqGTLtZukMqjb33U/YAdd9lqW2MXjvaJLXPuGgRAatisDTOEP/ckfhWA==} lighthouse-logger@1.4.2: - resolution: - { - integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==, - } + resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} lilconfig@3.1.3: - resolution: - { - integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} lint-staged@15.5.0: - resolution: - { - integrity: sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==, - } - engines: { node: '>=18.12.0' } + resolution: {integrity: sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==} + engines: {node: '>=18.12.0'} hasBin: true listr2@8.2.5: - resolution: - { - integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==, - } - engines: { node: '>=18.0.0' } + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} + engines: {node: '>=18.0.0'} locate-path@3.0.0: - resolution: - { - integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} locate-path@5.0.0: - resolution: - { - integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} locate-path@6.0.0: - resolution: - { - integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} lodash.debounce@4.0.8: - resolution: - { - integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==, - } + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: - resolution: - { - integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, - } + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} lodash.throttle@4.1.1: - resolution: - { - integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==, - } + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} log-update@6.1.0: - resolution: - { - integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} logform@2.7.0: - resolution: - { - integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==, - } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} loose-envify@1.4.0: - resolution: - { - integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, - } + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true lru-cache@10.4.3: - resolution: - { - integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, - } + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} lru-cache@5.1.1: - resolution: - { - integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, - } + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} lru@3.1.0: - resolution: - { - integrity: sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==, - } - engines: { node: '>= 0.4.0' } + resolution: {integrity: sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==} + engines: {node: '>= 0.4.0'} make-dir@2.1.0: - resolution: - { - integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} makeerror@1.0.12: - resolution: - { - integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==, - } + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} marky@1.2.5: - resolution: - { - integrity: sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==, - } + resolution: {integrity: sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==} math-intrinsics@1.1.0: - resolution: - { - integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} media-typer@1.1.0: - resolution: - { - integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} memoize-one@5.2.1: - resolution: - { - integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==, - } + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} merge-descriptors@2.0.0: - resolution: - { - integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} merge-options@3.0.4: - resolution: - { - integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} + engines: {node: '>=10'} merge-stream@2.0.0: - resolution: - { - integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, - } + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} merge2@1.4.1: - resolution: - { - integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} metro-babel-transformer@0.81.4: - resolution: - { - integrity: sha512-WW0yswWrW+eTVK9sYD+b1HwWOiUlZlUoomiw9TIOk0C+dh2V90Wttn/8g62kYi0Y4i+cJfISerB2LbV4nuRGTA==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-WW0yswWrW+eTVK9sYD+b1HwWOiUlZlUoomiw9TIOk0C+dh2V90Wttn/8g62kYi0Y4i+cJfISerB2LbV4nuRGTA==} + engines: {node: '>=18.18'} metro-cache-key@0.81.4: - resolution: - { - integrity: sha512-3SaWQybvf1ivasjBegIxzVKLJzOpcz+KsnGwXFOYADQq0VN4cnM7tT+u2jkOhk6yJiiO1WIjl68hqyMOQJRRLg==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-3SaWQybvf1ivasjBegIxzVKLJzOpcz+KsnGwXFOYADQq0VN4cnM7tT+u2jkOhk6yJiiO1WIjl68hqyMOQJRRLg==} + engines: {node: '>=18.18'} metro-cache@0.81.4: - resolution: - { - integrity: sha512-sxCPH3gowDxazSaZZrwdNPEpnxR8UeXDnvPjBF9+5btDBNN2DpWvDAXPvrohkYkFImhc0LajS2V7eOXvu9PnvQ==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-sxCPH3gowDxazSaZZrwdNPEpnxR8UeXDnvPjBF9+5btDBNN2DpWvDAXPvrohkYkFImhc0LajS2V7eOXvu9PnvQ==} + engines: {node: '>=18.18'} metro-config@0.81.4: - resolution: - { - integrity: sha512-QnhMy3bRiuimCTy7oi5Ug60javrSa3lPh0gpMAspQZHY9h6y86jwHtZPLtlj8hdWQESIlrbeL8inMSF6qI/i9Q==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-QnhMy3bRiuimCTy7oi5Ug60javrSa3lPh0gpMAspQZHY9h6y86jwHtZPLtlj8hdWQESIlrbeL8inMSF6qI/i9Q==} + engines: {node: '>=18.18'} metro-core@0.81.4: - resolution: - { - integrity: sha512-GdL4IgmgJhrMA/rTy2lRqXKeXfC77Rg+uvhUEkbhyfj/oz7PrdSgvIFzziapjdHwk1XYq0KyFh/CcVm8ZawG6A==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-GdL4IgmgJhrMA/rTy2lRqXKeXfC77Rg+uvhUEkbhyfj/oz7PrdSgvIFzziapjdHwk1XYq0KyFh/CcVm8ZawG6A==} + engines: {node: '>=18.18'} metro-file-map@0.81.4: - resolution: - { - integrity: sha512-qUIBzkiqOi3qEuscu4cJ83OYQ4hVzjON19FAySWqYys9GKCmxlKa7LkmwqdpBso6lQl+JXZ7nCacX90w5wQvPA==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-qUIBzkiqOi3qEuscu4cJ83OYQ4hVzjON19FAySWqYys9GKCmxlKa7LkmwqdpBso6lQl+JXZ7nCacX90w5wQvPA==} + engines: {node: '>=18.18'} metro-minify-terser@0.81.4: - resolution: - { - integrity: sha512-oVvq/AGvqmbhuijJDZZ9npeWzaVyeBwQKtdlnjcQ9fH7nR15RiBr5y2zTdgTEdynqOIb1Kc16l8CQIUSzOWVFA==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-oVvq/AGvqmbhuijJDZZ9npeWzaVyeBwQKtdlnjcQ9fH7nR15RiBr5y2zTdgTEdynqOIb1Kc16l8CQIUSzOWVFA==} + engines: {node: '>=18.18'} metro-resolver@0.81.4: - resolution: - { - integrity: sha512-Ng7G2mXjSExMeRzj6GC19G6IJ0mfIbOLgjArsMWJgtt9ViZiluCwgWsMW9juBC5NSwjJxUMK2x6pC5NIMFLiHA==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-Ng7G2mXjSExMeRzj6GC19G6IJ0mfIbOLgjArsMWJgtt9ViZiluCwgWsMW9juBC5NSwjJxUMK2x6pC5NIMFLiHA==} + engines: {node: '>=18.18'} metro-runtime@0.81.4: - resolution: - { - integrity: sha512-fBoRgqkF69CwyPtBNxlDi5ha26Zc8f85n2THXYoh13Jn/Bkg8KIDCdKPp/A1BbSeNnkH/++H2EIIfnmaff4uRg==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-fBoRgqkF69CwyPtBNxlDi5ha26Zc8f85n2THXYoh13Jn/Bkg8KIDCdKPp/A1BbSeNnkH/++H2EIIfnmaff4uRg==} + engines: {node: '>=18.18'} metro-source-map@0.81.4: - resolution: - { - integrity: sha512-IOwVQ7mLqoqvsL70RZtl1EyE3f9jp43kVsAsb/B/zoWmu0/k4mwEhGLTxmjdXRkLJqPqPrh7WmFChAEf9trW4Q==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-IOwVQ7mLqoqvsL70RZtl1EyE3f9jp43kVsAsb/B/zoWmu0/k4mwEhGLTxmjdXRkLJqPqPrh7WmFChAEf9trW4Q==} + engines: {node: '>=18.18'} metro-symbolicate@0.81.4: - resolution: - { - integrity: sha512-rWxTmYVN6/BOSaMDUHT8HgCuRf6acd0AjHkenYlHpmgxg7dqdnAG1hLq999q2XpW5rX+cMamZD5W5Ez2LqGaag==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-rWxTmYVN6/BOSaMDUHT8HgCuRf6acd0AjHkenYlHpmgxg7dqdnAG1hLq999q2XpW5rX+cMamZD5W5Ez2LqGaag==} + engines: {node: '>=18.18'} hasBin: true metro-transform-plugins@0.81.4: - resolution: - { - integrity: sha512-nlP069nDXm4v28vbll4QLApAlvVtlB66rP6h+ml8Q/CCQCPBXu2JLaoxUmkIOJQjLhMRUcgTyQHq+TXWJhydOQ==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-nlP069nDXm4v28vbll4QLApAlvVtlB66rP6h+ml8Q/CCQCPBXu2JLaoxUmkIOJQjLhMRUcgTyQHq+TXWJhydOQ==} + engines: {node: '>=18.18'} metro-transform-worker@0.81.4: - resolution: - { - integrity: sha512-lKAeRZ8EUMtx2cA/Y4KvICr9bIr5SE03iK3lm+l9wyn2lkjLUuPjYVep159inLeDqC6AtSubsA8MZLziP7c03g==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-lKAeRZ8EUMtx2cA/Y4KvICr9bIr5SE03iK3lm+l9wyn2lkjLUuPjYVep159inLeDqC6AtSubsA8MZLziP7c03g==} + engines: {node: '>=18.18'} metro@0.81.4: - resolution: - { - integrity: sha512-78f0aBNPuwXW7GFnSc+Y0vZhbuQorXxdgqQfvSRqcSizqwg9cwF27I05h47tL8AzQcizS1JZncvq4xf5u/Qykw==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-78f0aBNPuwXW7GFnSc+Y0vZhbuQorXxdgqQfvSRqcSizqwg9cwF27I05h47tL8AzQcizS1JZncvq4xf5u/Qykw==} + engines: {node: '>=18.18'} hasBin: true micromatch@4.0.8: - resolution: - { - integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, - } - engines: { node: '>=8.6' } + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} mime-db@1.52.0: - resolution: - { - integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} mime-db@1.54.0: - resolution: - { - integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} mime-types@2.1.35: - resolution: - { - integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} mime-types@3.0.1: - resolution: - { - integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} mime@1.6.0: - resolution: - { - integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} hasBin: true + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-fn@4.0.0: - resolution: - { - integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} mimic-function@5.0.1: - resolution: - { - integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} mimic-response@3.1.0: - resolution: - { - integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} minimatch@3.1.2: - resolution: - { - integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, - } + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} minimatch@9.0.5: - resolution: - { - integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==, - } - engines: { node: '>=16 || 14 >=14.17' } + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: - resolution: - { - integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, - } + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} minipass@7.1.2: - resolution: - { - integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, - } - engines: { node: '>=16 || 14 >=14.17' } + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} mkdirp-classic@0.5.3: - resolution: - { - integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==, - } + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} mkdirp@1.0.4: - resolution: - { - integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} hasBin: true module-error@1.0.2: - resolution: - { - integrity: sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==} + engines: {node: '>=10'} mortice@3.0.6: - resolution: - { - integrity: sha512-xUjsTQreX8rO3pHuGYDZ3PY/sEiONIzqzjLeog5akdY4bz9TlDDuvYlU8fm+6qnm4rnpa6AFxLhsfSBThLijdA==, - } + resolution: {integrity: sha512-xUjsTQreX8rO3pHuGYDZ3PY/sEiONIzqzjLeog5akdY4bz9TlDDuvYlU8fm+6qnm4rnpa6AFxLhsfSBThLijdA==} ms@2.0.0: - resolution: - { - integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==, - } + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} ms@2.1.2: - resolution: - { - integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, - } + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} ms@2.1.3: - resolution: - { - integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, - } + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} ms@3.0.0-canary.1: - resolution: - { - integrity: sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==, - } - engines: { node: '>=12.13' } + resolution: {integrity: sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==} + engines: {node: '>=12.13'} multicast-dns@7.2.5: - resolution: - { - integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==, - } + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true multiformats@12.1.3: - resolution: - { - integrity: sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} multiformats@13.3.2: - resolution: - { - integrity: sha512-qbB0CQDt3QKfiAzZ5ZYjLFOs+zW43vA4uyM8g27PeEuXZybUOFyjrVdP93HPBHMoglibwfkdVwbzfUq8qGcH6g==, - } + resolution: {integrity: sha512-qbB0CQDt3QKfiAzZ5ZYjLFOs+zW43vA4uyM8g27PeEuXZybUOFyjrVdP93HPBHMoglibwfkdVwbzfUq8qGcH6g==} murmurhash3js-revisited@3.0.0: - resolution: - { - integrity: sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==} + engines: {node: '>=8.0.0'} nanoid@5.1.5: - resolution: - { - integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==, - } - engines: { node: ^18 || >=20 } + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} hasBin: true napi-build-utils@2.0.0: - resolution: - { - integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==, - } + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} napi-macros@2.2.2: - resolution: - { - integrity: sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==, - } + resolution: {integrity: sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==} + + napi-postinstall@0.2.4: + resolution: {integrity: sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true natural-compare@1.4.0: - resolution: - { - integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, - } + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} negotiator@0.6.3: - resolution: - { - integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} negotiator@1.0.0: - resolution: - { - integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} neo-async@2.6.2: - resolution: - { - integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==, - } + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} netmask@2.0.2: - resolution: - { - integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==, - } - engines: { node: '>= 0.4.0' } + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} node-abi@3.74.0: - resolution: - { - integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==} + engines: {node: '>=10'} node-cache@5.1.2: - resolution: - { - integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==, - } - engines: { node: '>= 8.0.0' } + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} node-fetch@2.7.0: - resolution: - { - integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, - } - engines: { node: 4.x || >=6.0.0 } + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} peerDependencies: encoding: ^0.1.0 peerDependenciesMeta: @@ -5142,513 +3770,315 @@ packages: optional: true node-forge@1.3.1: - resolution: - { - integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==, - } - engines: { node: '>= 6.13.0' } + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} node-gyp-build@4.8.4: - resolution: - { - integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==, - } + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true node-int64@0.4.0: - resolution: - { - integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==, - } + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} node-releases@2.0.19: - resolution: - { - integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==, - } + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} normalize-path@3.0.0: - resolution: - { - integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} npm-run-path@5.3.0: - resolution: - { - integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} nullthrows@1.1.1: - resolution: - { - integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==, - } + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} ob1@0.81.4: - resolution: - { - integrity: sha512-EZLYM8hfPraC2SYOR5EWLFAPV5e6g+p83m2Jth9bzCpFxP1NDQJYXdmXRB2bfbaWQSmm6NkIQlbzk7uU5lLfgg==, - } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-EZLYM8hfPraC2SYOR5EWLFAPV5e6g+p83m2Jth9bzCpFxP1NDQJYXdmXRB2bfbaWQSmm6NkIQlbzk7uU5lLfgg==} + engines: {node: '>=18.18'} object-inspect@1.13.4: - resolution: - { - integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} observable-webworkers@2.0.1: - resolution: - { - integrity: sha512-JI1vB0u3pZjoQKOK1ROWzp0ygxSi7Yb0iR+7UNsw4/Zn4cQ0P3R7XL38zac/Dy2tEA7Lg88/wIJTjF8vYXZ0uw==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-JI1vB0u3pZjoQKOK1ROWzp0ygxSi7Yb0iR+7UNsw4/Zn4cQ0P3R7XL38zac/Dy2tEA7Lg88/wIJTjF8vYXZ0uw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} on-finished@2.3.0: - resolution: - { - integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} on-finished@2.4.1: - resolution: - { - integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} once@1.4.0: - resolution: - { - integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, - } + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} one-time@1.0.0: - resolution: - { - integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==, - } + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} onetime@6.0.0: - resolution: - { - integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} onetime@7.0.0: - resolution: - { - integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} open@7.4.2: - resolution: - { - integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} optionator@0.9.4: - resolution: - { - integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} p-defer@4.0.1: - resolution: - { - integrity: sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==} + engines: {node: '>=12'} p-event@6.0.1: - resolution: - { - integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==, - } - engines: { node: '>=16.17' } + resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} + engines: {node: '>=16.17'} p-limit@2.3.0: - resolution: - { - integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} p-limit@3.1.0: - resolution: - { - integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} p-locate@3.0.0: - resolution: - { - integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} p-locate@4.1.0: - resolution: - { - integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} p-locate@5.0.0: - resolution: - { - integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} p-queue@8.1.0: - resolution: - { - integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==} + engines: {node: '>=18'} p-retry@6.2.1: - resolution: - { - integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==, - } - engines: { node: '>=16.17' } + resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} + engines: {node: '>=16.17'} p-timeout@6.1.4: - resolution: - { - integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==, - } - engines: { node: '>=14.16' } + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} p-try@2.2.0: - resolution: - { - integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} p-wait-for@5.0.2: - resolution: - { - integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==} + engines: {node: '>=12'} package-json-from-dist@1.0.1: - resolution: - { - integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, - } + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} parent-module@1.0.1: - resolution: - { - integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} parse-json@4.0.0: - resolution: - { - integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} parseurl@1.3.3: - resolution: - { - integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} path-exists@3.0.0: - resolution: - { - integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} path-exists@4.0.0: - resolution: - { - integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} path-is-absolute@1.0.1: - resolution: - { - integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} path-key@3.1.1: - resolution: - { - integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} path-key@4.0.0: - resolution: - { - integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} path-parse@1.0.7: - resolution: - { - integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, - } + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} path-scurry@1.11.1: - resolution: - { - integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==, - } - engines: { node: '>=16 || 14 >=14.18' } + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} path-to-regexp@8.2.0: - resolution: - { - integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} picocolors@1.1.1: - resolution: - { - integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, - } + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.1: - resolution: - { - integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, - } - engines: { node: '>=8.6' } + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} pidtree@0.6.0: - resolution: - { - integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} hasBin: true pify@4.0.1: - resolution: - { - integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} pirates@4.0.7: - resolution: - { - integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} pkg-dir@3.0.0: - resolution: - { - integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} prebuild-install@7.1.3: - resolution: - { - integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} hasBin: true prelude-ls@1.2.1: - resolution: - { - integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} prettier-linter-helpers@1.0.0: - resolution: - { - integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} prettier@3.5.3: - resolution: - { - integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} hasBin: true pretty-format@29.7.0: - resolution: - { - integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + pretty-format@30.0.1: + resolution: {integrity: sha512-2pkYD4WKYrAVyx/Jo7DmV+XAVJ9PuC0gVi9/gCPOxd+dN6WD+Pa7+ScUdh3f9m2klEPEZmfu8HoyYnuaGXzGAA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} progress-events@1.0.1: - resolution: - { - integrity: sha512-MOzLIwhpt64KIVN64h1MwdKWiyKFNc/S6BoYKPIVUHFg0/eIEyBulhWCgn678v/4c0ri3FdGuzXymNCv02MUIw==, - } + resolution: {integrity: sha512-MOzLIwhpt64KIVN64h1MwdKWiyKFNc/S6BoYKPIVUHFg0/eIEyBulhWCgn678v/4c0ri3FdGuzXymNCv02MUIw==} promise@8.3.0: - resolution: - { - integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==, - } + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} protons-runtime@5.5.0: - resolution: - { - integrity: sha512-EsALjF9QsrEk6gbCx3lmfHxVN0ah7nG3cY7GySD4xf4g8cr7g543zB88Foh897Sr1RQJ9yDCUsoT1i1H/cVUFA==, - } + resolution: {integrity: sha512-EsALjF9QsrEk6gbCx3lmfHxVN0ah7nG3cY7GySD4xf4g8cr7g543zB88Foh897Sr1RQJ9yDCUsoT1i1H/cVUFA==} proxy-addr@2.0.7: - resolution: - { - integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==, - } - engines: { node: '>= 0.10' } + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} proxy-from-env@1.1.0: - resolution: - { - integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, - } + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} pump@3.0.2: - resolution: - { - integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==, - } + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} punycode@2.3.1: - resolution: - { - integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} pvtsutils@1.3.6: - resolution: - { - integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==, - } + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} pvutils@1.1.3: - resolution: - { - integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} qs@6.14.0: - resolution: - { - integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==, - } - engines: { node: '>=0.6' } + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} queue-microtask@1.2.3: - resolution: - { - integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, - } + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} queue@6.0.2: - resolution: - { - integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==, - } + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} rabin-wasm@0.1.5: - resolution: - { - integrity: sha512-uWgQTo7pim1Rnj5TuWcCewRDTf0PEFTSlaUjWP4eY9EbLV9em08v89oCz/WO+wRxpYuO36XEHp4wgYQnAgOHzA==, - } + resolution: {integrity: sha512-uWgQTo7pim1Rnj5TuWcCewRDTf0PEFTSlaUjWP4eY9EbLV9em08v89oCz/WO+wRxpYuO36XEHp4wgYQnAgOHzA==} hasBin: true race-event@1.3.0: - resolution: - { - integrity: sha512-kaLm7axfOnahIqD3jQ4l1e471FIFcEGebXEnhxyLscuUzV8C94xVHtWEqDDXxll7+yu/6lW0w1Ff4HbtvHvOHg==, - } + resolution: {integrity: sha512-kaLm7axfOnahIqD3jQ4l1e471FIFcEGebXEnhxyLscuUzV8C94xVHtWEqDDXxll7+yu/6lW0w1Ff4HbtvHvOHg==} race-signal@1.1.3: - resolution: - { - integrity: sha512-Mt2NznMgepLfORijhQMncE26IhkmjEphig+/1fKC0OtaKwys/gpvpmswSjoN01SS+VO951mj0L4VIDXdXsjnfA==, - } + resolution: {integrity: sha512-Mt2NznMgepLfORijhQMncE26IhkmjEphig+/1fKC0OtaKwys/gpvpmswSjoN01SS+VO951mj0L4VIDXdXsjnfA==} range-parser@1.2.1: - resolution: - { - integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} raw-body@3.0.0: - resolution: - { - integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} rc@1.2.8: - resolution: - { - integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==, - } + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true react-devtools-core@6.1.1: - resolution: - { - integrity: sha512-TFo1MEnkqE6hzAbaztnyR5uLTMoz6wnEWwWBsCUzNt+sVXJycuRJdDqvL078M4/h65BI/YO5XWTaxZDWVsW0fw==, - } + resolution: {integrity: sha512-TFo1MEnkqE6hzAbaztnyR5uLTMoz6wnEWwWBsCUzNt+sVXJycuRJdDqvL078M4/h65BI/YO5XWTaxZDWVsW0fw==} react-is@18.3.1: - resolution: - { - integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==, - } + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} react-native-webrtc@124.0.5: - resolution: - { - integrity: sha512-LIQJKst+t53bJOcQef9VXuz3pVheSBUA4olQGkxosbF4pHW1gsWoXYmf6wmI2zrqOA+aZsjjB6aT9AKLyr6a0Q==, - } + resolution: {integrity: sha512-LIQJKst+t53bJOcQef9VXuz3pVheSBUA4olQGkxosbF4pHW1gsWoXYmf6wmI2zrqOA+aZsjjB6aT9AKLyr6a0Q==} peerDependencies: react-native: '>=0.60.0' react-native@0.78.1: - resolution: - { - integrity: sha512-3CK/xxX02GeeVFyrXbsHvREZFVaXwHW43Km/EdYITn5G32cccWTGaqY9QdPddEBLw5O3BPip3LHbR1SywE0cpA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-3CK/xxX02GeeVFyrXbsHvREZFVaXwHW43Km/EdYITn5G32cccWTGaqY9QdPddEBLw5O3BPip3LHbR1SywE0cpA==} + engines: {node: '>=18'} hasBin: true peerDependencies: '@types/react': ^19.0.0 @@ -5658,1092 +4088,684 @@ packages: optional: true react-refresh@0.14.2: - resolution: - { - integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} react@19.1.0: - resolution: - { - integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} readable-stream@3.6.2: - resolution: - { - integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} readline@1.3.0: - resolution: - { - integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==, - } + resolution: {integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==} recast@0.23.11: - resolution: - { - integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} reflect-metadata@0.2.2: - resolution: - { - integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==, - } + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} regenerate-unicode-properties@10.2.0: - resolution: - { - integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} regenerate@1.4.2: - resolution: - { - integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==, - } + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} regenerator-runtime@0.13.11: - resolution: - { - integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==, - } + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} regenerator-runtime@0.14.1: - resolution: - { - integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==, - } + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} regenerator-transform@0.15.2: - resolution: - { - integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==, - } + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} regexpu-core@6.2.0: - resolution: - { - integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} regjsgen@0.8.0: - resolution: - { - integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==, - } + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} regjsparser@0.12.0: - resolution: - { - integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==, - } + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} hasBin: true require-directory@2.1.1: - resolution: - { - integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} resolve-from@3.0.0: - resolution: - { - integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} resolve-from@4.0.0: - resolution: - { - integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} resolve-from@5.0.0: - resolution: - { - integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} resolve@1.22.10: - resolution: - { - integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} hasBin: true restore-cursor@5.1.0: - resolution: - { - integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} retimeable-signal@1.0.1: - resolution: - { - integrity: sha512-Cy26CYfbWnYu8HMoJeDhaMpW/EYFIbne3vMf6G9RSrOyWYXbPehja/BEdzpqmM84uy2bfBD7NPZhoQ4GZEtgvg==, - } + resolution: {integrity: sha512-Cy26CYfbWnYu8HMoJeDhaMpW/EYFIbne3vMf6G9RSrOyWYXbPehja/BEdzpqmM84uy2bfBD7NPZhoQ4GZEtgvg==} retimer@3.0.0: - resolution: - { - integrity: sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==, - } + resolution: {integrity: sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==} retry@0.13.1: - resolution: - { - integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} reusify@1.1.0: - resolution: - { - integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, - } - engines: { iojs: '>=1.0.0', node: '>=0.10.0' } + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} rfdc@1.4.1: - resolution: - { - integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==, - } + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} rimraf@3.0.2: - resolution: - { - integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==, - } + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@5.0.10: - resolution: - { - integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==, - } + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true router@2.2.0: - resolution: - { - integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==, - } - engines: { node: '>= 18' } + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} run-parallel-limit@1.1.0: - resolution: - { - integrity: sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==, - } + resolution: {integrity: sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==} run-parallel@1.2.0: - resolution: - { - integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, - } + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safe-buffer@5.2.1: - resolution: - { - integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, - } + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safe-stable-stringify@2.5.0: - resolution: - { - integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} safer-buffer@2.1.2: - resolution: - { - integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, - } + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} sanitize-filename@1.6.3: - resolution: - { - integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==, - } + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} sax@1.4.1: - resolution: - { - integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==, - } + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} scheduler@0.25.0: - resolution: - { - integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==, - } + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} selfsigned@2.4.1: - resolution: - { - integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} semver@5.7.2: - resolution: - { - integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==, - } + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true semver@6.3.1: - resolution: - { - integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, - } + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true semver@7.7.1: - resolution: - { - integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} hasBin: true send@0.19.0: - resolution: - { - integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} send@1.2.0: - resolution: - { - integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==, - } - engines: { node: '>= 18' } + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} serialize-error@2.1.0: - resolution: - { - integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} + engines: {node: '>=0.10.0'} serve-static@1.16.2: - resolution: - { - integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} serve-static@2.2.0: - resolution: - { - integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==, - } - engines: { node: '>= 18' } + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} setprototypeof@1.2.0: - resolution: - { - integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, - } + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} shallow-clone@3.0.1: - resolution: - { - integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} shebang-command@2.0.0: - resolution: - { - integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} shebang-regex@3.0.0: - resolution: - { - integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} shell-quote@1.8.2: - resolution: - { - integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} + engines: {node: '>= 0.4'} side-channel-list@1.0.0: - resolution: - { - integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} side-channel-map@1.0.1: - resolution: - { - integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} side-channel-weakmap@1.0.2: - resolution: - { - integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} side-channel@1.1.0: - resolution: - { - integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} signal-exit@3.0.7: - resolution: - { - integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, - } + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} signal-exit@4.1.0: - resolution: - { - integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} simple-concat@1.0.1: - resolution: - { - integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==, - } + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} simple-get@4.0.1: - resolution: - { - integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==, - } + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} simple-swizzle@0.2.2: - resolution: - { - integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==, - } + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} slash@3.0.0: - resolution: - { - integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} slice-ansi@5.0.0: - resolution: - { - integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} slice-ansi@7.1.0: - resolution: - { - integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} source-map-support@0.5.21: - resolution: - { - integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==, - } + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} source-map@0.5.7: - resolution: - { - integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} source-map@0.6.1: - resolution: - { - integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} sparse-array@1.3.2: - resolution: - { - integrity: sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg==, - } + resolution: {integrity: sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg==} sprintf-js@1.0.3: - resolution: - { - integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==, - } + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} stack-trace@0.0.10: - resolution: - { - integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==, - } + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} stack-utils@2.0.6: - resolution: - { - integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} stackframe@1.3.4: - resolution: - { - integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==, - } + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} stacktrace-parser@0.1.11: - resolution: - { - integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} + engines: {node: '>=6'} statuses@1.5.0: - resolution: - { - integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} statuses@2.0.1: - resolution: - { - integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} steno@4.0.2: - resolution: - { - integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} + engines: {node: '>=18'} stream-to-it@1.0.1: - resolution: - { - integrity: sha512-AqHYAYPHcmvMrcLNgncE/q0Aj/ajP6A4qGhxP6EVn7K3YTNs0bJpJyk57wc2Heb7MUL64jurvmnmui8D9kjZgA==, - } + resolution: {integrity: sha512-AqHYAYPHcmvMrcLNgncE/q0Aj/ajP6A4qGhxP6EVn7K3YTNs0bJpJyk57wc2Heb7MUL64jurvmnmui8D9kjZgA==} string-argv@0.3.2: - resolution: - { - integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==, - } - engines: { node: '>=0.6.19' } + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} string-width@4.2.3: - resolution: - { - integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} string-width@5.1.2: - resolution: - { - integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} string-width@7.2.0: - resolution: - { - integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} string_decoder@1.3.0: - resolution: - { - integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, - } + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} strip-ansi@6.0.1: - resolution: - { - integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} strip-ansi@7.1.0: - resolution: - { - integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} strip-final-newline@3.0.0: - resolution: - { - integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} strip-json-comments@2.0.1: - resolution: - { - integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} strip-json-comments@3.1.1: - resolution: - { - integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} super-regex@0.2.0: - resolution: - { - integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==, - } - engines: { node: '>=14.16' } + resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==} + engines: {node: '>=14.16'} supports-color@7.2.0: - resolution: - { - integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} supports-color@8.1.1: - resolution: - { - integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} supports-color@9.4.0: - resolution: - { - integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} supports-preserve-symlinks-flag@1.0.0: - resolution: - { - integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} synckit@0.11.2: - resolution: - { - integrity: sha512-1IUffI8zZ8qUMB3NUJIjk0RpLroG/8NkQDAWH1NbB2iJ0/5pn3M8rxfNzMz4GH9OnYaGYn31LEDSXJp/qIlxgA==, - } - engines: { node: ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-1IUffI8zZ8qUMB3NUJIjk0RpLroG/8NkQDAWH1NbB2iJ0/5pn3M8rxfNzMz4GH9OnYaGYn31LEDSXJp/qIlxgA==} + engines: {node: ^14.18.0 || >=16.0.0} + + synckit@0.11.8: + resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} + engines: {node: ^14.18.0 || >=16.0.0} tar-fs@2.1.2: - resolution: - { - integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==, - } + resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} tar-stream@2.2.0: - resolution: - { - integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} terser@5.39.0: - resolution: - { - integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} + engines: {node: '>=10'} hasBin: true test-exclude@6.0.0: - resolution: - { - integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} text-hex@1.0.0: - resolution: - { - integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==, - } + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} throat@5.0.0: - resolution: - { - integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==, - } + resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} thunky@1.1.0: - resolution: - { - integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==, - } + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} time-span@5.1.0: - resolution: - { - integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} + engines: {node: '>=12'} timeout-abort-controller@3.0.0: - resolution: - { - integrity: sha512-O3e+2B8BKrQxU2YRyEjC/2yFdb33slI22WRdUaDx6rvysfi9anloNZyR2q0l6LnePo5qH7gSM7uZtvvwZbc2yA==, - } + resolution: {integrity: sha512-O3e+2B8BKrQxU2YRyEjC/2yFdb33slI22WRdUaDx6rvysfi9anloNZyR2q0l6LnePo5qH7gSM7uZtvvwZbc2yA==} timestamp-nano@1.0.1: - resolution: - { - integrity: sha512-4oGOVZWTu5sl89PtCDnhQBSt7/vL1zVEwAfxH1p49JhTosxzVQWYBYFRFZ8nJmo0G6f824iyP/44BFAwIoKvIA==, - } - engines: { node: '>= 4.5.0' } + resolution: {integrity: sha512-4oGOVZWTu5sl89PtCDnhQBSt7/vL1zVEwAfxH1p49JhTosxzVQWYBYFRFZ8nJmo0G6f824iyP/44BFAwIoKvIA==} + engines: {node: '>= 4.5.0'} tiny-invariant@1.3.3: - resolution: - { - integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==, - } + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} tldts-core@6.1.85: - resolution: - { - integrity: sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==, - } + resolution: {integrity: sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==} tldts@6.1.85: - resolution: - { - integrity: sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==, - } + resolution: {integrity: sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==} hasBin: true tmp@0.2.3: - resolution: - { - integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==, - } - engines: { node: '>=14.14' } + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} tmpl@1.0.5: - resolution: - { - integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==, - } + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} to-regex-range@5.0.1: - resolution: - { - integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, - } - engines: { node: '>=8.0' } + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} toidentifier@1.0.1: - resolution: - { - integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, - } - engines: { node: '>=0.6' } + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} tough-cookie@5.1.2: - resolution: - { - integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} tr46@0.0.3: - resolution: - { - integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, - } + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} triple-beam@1.4.1: - resolution: - { - integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==, - } - engines: { node: '>= 14.0.0' } + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} truncate-utf8-bytes@1.0.2: - resolution: - { - integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==, - } + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} ts-api-utils@2.1.0: - resolution: - { - integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==, - } - engines: { node: '>=18.12' } + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' + ts-jest@29.4.0: + resolution: {integrity: sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + tsc-esm-fix@3.1.2: - resolution: - { - integrity: sha512-1/OpZssMcEp2ae6DyZV+yvDviofuCdDf7dEWEaBvm/ac8vtS04lFyl0LVs8LQE56vjKHytgzVjPIL9udM4QuNg==, - } - engines: { node: '>=18.0.0' } + resolution: {integrity: sha512-1/OpZssMcEp2ae6DyZV+yvDviofuCdDf7dEWEaBvm/ac8vtS04lFyl0LVs8LQE56vjKHytgzVjPIL9udM4QuNg==} + engines: {node: '>=18.0.0'} hasBin: true tslib@1.14.1: - resolution: - { - integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==, - } + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} tslib@2.8.1: - resolution: - { - integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, - } + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true tsyringe@4.8.0: - resolution: - { - integrity: sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==, - } - engines: { node: '>= 6.0.0' } + resolution: {integrity: sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==} + engines: {node: '>= 6.0.0'} tunnel-agent@0.6.0: - resolution: - { - integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==, - } + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} type-check@0.4.0: - resolution: - { - integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} type-detect@4.0.8: - resolution: - { - integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} type-fest@0.7.1: - resolution: - { - integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} type-flag@3.0.0: - resolution: - { - integrity: sha512-3YaYwMseXCAhBB14RXW5cRQfJQlEknS6i4C8fCfeUdS3ihG9EdccdR9kt3vP73ZdeTGmPb4bZtkDn5XMIn1DLA==, - } + resolution: {integrity: sha512-3YaYwMseXCAhBB14RXW5cRQfJQlEknS6i4C8fCfeUdS3ihG9EdccdR9kt3vP73ZdeTGmPb4bZtkDn5XMIn1DLA==} type-is@2.0.1: - resolution: - { - integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} typescript-eslint@8.29.0: - resolution: - { - integrity: sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' typescript@5.8.2: - resolution: - { - integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==, - } - engines: { node: '>=14.17' } + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} hasBin: true uint8-varint@2.0.4: - resolution: - { - integrity: sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw==, - } + resolution: {integrity: sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw==} uint8arraylist@2.4.8: - resolution: - { - integrity: sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ==, - } + resolution: {integrity: sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ==} uint8arrays@5.1.0: - resolution: - { - integrity: sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==, - } + resolution: {integrity: sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==} undici-types@6.20.0: - resolution: - { - integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==, - } + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} undici-types@6.21.0: - resolution: - { - integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, - } + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} undici@6.21.2: - resolution: - { - integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==, - } - engines: { node: '>=18.17' } + resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==} + engines: {node: '>=18.17'} unicode-canonical-property-names-ecmascript@2.0.1: - resolution: - { - integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} unicode-match-property-ecmascript@2.0.0: - resolution: - { - integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} unicode-match-property-value-ecmascript@2.2.0: - resolution: - { - integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} unicode-property-aliases-ecmascript@2.1.0: - resolution: - { - integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} universalify@2.0.1: - resolution: - { - integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==, - } - engines: { node: '>= 10.0.0' } + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} unpipe@1.0.0: - resolution: - { - integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.9.0: + resolution: {integrity: sha512-wqaRu4UnzBD2ABTC1kLfBjAqIDZ5YUTr/MLGa7By47JV1bJDSW7jq/ZSLigB7enLe7ubNaJhtnBXgrc/50cEhg==} update-browserslist-db@1.1.3: - resolution: - { - integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==, - } + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' uri-js@4.4.1: - resolution: - { - integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, - } + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} utf8-byte-length@1.0.5: - resolution: - { - integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==, - } + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} util-deprecate@1.0.2: - resolution: - { - integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, - } + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} utils-merge@1.0.1: - resolution: - { - integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==, - } - engines: { node: '>= 0.4.0' } + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} vary@1.1.2: - resolution: - { - integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} vlq@1.0.1: - resolution: - { - integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==, - } + resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} walker@1.0.8: - resolution: - { - integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==, - } + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} weald@1.0.4: - resolution: - { - integrity: sha512-+kYTuHonJBwmFhP1Z4YQK/dGi3jAnJGCYhyODFpHK73rbxnp9lnZQj7a2m+WVgn8fXr5bJaxUpF6l8qZpPeNWQ==, - } + resolution: {integrity: sha512-+kYTuHonJBwmFhP1Z4YQK/dGi3jAnJGCYhyODFpHK73rbxnp9lnZQj7a2m+WVgn8fXr5bJaxUpF6l8qZpPeNWQ==} webcrypto-core@1.8.1: - resolution: - { - integrity: sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==, - } + resolution: {integrity: sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==} webidl-conversions@3.0.1: - resolution: - { - integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, - } + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} whatwg-fetch@3.6.20: - resolution: - { - integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==, - } + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} whatwg-url@5.0.0: - resolution: - { - integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, - } + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} wherearewe@2.0.1: - resolution: - { - integrity: sha512-XUguZbDxCA2wBn2LoFtcEhXL6AXo+hVjGonwhSTTTU9SzbWG8Xu3onNIpzf9j/mYUcJQ0f+m37SzG77G851uFw==, - } - engines: { node: '>=16.0.0', npm: '>=7.0.0' } + resolution: {integrity: sha512-XUguZbDxCA2wBn2LoFtcEhXL6AXo+hVjGonwhSTTTU9SzbWG8Xu3onNIpzf9j/mYUcJQ0f+m37SzG77G851uFw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} which@2.0.2: - resolution: - { - integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} hasBin: true winston-transport@4.9.0: - resolution: - { - integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==, - } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} winston@3.17.0: - resolution: - { - integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==, - } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} word-wrap@1.2.5: - resolution: - { - integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} wrap-ansi@7.0.0: - resolution: - { - integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} wrap-ansi@8.1.0: - resolution: - { - integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} wrap-ansi@9.0.0: - resolution: - { - integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} wrappy@1.0.2: - resolution: - { - integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, - } + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} write-file-atomic@4.0.2: - resolution: - { - integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==, - } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} write-file-atomic@5.0.1: - resolution: - { - integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==, - } - engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} ws@6.2.3: - resolution: - { - integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==, - } + resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==} peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -6754,11 +4776,8 @@ packages: optional: true ws@7.5.10: - resolution: - { - integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==, - } - engines: { node: '>=8.3.0' } + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -6769,11 +4788,8 @@ packages: optional: true ws@8.18.1: - resolution: - { - integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==, - } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 utf-8-validate: '>=5.0.2' @@ -6784,62 +4800,39 @@ packages: optional: true xml2js@0.6.2: - resolution: - { - integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==, - } - engines: { node: '>=4.0.0' } + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} xmlbuilder@11.0.1: - resolution: - { - integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==, - } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} y18n@5.0.8: - resolution: - { - integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} yallist@3.1.1: - resolution: - { - integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, - } + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} yaml@2.7.1: - resolution: - { - integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==, - } - engines: { node: '>= 14' } + resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} + engines: {node: '>= 14'} hasBin: true yargs-parser@21.1.1: - resolution: - { - integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} yargs@17.7.2: - resolution: - { - integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} yocto-queue@0.1.0: - resolution: - { - integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} snapshots: + '@achingbrain/http-parser-js@0.5.8': dependencies: uint8arrays: 5.1.0 @@ -6876,8 +4869,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.26.8': {} + '@babel/compat-data@7.27.5': {} + '@babel/core@7.26.10': dependencies: '@ampproject/remapping': 2.3.0 @@ -6898,6 +4899,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.27.4': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.27.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.27.0': dependencies: '@babel/parser': 7.27.0 @@ -6906,9 +4927,17 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.27.5': + dependencies: + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.25.9': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.27.6 '@babel/helper-compilation-targets@7.27.0': dependencies: @@ -6918,31 +4947,39 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.27.0(@babel/core@7.26.10)': + '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/core': 7.26.10 + '@babel/compat-data': 7.27.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-member-expression-to-functions': 7.25.9 '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.27.4) '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/traverse': 7.27.4 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.27.0(@babel/core@7.26.10)': + '@babel/helper-create-regexp-features-plugin@7.27.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-annotate-as-pure': 7.25.9 regexpu-core: 6.2.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.26.10)': + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-compilation-targets': 7.27.0 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 debug: 4.4.0 lodash.debounce: 4.0.8 resolve: 1.22.10 @@ -6951,8 +4988,8 @@ snapshots: '@babel/helper-member-expression-to-functions@7.25.9': dependencies: - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 transitivePeerDependencies: - supports-color @@ -6963,6 +5000,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -6972,48 +5016,65 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.25.9': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-plugin-utils@7.26.5': {} - - '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-wrap-function': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.10)': + '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/core': 7.26.10 + '@babel/types': 7.27.6 + + '@babel/helper-plugin-utils@7.26.5': {} + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.26.5(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 '@babel/helper-member-expression-to-functions': 7.25.9 '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/traverse': 7.27.4 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-wrap-function@7.25.9': dependencies: - '@babel/template': 7.27.0 - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 transitivePeerDependencies: - supports-color @@ -7022,642 +5083,651 @@ snapshots: '@babel/template': 7.27.0 '@babel/types': 7.27.0 + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.6 + '@babel/parser@7.27.0': dependencies: '@babel/types': 7.27.0 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.10)': + '@babel/parser@7.27.5': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/traverse': 7.27.0 + '@babel/types': 7.27.6 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.4 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.27.4) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/traverse': 7.27.0 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.4 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-export-default-from@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-proposal-export-default-from@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.10)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.10)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.10)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.10)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-export-default-from@7.25.9(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-flow@7.26.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.26.10)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-export-default-from@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.10)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.10)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.10)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.10)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.10)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.10)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.10)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.10)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.10)': + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.10)': + '@babel/plugin-transform-async-generator-functions@7.26.8(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-transform-async-generator-functions@7.26.8(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.10) - '@babel/traverse': 7.27.0 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.27.4) + '@babel/traverse': 7.27.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.27.4) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.26.5(@babel/core@7.26.10)': + '@babel/plugin-transform-block-scoped-functions@7.26.5(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.27.0(@babel/core@7.26.10)': + '@babel/plugin-transform-block-scoping@7.27.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.10)': + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-compilation-targets': 7.27.0 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) - '@babel/traverse': 7.27.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.27.4) + '@babel/traverse': 7.27.4 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/template': 7.27.0 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 - '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-exponentiation-operator@7.26.3(@babel/core@7.26.10)': + '@babel/plugin-transform-exponentiation-operator@7.26.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-flow-strip-types@7.26.5(@babel/core@7.26.10)': + '@babel/plugin-transform-flow-strip-types@7.26.5(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.27.4) - '@babel/plugin-transform-for-of@7.26.9(@babel/core@7.26.10)': + '@babel/plugin-transform-for-of@7.26.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-compilation-targets': 7.27.0 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/traverse': 7.27.0 + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.10)': + '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.26.6(@babel/core@7.26.10)': + '@babel/plugin-transform-nullish-coalescing-operator@7.26.6(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-compilation-targets': 7.27.0 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.27.4) - '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.27.4) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-display-name@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-react-display-name@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/types': 7.27.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/types': 7.27.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-regenerator@7.27.0(@babel/core@7.26.10)': + '@babel/plugin-transform-regenerator@7.27.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 regenerator-transform: 0.15.2 - '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.10)': + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-runtime@7.26.10(@babel/core@7.26.10)': + '@babel/plugin-transform-runtime@7.26.10(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-plugin-utils': 7.26.5 - babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.26.10) - babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.26.10) - babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.27.4) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.4) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.27.4) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.26.8(@babel/core@7.26.10)': + '@babel/plugin-transform-template-literals@7.26.8(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.0(@babel/core@7.26.10)': + '@babel/plugin-transform-typeof-symbol@7.27.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.27.0(@babel/core@7.26.10)': + '@babel/plugin-transform-typescript@7.27.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/preset-env@7.26.9(@babel/core@7.26.10)': + '@babel/preset-env@7.26.9(@babel/core@7.27.4)': dependencies: - '@babel/compat-data': 7.26.8 - '@babel/core': 7.26.10 - '@babel/helper-compilation-targets': 7.27.0 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-option': 7.25.9 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.10) - '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.10) - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.10) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.10) - '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.26.10) - '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-block-scoped-functions': 7.26.5(@babel/core@7.26.10) - '@babel/plugin-transform-block-scoping': 7.27.0(@babel/core@7.26.10) - '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.10) - '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-exponentiation-operator': 7.26.3(@babel/core@7.26.10) - '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-for-of': 7.26.9(@babel/core@7.26.10) - '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) - '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.26.10) - '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.26.10) - '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.10) - '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-template-literals': 7.26.8(@babel/core@7.26.10) - '@babel/plugin-transform-typeof-symbol': 7.27.0(@babel/core@7.26.10) - '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.10) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.10) - babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.26.10) - babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.26.10) - babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.26.10) + '@babel/compat-data': 7.27.5 + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.27.4) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.27.4) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.27.4) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.27.4) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.27.4) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-block-scoped-functions': 7.26.5(@babel/core@7.27.4) + '@babel/plugin-transform-block-scoping': 7.27.0(@babel/core@7.27.4) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.27.4) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-exponentiation-operator': 7.26.3(@babel/core@7.27.4) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-for-of': 7.26.9(@babel/core@7.27.4) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.27.4) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.27.4) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.27.4) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.27.4) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-template-literals': 7.26.8(@babel/core@7.27.4) + '@babel/plugin-transform-typeof-symbol': 7.27.0(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.27.4) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.27.4) + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.27.4) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.4) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.27.4) core-js-compat: 3.41.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-flow@7.25.9(@babel/core@7.26.10)': + '@babel/preset-flow@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-option': 7.25.9 - '@babel/plugin-transform-flow-strip-types': 7.26.5(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-flow-strip-types': 7.26.5(@babel/core@7.27.4) - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.10)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/types': 7.27.0 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.27.6 esutils: 2.0.3 - '@babel/preset-typescript@7.27.0(@babel/core@7.26.10)': + '@babel/preset-typescript@7.27.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-option': 7.25.9 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) - '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.27.4) + '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.27.4) transitivePeerDependencies: - supports-color - '@babel/register@7.25.9(@babel/core@7.26.10)': + '@babel/register@7.25.9(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -7674,6 +5744,12 @@ snapshots: '@babel/parser': 7.27.0 '@babel/types': 7.27.0 + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 + '@babel/traverse@7.27.0': dependencies: '@babel/code-frame': 7.26.2 @@ -7686,11 +5762,30 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.27.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.5 + '@babel/template': 7.27.2 + '@babel/types': 7.27.6 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/types@7.27.0': dependencies: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.27.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + '@chainsafe/as-chacha20poly1305@0.1.0': {} '@chainsafe/as-sha256@1.0.1': {} @@ -7756,6 +5851,97 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 + '@emnapi/core@1.4.3': + dependencies: + '@emnapi/wasi-threads': 1.0.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + '@eslint-community/eslint-utils@4.5.1(eslint@9.24.0)': dependencies: eslint: 9.24.0 @@ -8012,10 +6198,57 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jest/console@30.0.1': + dependencies: + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + chalk: 4.1.2 + jest-message-util: 30.0.1 + jest-util: 30.0.1 + slash: 3.0.0 + + '@jest/core@30.0.1': + dependencies: + '@jest/console': 30.0.1 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.0.1 + '@jest/test-result': 30.0.1 + '@jest/transform': 30.0.1 + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.2.0 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.0.1 + jest-config: 30.0.1(@types/node@22.14.0) + jest-haste-map: 30.0.1 + jest-message-util: 30.0.1 + jest-regex-util: 30.0.1 + jest-resolve: 30.0.1 + jest-resolve-dependencies: 30.0.1 + jest-runner: 30.0.1 + jest-runtime: 30.0.1 + jest-snapshot: 30.0.1 + jest-util: 30.0.1 + jest-validate: 30.0.1 + jest-watcher: 30.0.1 + micromatch: 4.0.8 + pretty-format: 30.0.1 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + '@jest/create-cache-key-function@29.7.0': dependencies: '@jest/types': 29.6.3 + '@jest/diff-sequences@30.0.1': {} + '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 @@ -8023,6 +6256,24 @@ snapshots: '@types/node': 22.14.0 jest-mock: 29.7.0 + '@jest/environment@30.0.1': + dependencies: + '@jest/fake-timers': 30.0.1 + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + jest-mock: 30.0.1 + + '@jest/expect-utils@30.0.1': + dependencies: + '@jest/get-type': 30.0.1 + + '@jest/expect@30.0.1': + dependencies: + expect: 30.0.1 + jest-snapshot: 30.0.1 + transitivePeerDependencies: + - supports-color + '@jest/fake-timers@29.7.0': dependencies: '@jest/types': 29.6.3 @@ -8032,13 +6283,97 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 + '@jest/fake-timers@30.0.1': + dependencies: + '@jest/types': 30.0.1 + '@sinonjs/fake-timers': 13.0.5 + '@types/node': 22.14.0 + jest-message-util: 30.0.1 + jest-mock: 30.0.1 + jest-util: 30.0.1 + + '@jest/get-type@30.0.1': {} + + '@jest/globals@30.0.1': + dependencies: + '@jest/environment': 30.0.1 + '@jest/expect': 30.0.1 + '@jest/types': 30.0.1 + jest-mock: 30.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 22.14.0 + jest-regex-util: 30.0.1 + + '@jest/reporters@30.0.1': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.0.1 + '@jest/test-result': 30.0.1 + '@jest/transform': 30.0.1 + '@jest/types': 30.0.1 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 22.14.0 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit-x: 0.2.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + jest-message-util: 30.0.1 + jest-util: 30.0.1 + jest-worker: 30.0.1 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 + '@jest/schemas@30.0.1': + dependencies: + '@sinclair/typebox': 0.34.35 + + '@jest/snapshot-utils@30.0.1': + dependencies: + '@jest/types': 30.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.0.1': + dependencies: + '@jest/console': 30.0.1 + '@jest/types': 30.0.1 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@30.0.1': + dependencies: + '@jest/test-result': 30.0.1 + graceful-fs: 4.2.11 + jest-haste-map: 30.0.1 + slash: 3.0.0 + '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 @@ -8056,6 +6391,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/transform@30.0.1': + dependencies: + '@babel/core': 7.27.4 + '@jest/types': 30.0.1 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 7.0.0 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.0.1 + jest-regex-util: 30.0.1 + jest-util: 30.0.1 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + '@jest/types@29.6.3': dependencies: '@jest/schemas': 29.6.3 @@ -8065,6 +6420,16 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jest/types@30.0.1': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.1 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.14.0 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -8438,7 +6803,7 @@ snapshots: uint8arraylist: 2.4.8 uint8arrays: 5.1.0 - '@libp2p/webrtc@5.2.9(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0))': + '@libp2p/webrtc@5.2.9(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0))': dependencies: '@chainsafe/is-ip': 2.1.0 '@chainsafe/libp2p-noise': 16.1.0 @@ -8466,7 +6831,7 @@ snapshots: protons-runtime: 5.5.0 race-event: 1.3.0 race-signal: 1.1.3 - react-native-webrtc: 124.0.5(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0)) + react-native-webrtc: 124.0.5(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0)) uint8-varint: 2.0.4 uint8arraylist: 2.4.8 uint8arrays: 5.1.0 @@ -8535,6 +6900,13 @@ snapshots: '@multiformats/multiaddr': 12.4.0 is-ip: 5.0.1 + '@napi-rs/wasm-runtime@0.2.11': + dependencies: + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 + '@tybys/wasm-util': 0.9.0 + optional: true + '@noble/ciphers@1.2.1': {} '@noble/curves@1.8.1': @@ -8569,10 +6941,10 @@ snapshots: timeout-abort-controller: 3.0.0 uint8arrays: 5.1.0 - '@orbitdb/feed-db@1.1.2(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0))': + '@orbitdb/feed-db@1.1.2(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0))': dependencies: '@orbitdb/core': 2.5.0 - helia: 5.3.0(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0)) + helia: 5.3.0(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0)) transitivePeerDependencies: - bufferutil - react-native @@ -8686,84 +7058,86 @@ snapshots: '@pkgr/core@0.2.1': {} + '@pkgr/core@0.2.7': {} + '@react-native/assets-registry@0.78.1': {} - '@react-native/babel-plugin-codegen@0.78.1(@babel/preset-env@7.26.9(@babel/core@7.26.10))': + '@react-native/babel-plugin-codegen@0.78.1(@babel/preset-env@7.26.9(@babel/core@7.27.4))': dependencies: - '@babel/traverse': 7.27.0 - '@react-native/codegen': 0.78.1(@babel/preset-env@7.26.9(@babel/core@7.26.10)) + '@babel/traverse': 7.27.4 + '@react-native/codegen': 0.78.1(@babel/preset-env@7.26.9(@babel/core@7.27.4)) transitivePeerDependencies: - '@babel/preset-env' - supports-color - '@react-native/babel-preset@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))': + '@react-native/babel-preset@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))': dependencies: - '@babel/core': 7.26.10 - '@babel/plugin-proposal-export-default-from': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-syntax-export-default-from': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.26.10) - '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-block-scoping': 7.27.0(@babel/core@7.26.10) - '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-flow-strip-types': 7.26.5(@babel/core@7.26.10) - '@babel/plugin-transform-for-of': 7.26.9(@babel/core@7.26.10) - '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) - '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.26.10) - '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.26.10) - '@babel/plugin-transform-runtime': 7.26.10(@babel/core@7.26.10) - '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10) - '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.10) - '@babel/template': 7.27.0 - '@react-native/babel-plugin-codegen': 0.78.1(@babel/preset-env@7.26.9(@babel/core@7.26.10)) + '@babel/core': 7.27.4 + '@babel/plugin-proposal-export-default-from': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-export-default-from': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.27.4) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-block-scoping': 7.27.0(@babel/core@7.27.4) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-flow-strip-types': 7.26.5(@babel/core@7.27.4) + '@babel/plugin-transform-for-of': 7.26.9(@babel/core@7.27.4) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.27.4) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.27.4) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.27.4) + '@babel/plugin-transform-runtime': 7.26.10(@babel/core@7.27.4) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.27.4) + '@babel/template': 7.27.2 + '@react-native/babel-plugin-codegen': 0.78.1(@babel/preset-env@7.26.9(@babel/core@7.27.4)) babel-plugin-syntax-hermes-parser: 0.25.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.10) + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.4) react-refresh: 0.14.2 transitivePeerDependencies: - '@babel/preset-env' - supports-color - '@react-native/codegen@0.78.1(@babel/preset-env@7.26.9(@babel/core@7.26.10))': + '@react-native/codegen@0.78.1(@babel/preset-env@7.26.9(@babel/core@7.27.4))': dependencies: - '@babel/parser': 7.27.0 - '@babel/preset-env': 7.26.9(@babel/core@7.26.10) + '@babel/parser': 7.27.5 + '@babel/preset-env': 7.26.9(@babel/core@7.27.4) glob: 7.2.3 hermes-parser: 0.25.1 invariant: 2.2.4 - jscodeshift: 17.3.0(@babel/preset-env@7.26.9(@babel/core@7.26.10)) + jscodeshift: 17.3.0(@babel/preset-env@7.26.9(@babel/core@7.27.4)) nullthrows: 1.1.1 yargs: 17.7.2 transitivePeerDependencies: - supports-color - '@react-native/community-cli-plugin@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))': + '@react-native/community-cli-plugin@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))': dependencies: '@react-native/dev-middleware': 0.78.1 - '@react-native/metro-babel-transformer': 0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10)) + '@react-native/metro-babel-transformer': 0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4)) chalk: 4.1.2 debug: 2.6.9 invariant: 2.2.4 @@ -8771,7 +7145,7 @@ snapshots: metro-config: 0.81.4 metro-core: 0.81.4 readline: 1.3.0 - semver: 7.7.1 + semver: 7.7.2 transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' @@ -8804,10 +7178,10 @@ snapshots: '@react-native/js-polyfills@0.78.1': {} - '@react-native/metro-babel-transformer@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))': + '@react-native/metro-babel-transformer@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))': dependencies: - '@babel/core': 7.26.10 - '@react-native/babel-preset': 0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10)) + '@babel/core': 7.27.4 + '@react-native/babel-preset': 0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4)) hermes-parser: 0.25.1 nullthrows: 1.1.1 transitivePeerDependencies: @@ -8816,15 +7190,17 @@ snapshots: '@react-native/normalize-colors@0.78.1': {} - '@react-native/virtualized-lists@0.78.1(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0))(react@19.1.0)': + '@react-native/virtualized-lists@0.78.1(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0))(react@19.1.0)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.1.0 - react-native: 0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0) + react-native: 0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0) '@sinclair/typebox@0.27.8': {} + '@sinclair/typebox@0.34.35': {} + '@sindresorhus/fnv1a@3.1.0': {} '@sinonjs/commons@3.0.1': @@ -8835,8 +7211,17 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@13.0.5': + dependencies: + '@sinonjs/commons': 3.0.1 + '@topoconfig/extends@0.16.2': {} + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.27.0 @@ -8902,6 +7287,11 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jest@30.0.0': + dependencies: + expect: 30.0.1 + pretty-format: 30.0.1 + '@types/json-schema@7.0.15': {} '@types/mime@1.3.5': {} @@ -8911,6 +7301,11 @@ snapshots: '@types/dns-packet': 5.6.5 '@types/node': 22.13.16 + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.14.0 + form-data: 4.0.2 + '@types/node-forge@1.3.11': dependencies: '@types/node': 22.13.16 @@ -9037,6 +7432,67 @@ snapshots: '@typescript-eslint/types': 8.29.0 eslint-visitor-keys: 4.2.0 + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.9.0': + optional: true + + '@unrs/resolver-binding-android-arm64@1.9.0': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.9.0': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.9.0': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.9.0': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.9.0': + dependencies: + '@napi-rs/wasm-runtime': 0.2.11 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.9.0': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.9.0': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.9.0': + optional: true + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -9090,6 +7546,10 @@ snapshots: anser@1.4.10: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-escapes@7.0.0: dependencies: environment: 1.1.0 @@ -9145,13 +7605,26 @@ snapshots: transitivePeerDependencies: - debug - babel-jest@29.7.0(@babel/core@7.26.10): + babel-jest@29.7.0(@babel/core@7.27.4): dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.26.10) + babel-preset-jest: 29.6.3(@babel/core@7.27.4) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-jest@30.0.1(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + '@jest/transform': 30.0.1 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.0 + babel-preset-jest: 30.0.1(@babel/core@7.27.4) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -9160,7 +7633,7 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-plugin-utils': 7.27.1 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -9168,34 +7641,50 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-istanbul@7.0.0: + dependencies: + '@babel/helper-plugin-utils': 7.26.5 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/template': 7.27.2 + '@babel/types': 7.27.6 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.7 - babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.26.10): + babel-plugin-jest-hoist@30.0.1: dependencies: - '@babel/compat-data': 7.26.8 - '@babel/core': 7.26.10 - '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) + '@babel/template': 7.27.2 + '@babel/types': 7.27.6 + '@types/babel__core': 7.20.5 + + babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.27.4): + dependencies: + '@babel/compat-data': 7.27.5 + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.4) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.26.10): + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.27.4): dependencies: - '@babel/core': 7.26.10 - '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.4) core-js-compat: 3.41.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.26.10): + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.27.4): dependencies: - '@babel/core': 7.26.10 - '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.4) transitivePeerDependencies: - supports-color @@ -9203,36 +7692,42 @@ snapshots: dependencies: hermes-parser: 0.25.1 - babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.26.10): + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.27.4): dependencies: - '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.27.4) transitivePeerDependencies: - '@babel/core' - babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.10): + babel-preset-current-node-syntax@1.1.0(@babel/core@7.27.4): dependencies: - '@babel/core': 7.26.10 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.10) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.10) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.10) - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.10) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.10) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.10) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.10) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.10) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.10) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.27.4) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.27.4) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.27.4) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.27.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.4) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.27.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.27.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.4) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.4) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.4) - babel-preset-jest@29.6.3(@babel/core@7.26.10): + babel-preset-jest@29.6.3(@babel/core@7.27.4): dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.10) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.4) + + babel-preset-jest@30.0.1(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + babel-plugin-jest-hoist: 30.0.1 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.4) balanced-match@1.0.2: {} @@ -9314,6 +7809,10 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -9371,6 +7870,8 @@ snapshots: chalk@5.4.1: {} + char-regex@1.0.2: {} + chownr@1.1.4: {} chrome-launcher@0.15.2: @@ -9397,6 +7898,10 @@ snapshots: ci-info@3.9.0: {} + ci-info@4.2.0: {} + + cjs-module-lexer@2.1.0: {} + classic-level@1.4.1: dependencies: abstract-level: 1.0.4 @@ -9432,6 +7937,10 @@ snapshots: clone@2.1.2: {} + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -9529,6 +8038,17 @@ snapshots: it-sort: 3.0.7 it-take: 3.0.7 + datastore-fs@10.0.4: + dependencies: + datastore-core: 10.0.2 + interface-datastore: 8.3.1 + interface-store: 6.0.2 + it-glob: 3.0.4 + it-map: 3.1.4 + it-parallel-batch: 3.0.9 + race-signal: 1.1.3 + steno: 4.0.2 + debug@2.6.9: dependencies: ms: 2.0.0 @@ -9545,10 +8065,14 @@ snapshots: dependencies: mimic-response: 3.1.0 + dedent@1.6.0: {} + deep-extend@0.6.0: {} deep-is@0.1.4: {} + deepmerge@4.3.1: {} + delay@6.0.0: {} delayed-stream@1.0.0: {} @@ -9565,6 +8089,8 @@ snapshots: detect-libc@2.0.3: {} + detect-newline@3.1.0: {} + dns-packet@5.6.1: dependencies: '@leichtgewicht/ip-codec': 2.0.5 @@ -9579,8 +8105,14 @@ snapshots: ee-first@1.1.1: {} + ejs@3.1.10: + dependencies: + jake: 10.9.2 + electron-to-chromium@1.5.134: {} + emittery@0.13.1: {} + emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -9624,6 +8156,34 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -9724,6 +8284,18 @@ snapshots: eventemitter3@5.0.1: {} + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -9736,8 +8308,19 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + exit-x@0.2.2: {} + expand-template@2.0.3: {} + expect@30.0.1: + dependencies: + '@jest/expect-utils': 30.0.1 + '@jest/get-type': 30.0.1 + jest-matcher-utils: 30.0.1 + jest-message-util: 30.0.1 + jest-mock: 30.0.1 + jest-util: 30.0.1 + exponential-backoff@3.1.2: {} express@5.1.0: @@ -9802,6 +8385,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -9933,8 +8520,14 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@6.0.1: {} + get-stream@8.0.1: {} + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: {} glob-parent@5.1.2: @@ -9994,7 +8587,7 @@ snapshots: dependencies: function-bind: 1.1.2 - helia@5.3.0(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0)): + helia@5.3.0(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0)): dependencies: '@chainsafe/libp2p-noise': 16.1.0 '@chainsafe/libp2p-yamux': 7.0.1 @@ -10019,7 +8612,7 @@ snapshots: '@libp2p/tcp': 10.1.8 '@libp2p/tls': 2.1.1 '@libp2p/upnp-nat': 3.1.11 - '@libp2p/webrtc': 5.2.9(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0)) + '@libp2p/webrtc': 5.2.9(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0)) '@libp2p/websockets': 9.2.8 '@multiformats/dns': 1.0.6 blockstore-core: 5.0.2 @@ -10041,6 +8634,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + html-escaper@2.0.2: {} + http-cookie-agent@6.0.8(tough-cookie@5.1.2)(undici@6.21.2): dependencies: agent-base: 7.1.3 @@ -10056,6 +8651,8 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + human-signals@2.1.0: {} + human-signals@5.0.0: {} husky@8.0.3: {} @@ -10082,6 +8679,11 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + imurmurhash@0.1.4: {} inflight@1.0.6: @@ -10197,6 +8799,8 @@ snapshots: dependencies: get-east-asian-width: 1.3.0 + is-generator-fn@2.1.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -10238,14 +8842,43 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.26.10 - '@babel/parser': 7.27.0 + '@babel/core': 7.27.4 + '@babel/parser': 7.27.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 transitivePeerDependencies: - supports-color + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.26.10 + '@babel/parser': 7.27.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.4.0 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + it-all@3.0.7: {} it-batch@3.0.7: {} @@ -10272,6 +8905,10 @@ snapshots: dependencies: fast-glob: 3.3.3 + it-glob@3.0.4: + dependencies: + fast-glob: 3.3.3 + it-last@3.0.7: {} it-length-prefixed-stream@1.2.1: @@ -10303,6 +8940,10 @@ snapshots: dependencies: it-peekable: 3.0.6 + it-map@3.1.4: + dependencies: + it-peekable: 3.0.6 + it-merge@3.0.9: dependencies: it-queueless-pushable: 2.0.0 @@ -10320,6 +8961,10 @@ snapshots: dependencies: it-batch: 3.0.7 + it-parallel-batch@3.0.9: + dependencies: + it-batch: 3.0.7 + it-parallel@3.0.9: dependencies: p-defer: 4.0.1 @@ -10383,6 +9028,147 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jest-changed-files@30.0.1: + dependencies: + execa: 5.1.1 + jest-util: 30.0.1 + p-limit: 3.1.0 + + jest-circus@30.0.1: + dependencies: + '@jest/environment': 30.0.1 + '@jest/expect': 30.0.1 + '@jest/test-result': 30.0.1 + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.6.0 + is-generator-fn: 2.1.0 + jest-each: 30.0.1 + jest-matcher-utils: 30.0.1 + jest-message-util: 30.0.1 + jest-runtime: 30.0.1 + jest-snapshot: 30.0.1 + jest-util: 30.0.1 + p-limit: 3.1.0 + pretty-format: 30.0.1 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.0.1(@types/node@22.13.16): + dependencies: + '@jest/core': 30.0.1 + '@jest/test-result': 30.0.1 + '@jest/types': 30.0.1 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.0.1(@types/node@22.13.16) + jest-util: 30.0.1 + jest-validate: 30.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.0.1(@types/node@22.13.16): + dependencies: + '@babel/core': 7.27.4 + '@jest/get-type': 30.0.1 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.0.1 + '@jest/types': 30.0.1 + babel-jest: 30.0.1(@babel/core@7.27.4) + chalk: 4.1.2 + ci-info: 4.2.0 + deepmerge: 4.3.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-circus: 30.0.1 + jest-docblock: 30.0.1 + jest-environment-node: 30.0.1 + jest-regex-util: 30.0.1 + jest-resolve: 30.0.1 + jest-runner: 30.0.1 + jest-util: 30.0.1 + jest-validate: 30.0.1 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.0.1 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.13.16 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@30.0.1(@types/node@22.14.0): + dependencies: + '@babel/core': 7.27.4 + '@jest/get-type': 30.0.1 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.0.1 + '@jest/types': 30.0.1 + babel-jest: 30.0.1(@babel/core@7.27.4) + chalk: 4.1.2 + ci-info: 4.2.0 + deepmerge: 4.3.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-circus: 30.0.1 + jest-docblock: 30.0.1 + jest-environment-node: 30.0.1 + jest-regex-util: 30.0.1 + jest-resolve: 30.0.1 + jest-runner: 30.0.1 + jest-util: 30.0.1 + jest-validate: 30.0.1 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.0.1 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.14.0 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.0.1: + dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.0.1 + chalk: 4.1.2 + pretty-format: 30.0.1 + + jest-docblock@30.0.1: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.0.1: + dependencies: + '@jest/get-type': 30.0.1 + '@jest/types': 30.0.1 + chalk: 4.1.2 + jest-util: 30.0.1 + pretty-format: 30.0.1 + jest-environment-node@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -10392,6 +9178,16 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 + jest-environment-node@30.0.1: + dependencies: + '@jest/environment': 30.0.1 + '@jest/fake-timers': 30.0.1 + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + jest-mock: 30.0.1 + jest-util: 30.0.1 + jest-validate: 30.0.1 + jest-get-type@29.6.3: {} jest-haste-map@29.7.0: @@ -10410,9 +9206,36 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + jest-haste-map@30.0.1: + dependencies: + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.0.1 + jest-worker: 30.0.1 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.0.1: + dependencies: + '@jest/get-type': 30.0.1 + pretty-format: 30.0.1 + + jest-matcher-utils@30.0.1: + dependencies: + '@jest/get-type': 30.0.1 + chalk: 4.1.2 + jest-diff: 30.0.1 + pretty-format: 30.0.1 + jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -10422,14 +9245,136 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 + jest-message-util@30.0.1: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 30.0.1 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 30.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 '@types/node': 22.14.0 jest-util: 29.7.0 + jest-mock@30.0.1: + dependencies: + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + jest-util: 30.0.1 + + jest-pnp-resolver@1.2.3(jest-resolve@30.0.1): + optionalDependencies: + jest-resolve: 30.0.1 + jest-regex-util@29.6.3: {} + jest-regex-util@30.0.1: {} + + jest-resolve-dependencies@30.0.1: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.0.1 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.0.1: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.0.1 + jest-pnp-resolver: 1.2.3(jest-resolve@30.0.1) + jest-util: 30.0.1 + jest-validate: 30.0.1 + slash: 3.0.0 + unrs-resolver: 1.9.0 + + jest-runner@30.0.1: + dependencies: + '@jest/console': 30.0.1 + '@jest/environment': 30.0.1 + '@jest/test-result': 30.0.1 + '@jest/transform': 30.0.1 + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.0.1 + jest-environment-node: 30.0.1 + jest-haste-map: 30.0.1 + jest-leak-detector: 30.0.1 + jest-message-util: 30.0.1 + jest-resolve: 30.0.1 + jest-runtime: 30.0.1 + jest-util: 30.0.1 + jest-watcher: 30.0.1 + jest-worker: 30.0.1 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.0.1: + dependencies: + '@jest/environment': 30.0.1 + '@jest/fake-timers': 30.0.1 + '@jest/globals': 30.0.1 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.0.1 + '@jest/transform': 30.0.1 + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + chalk: 4.1.2 + cjs-module-lexer: 2.1.0 + collect-v8-coverage: 1.0.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-haste-map: 30.0.1 + jest-message-util: 30.0.1 + jest-mock: 30.0.1 + jest-regex-util: 30.0.1 + jest-resolve: 30.0.1 + jest-snapshot: 30.0.1 + jest-util: 30.0.1 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.0.1: + dependencies: + '@babel/core': 7.27.4 + '@babel/generator': 7.27.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) + '@babel/types': 7.27.6 + '@jest/expect-utils': 30.0.1 + '@jest/get-type': 30.0.1 + '@jest/snapshot-utils': 30.0.1 + '@jest/transform': 30.0.1 + '@jest/types': 30.0.1 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.4) + chalk: 4.1.2 + expect: 30.0.1 + graceful-fs: 4.2.11 + jest-diff: 30.0.1 + jest-matcher-utils: 30.0.1 + jest-message-util: 30.0.1 + jest-util: 30.0.1 + pretty-format: 30.0.1 + semver: 7.7.2 + synckit: 0.11.8 + transitivePeerDependencies: + - supports-color + jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 @@ -10439,6 +9384,15 @@ snapshots: graceful-fs: 4.2.11 picomatch: 2.3.1 + jest-util@30.0.1: + dependencies: + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + chalk: 4.1.2 + ci-info: 4.2.0 + graceful-fs: 4.2.11 + picomatch: 4.0.2 + jest-validate@29.7.0: dependencies: '@jest/types': 29.6.3 @@ -10448,6 +9402,26 @@ snapshots: leven: 3.1.0 pretty-format: 29.7.0 + jest-validate@30.0.1: + dependencies: + '@jest/get-type': 30.0.1 + '@jest/types': 30.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.0.1 + + jest-watcher@30.0.1: + dependencies: + '@jest/test-result': 30.0.1 + '@jest/types': 30.0.1 + '@types/node': 22.14.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.0.1 + string-length: 4.0.2 + jest-worker@29.7.0: dependencies: '@types/node': 22.14.0 @@ -10455,6 +9429,27 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest-worker@30.0.1: + dependencies: + '@types/node': 22.14.0 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.0.1 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.0.1(@types/node@22.13.16): + dependencies: + '@jest/core': 30.0.1 + '@jest/types': 30.0.1 + import-local: 3.2.0 + jest-cli: 30.0.1(@types/node@22.13.16) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -10468,18 +9463,18 @@ snapshots: jsc-safe-url@0.2.4: {} - jscodeshift@17.3.0(@babel/preset-env@7.26.9(@babel/core@7.26.10)): + jscodeshift@17.3.0(@babel/preset-env@7.26.9(@babel/core@7.27.4)): dependencies: - '@babel/core': 7.26.10 - '@babel/parser': 7.27.0 - '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) - '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.26.10) - '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.10) - '@babel/preset-flow': 7.25.9(@babel/core@7.26.10) - '@babel/preset-typescript': 7.27.0(@babel/core@7.26.10) - '@babel/register': 7.25.9(@babel/core@7.26.10) + '@babel/core': 7.27.4 + '@babel/parser': 7.27.5 + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.27.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.27.4) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.27.4) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.27.4) + '@babel/preset-flow': 7.25.9(@babel/core@7.27.4) + '@babel/preset-typescript': 7.27.0(@babel/core@7.27.4) + '@babel/register': 7.25.9(@babel/core@7.27.4) flow-parser: 0.266.1 graceful-fs: 4.2.11 micromatch: 4.0.8 @@ -10489,7 +9484,7 @@ snapshots: tmp: 0.2.3 write-file-atomic: 5.0.1 optionalDependencies: - '@babel/preset-env': 7.26.9(@babel/core@7.26.10) + '@babel/preset-env': 7.26.9(@babel/core@7.27.4) transitivePeerDependencies: - supports-color @@ -10501,6 +9496,8 @@ snapshots: json-parse-better-errors@1.0.2: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -10581,6 +9578,8 @@ snapshots: lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} + lint-staged@15.5.0: dependencies: chalk: 5.4.1 @@ -10620,6 +9619,8 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} lodash.throttle@4.1.1: {} @@ -10660,6 +9661,12 @@ snapshots: pify: 4.0.1 semver: 5.7.2 + make-dir@4.0.0: + dependencies: + semver: 7.7.1 + + make-error@1.3.6: {} + makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -10684,7 +9691,7 @@ snapshots: metro-babel-transformer@0.81.4: dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.4 flow-enums-runtime: 0.0.6 hermes-parser: 0.25.1 nullthrows: 1.1.1 @@ -10752,9 +9759,9 @@ snapshots: metro-source-map@0.81.4: dependencies: - '@babel/traverse': 7.27.0 - '@babel/traverse--for-generate-function-map': '@babel/traverse@7.27.0' - '@babel/types': 7.27.0 + '@babel/traverse': 7.27.4 + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.27.4' + '@babel/types': 7.27.6 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.81.4 @@ -10778,10 +9785,10 @@ snapshots: metro-transform-plugins@0.81.4: dependencies: - '@babel/core': 7.26.10 - '@babel/generator': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.27.0 + '@babel/core': 7.27.4 + '@babel/generator': 7.27.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: @@ -10789,10 +9796,10 @@ snapshots: metro-transform-worker@0.81.4: dependencies: - '@babel/core': 7.26.10 - '@babel/generator': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/core': 7.27.4 + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 flow-enums-runtime: 0.0.6 metro: 0.81.4 metro-babel-transformer: 0.81.4 @@ -10809,13 +9816,13 @@ snapshots: metro@0.81.4: dependencies: - '@babel/code-frame': 7.26.2 - '@babel/core': 7.26.10 - '@babel/generator': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/code-frame': 7.27.1 + '@babel/core': 7.27.4 + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -10873,6 +9880,8 @@ snapshots: mime@1.6.0: {} + mimic-fn@2.1.0: {} + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} @@ -10883,6 +9892,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -10928,6 +9941,8 @@ snapshots: napi-macros@2.2.2: {} + napi-postinstall@0.2.4: {} + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -10960,6 +9975,10 @@ snapshots: normalize-path@3.0.0: {} + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -10990,6 +10009,10 @@ snapshots: dependencies: fn.name: 1.1.0 + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -11068,6 +10091,13 @@ snapshots: error-ex: 1.3.2 json-parse-better-errors: 1.0.2 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parseurl@1.3.3: {} path-exists@3.0.0: {} @@ -11093,6 +10123,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.2: {} + pidtree@0.6.0: {} pify@4.0.1: {} @@ -11103,6 +10135,10 @@ snapshots: dependencies: find-up: 3.0.0 + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + prebuild-install@7.1.3: dependencies: detect-libc: 2.0.3 @@ -11132,6 +10168,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-format@30.0.1: + dependencies: + '@jest/schemas': 30.0.1 + ansi-styles: 5.2.0 + react-is: 18.3.1 + progress-events@1.0.1: {} promise@8.3.0: @@ -11158,6 +10200,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@7.0.1: {} + pvtsutils@1.3.6: dependencies: tslib: 2.8.1 @@ -11216,29 +10260,29 @@ snapshots: react-is@18.3.1: {} - react-native-webrtc@124.0.5(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0)): + react-native-webrtc@124.0.5(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0)): dependencies: base64-js: 1.5.1 debug: 4.3.4 event-target-shim: 6.0.2 - react-native: 0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0) + react-native: 0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0) transitivePeerDependencies: - supports-color - react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0): + react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.78.1 - '@react-native/codegen': 0.78.1(@babel/preset-env@7.26.9(@babel/core@7.26.10)) - '@react-native/community-cli-plugin': 0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10)) + '@react-native/codegen': 0.78.1(@babel/preset-env@7.26.9(@babel/core@7.27.4)) + '@react-native/community-cli-plugin': 0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4)) '@react-native/gradle-plugin': 0.78.1 '@react-native/js-polyfills': 0.78.1 '@react-native/normalize-colors': 0.78.1 - '@react-native/virtualized-lists': 0.78.1(react-native@0.78.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react@19.1.0))(react@19.1.0) + '@react-native/virtualized-lists': 0.78.1(react-native@0.78.1(@babel/core@7.27.4)(@babel/preset-env@7.26.9(@babel/core@7.27.4))(react@19.1.0))(react@19.1.0) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 - babel-jest: 29.7.0(@babel/core@7.26.10) + babel-jest: 29.7.0(@babel/core@7.27.4) babel-plugin-syntax-hermes-parser: 0.25.1 base64-js: 1.5.1 chalk: 4.1.2 @@ -11259,7 +10303,7 @@ snapshots: react-refresh: 0.14.2 regenerator-runtime: 0.13.11 scheduler: 0.25.0 - semver: 7.7.1 + semver: 7.7.2 stacktrace-parser: 0.1.11 whatwg-fetch: 3.6.20 ws: 6.2.3 @@ -11325,12 +10369,18 @@ snapshots: require-directory@2.1.1: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@3.0.0: {} resolve-from@4.0.0: {} resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -11403,6 +10453,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.2: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -11527,6 +10579,11 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -11564,6 +10621,11 @@ snapshots: string-argv@0.3.2: {} + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -11594,6 +10656,10 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + strip-final-newline@3.0.0: {} strip-json-comments@2.0.1: {} @@ -11623,6 +10689,10 @@ snapshots: '@pkgr/core': 0.2.1 tslib: 2.8.1 + synckit@0.11.8: + dependencies: + '@pkgr/core': 0.2.7 + tar-fs@2.1.2: dependencies: chownr: 1.1.4 @@ -11701,6 +10771,26 @@ snapshots: dependencies: typescript: 5.8.2 + ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@30.0.1)(@jest/types@30.0.1)(babel-jest@30.0.1(@babel/core@7.27.4))(jest-util@30.0.1)(jest@30.0.1(@types/node@22.13.16))(typescript@5.8.2): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 30.0.1(@types/node@22.13.16) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.8.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.27.4 + '@jest/transform': 30.0.1 + '@jest/types': 30.0.1 + babel-jest: 30.0.1(@babel/core@7.27.4) + jest-util: 30.0.1 + tsc-esm-fix@3.1.2: dependencies: '@topoconfig/extends': 0.16.2 @@ -11714,6 +10804,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.20.3: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + tsyringe@4.8.0: dependencies: tslib: 1.14.1 @@ -11728,8 +10825,12 @@ snapshots: type-detect@4.0.8: {} + type-fest@0.21.3: {} + type-fest@0.7.1: {} + type-fest@4.41.0: {} + type-flag@3.0.0: {} type-is@2.0.1: @@ -11784,6 +10885,30 @@ snapshots: unpipe@1.0.0: {} + unrs-resolver@1.9.0: + dependencies: + napi-postinstall: 0.2.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.9.0 + '@unrs/resolver-binding-android-arm64': 1.9.0 + '@unrs/resolver-binding-darwin-arm64': 1.9.0 + '@unrs/resolver-binding-darwin-x64': 1.9.0 + '@unrs/resolver-binding-freebsd-x64': 1.9.0 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.9.0 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.9.0 + '@unrs/resolver-binding-linux-arm64-gnu': 1.9.0 + '@unrs/resolver-binding-linux-arm64-musl': 1.9.0 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.9.0 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.9.0 + '@unrs/resolver-binding-linux-riscv64-musl': 1.9.0 + '@unrs/resolver-binding-linux-s390x-gnu': 1.9.0 + '@unrs/resolver-binding-linux-x64-gnu': 1.9.0 + '@unrs/resolver-binding-linux-x64-musl': 1.9.0 + '@unrs/resolver-binding-wasm32-wasi': 1.9.0 + '@unrs/resolver-binding-win32-arm64-msvc': 1.9.0 + '@unrs/resolver-binding-win32-ia32-msvc': 1.9.0 + '@unrs/resolver-binding-win32-x64-msvc': 1.9.0 + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -11800,6 +10925,12 @@ snapshots: utils-merge@1.0.1: {} + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + vary@1.1.2: {} vlq@1.0.1: {} diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index ac5adc6..0000000 --- a/src/config.ts +++ /dev/null @@ -1,74 +0,0 @@ -import path from 'path'; - -export interface DebrosConfig { - env: { - isDevelopment: boolean; - port: string | number; - fingerprint: string; - nickname?: string; - keyPath: string; - host: string; - }; - features: { - enableLoadBalancing: boolean; - }; - ipfs: { - repo: string; - swarmKey: string; - bootstrapNodes?: string; - blockstorePath: string; - serviceDiscovery: { - topic: string; - heartbeatInterval: number; - staleTimeout: number; - logInterval: number; - publicAddress: string; - }; - }; - orbitdb: { - directory: string; - }; - loadBalancer: { - maxConnections: number; - strategy: string; - }; -} - -// Default configuration values -export const defaultConfig: DebrosConfig = { - env: { - isDevelopment: process.env.NODE_ENV !== 'production', - port: process.env.PORT || 7777, - fingerprint: process.env.FINGERPRINT || 'default-fingerprint', - nickname: process.env.NICKNAME, - keyPath: process.env.KEY_PATH || '/var/lib/debros/keys', - host: process.env.HOST || '', - }, - features: { - enableLoadBalancing: process.env.ENABLE_LOAD_BALANCING !== 'false', - }, - ipfs: { - repo: './ipfs-repo', - swarmKey: path.resolve(process.cwd(), 'swarm.key'), - bootstrapNodes: process.env.BOOTSTRAP_NODES, - blockstorePath: path.resolve(process.cwd(), 'blockstore'), - serviceDiscovery: { - topic: process.env.SERVICE_DISCOVERY_TOPIC || 'debros-service-discovery', - heartbeatInterval: parseInt(process.env.HEARTBEAT_INTERVAL || '5000'), - staleTimeout: parseInt(process.env.STALE_PEER_TIMEOUT || '30000'), - logInterval: parseInt(process.env.PEER_LOG_INTERVAL || '60000'), - publicAddress: - process.env.NODE_PUBLIC_ADDRESS || `http://localhost:${process.env.PORT || 7777}`, - }, - }, - orbitdb: { - directory: path.resolve(process.cwd(), 'orbitdb/debros'), - }, - loadBalancer: { - maxConnections: parseInt(process.env.MAX_CONNECTIONS || '1000'), - strategy: process.env.LOAD_BALANCING_STRATEGY || 'least-loaded', - }, -}; - -// Export a singleton config -export const config = defaultConfig; diff --git a/src/db/core/connection.ts b/src/db/core/connection.ts deleted file mode 100644 index d202b9a..0000000 --- a/src/db/core/connection.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { createServiceLogger } from '../../utils/logger'; -import { init as initIpfs, stop as stopIpfs } from '../../ipfs/ipfsService'; -import { init as initOrbitDB } from '../../orbit/orbitDBService'; -import { DBConnection, ErrorCode } from '../types'; -import { DBError } from './error'; - -const logger = createServiceLogger('DB_CONNECTION'); - -// Connection pool of database instances -const connections = new Map(); -let defaultConnectionId: string | null = null; -let cleanupInterval: NodeJS.Timeout | null = null; - -// Configuration -const CONNECTION_TIMEOUT = 3600000; // 1 hour in milliseconds -const CLEANUP_INTERVAL = 300000; // 5 minutes in milliseconds -const MAX_RETRY_ATTEMPTS = 3; -const RETRY_DELAY = 2000; // 2 seconds - -/** - * Initialize the database service - * This abstracts away OrbitDB and IPFS from the end user - */ -export const init = async (connectionId?: string): Promise => { - // Start connection cleanup interval if not already running - if (!cleanupInterval) { - cleanupInterval = setInterval(cleanupStaleConnections, CLEANUP_INTERVAL); - logger.info(`Connection cleanup scheduled every ${CLEANUP_INTERVAL / 60000} minutes`); - } - - const connId = connectionId || `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - // Check if connection already exists - if (connections.has(connId)) { - const existingConnection = connections.get(connId)!; - if (existingConnection.isActive) { - logger.info(`Using existing active connection: ${connId}`); - return connId; - } - } - - logger.info(`Initializing DB service with connection ID: ${connId}`); - - let attempts = 0; - let lastError: any = null; - - // Retry initialization with exponential backoff - while (attempts < MAX_RETRY_ATTEMPTS) { - try { - // Initialize IPFS with retry logic - const ipfsInstance = await initIpfs().catch((error) => { - logger.error( - `IPFS initialization failed (attempt ${attempts + 1}/${MAX_RETRY_ATTEMPTS}):`, - error, - ); - throw error; - }); - - // Initialize OrbitDB - const orbitdbInstance = await initOrbitDB().catch((error) => { - logger.error( - `OrbitDB initialization failed (attempt ${attempts + 1}/${MAX_RETRY_ATTEMPTS}):`, - error, - ); - throw error; - }); - - // Store connection in pool - connections.set(connId, { - ipfs: ipfsInstance, - orbitdb: orbitdbInstance, - timestamp: Date.now(), - isActive: true, - }); - - // Set as default if no default exists - if (!defaultConnectionId) { - defaultConnectionId = connId; - } - - logger.info(`DB service initialized successfully with connection ID: ${connId}`); - return connId; - } catch (error) { - lastError = error; - attempts++; - - if (attempts >= MAX_RETRY_ATTEMPTS) { - logger.error( - `Failed to initialize DB service after ${MAX_RETRY_ATTEMPTS} attempts:`, - error, - ); - break; - } - - // Wait before retrying with exponential backoff - const delay = RETRY_DELAY * Math.pow(2, attempts - 1); - logger.info( - `Retrying initialization in ${delay}ms (attempt ${attempts + 1}/${MAX_RETRY_ATTEMPTS})...`, - ); - - // Clean up any partial initialization before retrying - try { - await stopIpfs(); - } catch (cleanupError) { - logger.warn('Error during cleanup before retry:', cleanupError); - } - - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - - throw new DBError( - ErrorCode.INITIALIZATION_FAILED, - `Failed to initialize database service after ${MAX_RETRY_ATTEMPTS} attempts`, - lastError, - ); -}; - -/** - * Get the active connection - */ -export const getConnection = (connectionId?: string): DBConnection => { - const connId = connectionId || defaultConnectionId; - - if (!connId || !connections.has(connId)) { - throw new DBError( - ErrorCode.NOT_INITIALIZED, - `No active database connection found${connectionId ? ` for ID: ${connectionId}` : ''}`, - ); - } - - const connection = connections.get(connId)!; - - if (!connection.isActive) { - throw new DBError(ErrorCode.CONNECTION_ERROR, `Connection ${connId} is no longer active`); - } - - // Update the timestamp to mark connection as recently used - connection.timestamp = Date.now(); - - return connection; -}; - -/** - * Cleanup stale connections to prevent memory leaks - */ -export const cleanupStaleConnections = (): void => { - try { - const now = Date.now(); - let removedCount = 0; - - // Identify stale connections (older than CONNECTION_TIMEOUT) - for (const [id, connection] of connections.entries()) { - if (connection.isActive && now - connection.timestamp > CONNECTION_TIMEOUT) { - logger.info( - `Closing stale connection: ${id} (inactive for ${(now - connection.timestamp) / 60000} minutes)`, - ); - - // Close connection asynchronously (don't await to avoid blocking) - closeConnection(id) - .then((success) => { - if (success) { - logger.info(`Successfully closed stale connection: ${id}`); - } else { - logger.warn(`Failed to close stale connection: ${id}`); - } - }) - .catch((error) => { - logger.error(`Error closing stale connection ${id}:`, error); - }); - - removedCount++; - } else if (!connection.isActive) { - // Remove inactive connections from the map - connections.delete(id); - removedCount++; - } - } - - if (removedCount > 0) { - logger.info(`Cleaned up ${removedCount} stale or inactive connections`); - } - } catch (error) { - logger.error('Error during connection cleanup:', error); - } -}; - -/** - * Close a specific database connection - */ -export const closeConnection = async (connectionId: string): Promise => { - if (!connections.has(connectionId)) { - return false; - } - - try { - const connection = connections.get(connectionId)!; - - // Stop OrbitDB - if (connection.orbitdb) { - await connection.orbitdb.stop(); - } - - // Mark connection as inactive - connection.isActive = false; - - // If this was the default connection, clear it - if (defaultConnectionId === connectionId) { - defaultConnectionId = null; - - // Try to find another active connection to be the default - for (const [id, conn] of connections.entries()) { - if (conn.isActive) { - defaultConnectionId = id; - break; - } - } - } - - // Remove the connection from the pool - connections.delete(connectionId); - - logger.info(`Closed database connection: ${connectionId}`); - return true; - } catch (error) { - logger.error(`Error closing connection ${connectionId}:`, error); - return false; - } -}; - -/** - * Stop all database connections - */ -export const stop = async (): Promise => { - try { - // Stop the cleanup interval - if (cleanupInterval) { - clearInterval(cleanupInterval); - cleanupInterval = null; - } - - // Close all connections - const promises: Promise[] = []; - for (const [id, connection] of connections.entries()) { - if (connection.isActive) { - promises.push(closeConnection(id)); - } - } - - // Wait for all connections to close - await Promise.allSettled(promises); - - // Stop IPFS if needed - const ipfs = connections.get(defaultConnectionId || '')?.ipfs; - if (ipfs) { - await stopIpfs(); - } - - // Clear all connections - connections.clear(); - defaultConnectionId = null; - - logger.info('All DB connections stopped successfully'); - } catch (error: any) { - logger.error('Error stopping DB connections:', error); - throw new Error(`Failed to stop database connections: ${error.message}`); - } -}; diff --git a/src/db/core/error.ts b/src/db/core/error.ts deleted file mode 100644 index 6efc697..0000000 --- a/src/db/core/error.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ErrorCode } from '../types'; - -// Re-export error code for easier access -export { ErrorCode }; - -// Custom error class with error codes -export class DBError extends Error { - code: ErrorCode; - details?: any; - - constructor(code: ErrorCode, message: string, details?: any) { - super(message); - this.name = 'DBError'; - this.code = code; - this.details = details; - } -} diff --git a/src/db/dbService.ts b/src/db/dbService.ts deleted file mode 100644 index 0caff52..0000000 --- a/src/db/dbService.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { createServiceLogger } from '../utils/logger'; -import { init, closeConnection, stop } from './core/connection'; -import { defineSchema } from './schema/validator'; -import * as events from './events/eventService'; -import { Transaction } from './transactions/transactionService'; -import { - StoreType, - CreateResult, - UpdateResult, - PaginatedResult, - QueryOptions, - ListOptions, - ErrorCode, -} from './types'; -import { DBError } from './core/error'; -import { getStore } from './stores/storeFactory'; -import { uploadFile, getFile, deleteFile } from './stores/fileStore'; - -// Re-export imported functions -export { init, closeConnection, stop, defineSchema, uploadFile, getFile, deleteFile }; - -const logger = createServiceLogger('DB_SERVICE'); - -/** - * Create a new transaction for batching operations - */ -export const createTransaction = (connectionId?: string): Transaction => { - return new Transaction(connectionId); -}; - -/** - * Execute all operations in a transaction - */ -export const commitTransaction = async ( - transaction: Transaction, -): Promise<{ success: boolean; results: any[] }> => { - try { - // Validate that we have operations - const operations = transaction.getOperations(); - if (operations.length === 0) { - return { success: true, results: [] }; - } - - const connectionId = transaction.getConnectionId(); - const results = []; - - // Execute all operations - for (const operation of operations) { - let result; - - switch (operation.type) { - case 'create': - result = await create(operation.collection, operation.id, operation.data, { - connectionId, - }); - break; - - case 'update': - result = await update(operation.collection, operation.id, operation.data, { - connectionId, - }); - break; - - case 'delete': - result = await remove(operation.collection, operation.id, { connectionId }); - break; - } - - results.push(result); - } - - return { success: true, results }; - } catch (error) { - logger.error('Transaction failed:', error); - throw new DBError(ErrorCode.TRANSACTION_FAILED, 'Failed to commit transaction', error); - } -}; - -/** - * Create a new document in the specified collection using the appropriate store - */ -export const create = async >( - collection: string, - id: string, - data: Omit, - options?: { connectionId?: string; storeType?: StoreType }, -): Promise => { - const storeType = options?.storeType || StoreType.KEYVALUE; - const store = getStore(storeType); - return store.create(collection, id, data, { connectionId: options?.connectionId }); -}; - -/** - * Get a document by ID from a collection - */ -export const get = async >( - collection: string, - id: string, - options?: { connectionId?: string; skipCache?: boolean; storeType?: StoreType }, -): Promise => { - const storeType = options?.storeType || StoreType.KEYVALUE; - const store = getStore(storeType); - return store.get(collection, id, options); -}; - -/** - * Update a document in a collection - */ -export const update = async >( - collection: string, - id: string, - data: Partial>, - options?: { connectionId?: string; upsert?: boolean; storeType?: StoreType }, -): Promise => { - const storeType = options?.storeType || StoreType.KEYVALUE; - const store = getStore(storeType); - return store.update(collection, id, data, options); -}; - -/** - * Delete a document from a collection - */ -export const remove = async ( - collection: string, - id: string, - options?: { connectionId?: string; storeType?: StoreType }, -): Promise => { - const storeType = options?.storeType || StoreType.KEYVALUE; - const store = getStore(storeType); - return store.remove(collection, id, options); -}; - -/** - * List all documents in a collection with pagination - */ -export const list = async >( - collection: string, - options?: ListOptions & { storeType?: StoreType }, -): Promise> => { - const storeType = options?.storeType || StoreType.KEYVALUE; - const store = getStore(storeType); - - // Remove storeType from options - const { storeType: _, ...storeOptions } = options || {}; - return store.list(collection, storeOptions); -}; - -/** - * Query documents in a collection with filtering and pagination - */ -export const query = async >( - collection: string, - filter: (doc: T) => boolean, - options?: QueryOptions & { storeType?: StoreType }, -): Promise> => { - const storeType = options?.storeType || StoreType.KEYVALUE; - const store = getStore(storeType); - - // Remove storeType from options - const { storeType: _, ...storeOptions } = options || {}; - return store.query(collection, filter, storeOptions); -}; - -/** - * Create an index for a collection to speed up queries - */ -export const createIndex = async ( - collection: string, - field: string, - options?: { connectionId?: string; storeType?: StoreType }, -): Promise => { - const storeType = options?.storeType || StoreType.KEYVALUE; - const store = getStore(storeType); - return store.createIndex(collection, field, { connectionId: options?.connectionId }); -}; - -/** - * Subscribe to database events - */ -export const subscribe = events.subscribe; - -// Re-export error types and codes -export { DBError } from './core/error'; -export { ErrorCode } from './types'; - -// Export store types -export { StoreType } from './types'; - -export default { - init, - create, - get, - update, - remove, - list, - query, - createIndex, - createTransaction, - commitTransaction, - subscribe, - uploadFile, - getFile, - deleteFile, - defineSchema, - closeConnection, - stop, - StoreType, -}; diff --git a/src/db/events/eventService.ts b/src/db/events/eventService.ts deleted file mode 100644 index f42a433..0000000 --- a/src/db/events/eventService.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { dbEvents } from '../types'; - -// Event types -type DBEventType = 'document:created' | 'document:updated' | 'document:deleted'; - -/** - * Subscribe to database events - */ -export const subscribe = (event: DBEventType, callback: (data: any) => void): (() => void) => { - dbEvents.on(event, callback); - - // Return unsubscribe function - return () => { - dbEvents.off(event, callback); - }; -}; - -/** - * Emit an event - */ -export const emit = (event: DBEventType, data: any): void => { - dbEvents.emit(event, data); -}; - -/** - * Remove all event listeners - */ -export const removeAllListeners = (): void => { - dbEvents.removeAllListeners(); -}; diff --git a/src/db/schema/validator.ts b/src/db/schema/validator.ts deleted file mode 100644 index 6501046..0000000 --- a/src/db/schema/validator.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { createServiceLogger } from '../../utils/logger'; -import { CollectionSchema, ErrorCode } from '../types'; -import { DBError } from '../core/error'; - -const logger = createServiceLogger('DB_SCHEMA'); - -// Store collection schemas -const schemas = new Map(); - -/** - * Define a schema for a collection - */ -export const defineSchema = (collection: string, schema: CollectionSchema): void => { - schemas.set(collection, schema); - logger.info(`Schema defined for collection: ${collection}`); -}; - -/** - * Validate a document against its schema - */ -export const validateDocument = (collection: string, document: any): boolean => { - const schema = schemas.get(collection); - - if (!schema) { - return true; // No schema defined, so validation passes - } - - // Check required fields - if (schema.required) { - for (const field of schema.required) { - if (document[field] === undefined) { - throw new DBError(ErrorCode.INVALID_SCHEMA, `Required field '${field}' is missing`, { - collection, - document, - }); - } - } - } - - // Validate properties - for (const [field, definition] of Object.entries(schema.properties)) { - const value = document[field]; - - // Skip undefined optional fields - if (value === undefined) { - if (definition.required) { - throw new DBError(ErrorCode.INVALID_SCHEMA, `Required field '${field}' is missing`, { - collection, - document, - }); - } - continue; - } - - // Type validation - switch (definition.type) { - case 'string': - if (typeof value !== 'string') { - throw new DBError(ErrorCode.INVALID_SCHEMA, `Field '${field}' must be a string`, { - collection, - field, - value, - }); - } - - // Pattern validation - if (definition.pattern && !new RegExp(definition.pattern).test(value)) { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Field '${field}' does not match pattern: ${definition.pattern}`, - { collection, field, value }, - ); - } - - // Length validation - if (definition.min !== undefined && value.length < definition.min) { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Field '${field}' must have at least ${definition.min} characters`, - { collection, field, value }, - ); - } - - if (definition.max !== undefined && value.length > definition.max) { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Field '${field}' must have at most ${definition.max} characters`, - { collection, field, value }, - ); - } - break; - - case 'number': - if (typeof value !== 'number') { - throw new DBError(ErrorCode.INVALID_SCHEMA, `Field '${field}' must be a number`, { - collection, - field, - value, - }); - } - - // Range validation - if (definition.min !== undefined && value < definition.min) { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Field '${field}' must be at least ${definition.min}`, - { collection, field, value }, - ); - } - - if (definition.max !== undefined && value > definition.max) { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Field '${field}' must be at most ${definition.max}`, - { collection, field, value }, - ); - } - break; - - case 'boolean': - if (typeof value !== 'boolean') { - throw new DBError(ErrorCode.INVALID_SCHEMA, `Field '${field}' must be a boolean`, { - collection, - field, - value, - }); - } - break; - - case 'array': - if (!Array.isArray(value)) { - throw new DBError(ErrorCode.INVALID_SCHEMA, `Field '${field}' must be an array`, { - collection, - field, - value, - }); - } - - // Length validation - if (definition.min !== undefined && value.length < definition.min) { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Field '${field}' must have at least ${definition.min} items`, - { collection, field, value }, - ); - } - - if (definition.max !== undefined && value.length > definition.max) { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Field '${field}' must have at most ${definition.max} items`, - { collection, field, value }, - ); - } - - // Validate array items if item schema is defined - if (definition.items && value.length > 0) { - for (let i = 0; i < value.length; i++) { - const item = value[i]; - - // This is a simplified item validation - // In a real implementation, this would recursively validate complex objects - switch (definition.items.type) { - case 'string': - if (typeof item !== 'string') { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Item at index ${i} in field '${field}' must be a string`, - { collection, field, item }, - ); - } - break; - - case 'number': - if (typeof item !== 'number') { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Item at index ${i} in field '${field}' must be a number`, - { collection, field, item }, - ); - } - break; - - case 'boolean': - if (typeof item !== 'boolean') { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Item at index ${i} in field '${field}' must be a boolean`, - { collection, field, item }, - ); - } - break; - } - } - } - break; - - case 'object': - if (typeof value !== 'object' || value === null || Array.isArray(value)) { - throw new DBError(ErrorCode.INVALID_SCHEMA, `Field '${field}' must be an object`, { - collection, - field, - value, - }); - } - - // Nested object validation would go here in a real implementation - break; - - case 'enum': - if (definition.enum && !definition.enum.includes(value)) { - throw new DBError( - ErrorCode.INVALID_SCHEMA, - `Field '${field}' must be one of: ${definition.enum.join(', ')}`, - { collection, field, value }, - ); - } - break; - } - } - - return true; -}; diff --git a/src/db/stores/abstractStore.ts b/src/db/stores/abstractStore.ts deleted file mode 100644 index 999dc0d..0000000 --- a/src/db/stores/abstractStore.ts +++ /dev/null @@ -1,413 +0,0 @@ -import { createServiceLogger } from '../../utils/logger'; -import { - ErrorCode, - StoreType, - StoreOptions, - CreateResult, - UpdateResult, - PaginatedResult, - QueryOptions, - ListOptions, - acquireLock, - releaseLock, - isLocked, -} from '../types'; -import { DBError } from '../core/error'; -import { BaseStore, openStore, prepareDocument } from './baseStore'; -import * as events from '../events/eventService'; - -/** - * Abstract store implementation with common CRUD operations - * Specific store types extend this class and customize only what's different - */ -export abstract class AbstractStore implements BaseStore { - protected logger = createServiceLogger(this.getLoggerName()); - protected storeType: StoreType; - - constructor(storeType: StoreType) { - this.storeType = storeType; - } - - /** - * Must be implemented by subclasses to provide the logger name - */ - protected abstract getLoggerName(): string; - - /** - * Create a new document in the specified collection - */ - async create>( - collection: string, - id: string, - data: Omit, - options?: StoreOptions, - ): Promise { - // Create a lock ID for this resource to prevent concurrent operations - const lockId = `${collection}:${id}:create`; - - // Try to acquire a lock - if (!acquireLock(lockId)) { - this.logger.warn( - `Concurrent operation detected on ${collection}/${id}, waiting for completion`, - ); - // Wait until the lock is released (poll every 100ms for max 5 seconds) - let attempts = 0; - while (isLocked(lockId) && attempts < 50) { - await new Promise((resolve) => setTimeout(resolve, 100)); - attempts++; - } - - if (isLocked(lockId)) { - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Timed out waiting for lock on ${collection}/${id}`, - ); - } - - // Try to acquire lock again - if (!acquireLock(lockId)) { - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to acquire lock on ${collection}/${id}`, - ); - } - } - - try { - const db = await openStore(collection, this.storeType, options); - - // Prepare document for storage with validation - const document = this.prepareCreateDocument(collection, id, data); - - // Add to database - this will be overridden by specific implementations if needed - const hash = await this.performCreate(db, id, document); - - // Emit change event - events.emit('document:created', { collection, id, document }); - - this.logger.info(`Created document in ${collection} with id ${id}`); - return { id, hash }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - this.logger.error(`Error creating document in ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to create document in ${collection}: ${error instanceof Error ? error.message : String(error)}`, - error, - ); - } finally { - // Always release the lock when done - releaseLock(lockId); - } - } - - /** - * Prepare a document for creation - can be overridden by subclasses - */ - protected prepareCreateDocument>( - collection: string, - id: string, - data: Omit, - ): any { - return prepareDocument(collection, data); - } - - /** - * Perform the actual create operation - should be implemented by subclasses - */ - protected abstract performCreate(db: any, id: string, document: any): Promise; - - /** - * Get a document by ID from a collection - */ - async get>( - collection: string, - id: string, - options?: StoreOptions & { skipCache?: boolean }, - ): Promise { - try { - const db = await openStore(collection, this.storeType, options); - const document = await this.performGet(db, id); - - return document; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - this.logger.error(`Error getting document ${id} from ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to get document ${id} from ${collection}: ${error instanceof Error ? error.message : String(error)}`, - error, - ); - } - } - - /** - * Perform the actual get operation - should be implemented by subclasses - */ - protected abstract performGet(db: any, id: string): Promise; - - /** - * Update a document in a collection - */ - async update>( - collection: string, - id: string, - data: Partial>, - options?: StoreOptions & { upsert?: boolean }, - ): Promise { - // Create a lock ID for this resource to prevent concurrent operations - const lockId = `${collection}:${id}:update`; - - // Try to acquire a lock - if (!acquireLock(lockId)) { - this.logger.warn( - `Concurrent operation detected on ${collection}/${id}, waiting for completion`, - ); - // Wait until the lock is released (poll every 100ms for max 5 seconds) - let attempts = 0; - while (isLocked(lockId) && attempts < 50) { - await new Promise((resolve) => setTimeout(resolve, 100)); - attempts++; - } - - if (isLocked(lockId)) { - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Timed out waiting for lock on ${collection}/${id}`, - ); - } - - // Try to acquire lock again - if (!acquireLock(lockId)) { - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to acquire lock on ${collection}/${id}`, - ); - } - } - - try { - const db = await openStore(collection, this.storeType, options); - const existing = await this.performGet(db, id); - - if (!existing && !options?.upsert) { - throw new DBError( - ErrorCode.DOCUMENT_NOT_FOUND, - `Document ${id} not found in ${collection}`, - { collection, id }, - ); - } - - // Prepare document for update with validation - const document = this.prepareUpdateDocument(collection, id, data, existing || undefined); - - // Update in database - const hash = await this.performUpdate(db, id, document); - - // Emit change event - events.emit('document:updated', { collection, id, document, previous: existing }); - - this.logger.info(`Updated document in ${collection} with id ${id}`); - return { id, hash }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - this.logger.error(`Error updating document in ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to update document in ${collection}: ${error instanceof Error ? error.message : String(error)}`, - error, - ); - } finally { - // Always release the lock when done - releaseLock(lockId); - } - } - - /** - * Prepare a document for update - can be overridden by subclasses - */ - protected prepareUpdateDocument>( - collection: string, - id: string, - data: Partial>, - existing?: T, - ): any { - return prepareDocument( - collection, - data as unknown as Omit, - existing, - ); - } - - /** - * Perform the actual update operation - should be implemented by subclasses - */ - protected abstract performUpdate(db: any, id: string, document: any): Promise; - - /** - * Delete a document from a collection - */ - async remove(collection: string, id: string, options?: StoreOptions): Promise { - // Create a lock ID for this resource to prevent concurrent operations - const lockId = `${collection}:${id}:remove`; - - // Try to acquire a lock - if (!acquireLock(lockId)) { - this.logger.warn( - `Concurrent operation detected on ${collection}/${id}, waiting for completion`, - ); - // Wait until the lock is released (poll every 100ms for max 5 seconds) - let attempts = 0; - while (isLocked(lockId) && attempts < 50) { - await new Promise((resolve) => setTimeout(resolve, 100)); - attempts++; - } - - if (isLocked(lockId)) { - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Timed out waiting for lock on ${collection}/${id}`, - ); - } - - // Try to acquire lock again - if (!acquireLock(lockId)) { - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to acquire lock on ${collection}/${id}`, - ); - } - } - - try { - const db = await openStore(collection, this.storeType, options); - - // Get the document before deleting for the event - const document = await this.performGet(db, id); - - if (!document) { - this.logger.warn(`Document ${id} not found in ${collection} for deletion`); - return false; - } - - // Delete from database - await this.performRemove(db, id); - - // Emit change event - events.emit('document:deleted', { collection, id, document }); - - this.logger.info(`Deleted document in ${collection} with id ${id}`); - return true; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - this.logger.error(`Error deleting document in ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to delete document in ${collection}: ${error instanceof Error ? error.message : String(error)}`, - error, - ); - } finally { - // Always release the lock when done - releaseLock(lockId); - } - } - - /** - * Perform the actual remove operation - should be implemented by subclasses - */ - protected abstract performRemove(db: any, id: string): Promise; - - /** - * Apply sorting to a list of documents - */ - protected applySorting>( - documents: T[], - options?: ListOptions | QueryOptions, - ): T[] { - if (!options?.sort) { - return documents; - } - - const { field, order } = options.sort; - - return [...documents].sort((a, b) => { - const valueA = a[field]; - const valueB = b[field]; - - // Handle different data types for sorting - if (typeof valueA === 'string' && typeof valueB === 'string') { - return order === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA); - } else if (typeof valueA === 'number' && typeof valueB === 'number') { - return order === 'asc' ? valueA - valueB : valueB - valueA; - } else if (valueA instanceof Date && valueB instanceof Date) { - return order === 'asc' - ? valueA.getTime() - valueB.getTime() - : valueB.getTime() - valueA.getTime(); - } - - // Default comparison for other types - return order === 'asc' - ? String(valueA).localeCompare(String(valueB)) - : String(valueB).localeCompare(String(valueA)); - }); - } - - /** - * Apply pagination to a list of documents - */ - protected applyPagination( - documents: T[], - options?: ListOptions | QueryOptions, - ): { - documents: T[]; - total: number; - hasMore: boolean; - } { - const total = documents.length; - const offset = options?.offset || 0; - const limit = options?.limit || total; - - const paginatedDocuments = documents.slice(offset, offset + limit); - const hasMore = offset + limit < total; - - return { - documents: paginatedDocuments, - total, - hasMore, - }; - } - - /** - * List all documents in a collection with pagination - */ - abstract list>( - collection: string, - options?: ListOptions, - ): Promise>; - - /** - * Query documents in a collection with filtering and pagination - */ - abstract query>( - collection: string, - filter: (doc: T) => boolean, - options?: QueryOptions, - ): Promise>; - - /** - * Create an index for a collection to speed up queries - */ - abstract createIndex(collection: string, field: string, options?: StoreOptions): Promise; -} diff --git a/src/db/stores/baseStore.ts b/src/db/stores/baseStore.ts deleted file mode 100644 index 74bea1b..0000000 --- a/src/db/stores/baseStore.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { createServiceLogger } from '../../utils/logger'; -import { openDB } from '../../orbit/orbitDBService'; -import { validateDocument } from '../schema/validator'; -import { - ErrorCode, - StoreType, - StoreOptions, - CreateResult, - UpdateResult, - PaginatedResult, - QueryOptions, - ListOptions, -} from '../types'; -import { DBError } from '../core/error'; - -const logger = createServiceLogger('DB_STORE'); - -/** - * Base Store interface that all store implementations should extend - */ -export interface BaseStore { - create>( - collection: string, - id: string, - data: Omit, - options?: StoreOptions, - ): Promise; - - get>( - collection: string, - id: string, - options?: StoreOptions & { skipCache?: boolean }, - ): Promise; - - update>( - collection: string, - id: string, - data: Partial>, - options?: StoreOptions & { upsert?: boolean }, - ): Promise; - - remove(collection: string, id: string, options?: StoreOptions): Promise; - - list>( - collection: string, - options?: ListOptions, - ): Promise>; - - query>( - collection: string, - filter: (doc: T) => boolean, - options?: QueryOptions, - ): Promise>; - - createIndex(collection: string, field: string, options?: StoreOptions): Promise; -} - -/** - * Open a store of the specified type - */ -export async function openStore( - collection: string, - storeType: StoreType, - options?: StoreOptions, -): Promise { - try { - // Log minimal connection info to avoid leaking sensitive data - logger.info( - `Opening ${storeType} store for collection: ${collection} (connection ID: ${options?.connectionId || 'default'})`, - ); - - return await openDB(collection, storeType).catch((err) => { - throw new Error(`OrbitDB openDB failed: ${err.message}`); - }); - } catch (error) { - logger.error(`Error opening ${storeType} store for collection ${collection}:`, error); - - // Add more context to the error for improved debugging - const errorMessage = error instanceof Error ? error.message : String(error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to open ${storeType} store for collection ${collection}: ${errorMessage}`, - error, - ); - } -} - -/** - * Recursively sanitize an object by removing undefined values - * This is necessary because IPLD doesn't support undefined values - */ -function deepSanitizeUndefined(obj: any): any { - if (obj === null || obj === undefined) { - return null; - } - - if (Array.isArray(obj)) { - return obj.map(deepSanitizeUndefined).filter((item) => item !== undefined); - } - - if (typeof obj === 'object' && obj.constructor === Object) { - const sanitized: any = {}; - for (const [key, value] of Object.entries(obj)) { - const sanitizedValue = deepSanitizeUndefined(value); - // Only include the property if it's not undefined - if (sanitizedValue !== undefined) { - sanitized[key] = sanitizedValue; - } - } - return sanitized; - } - - return obj; -} - -/** - * Helper function to prepare a document for storage - */ -export function prepareDocument>( - collection: string, - data: Omit, - existingDoc?: T | null, -): T { - const timestamp = Date.now(); - - // Deep sanitize the input data by removing undefined values - const sanitizedData = deepSanitizeUndefined(data) as Omit; - - // Prepare document for validation - let docToValidate: T; - - // If it's an update to an existing document - if (existingDoc) { - docToValidate = { - ...existingDoc, - ...sanitizedData, - updatedAt: timestamp, - } as T; - } else { - // Otherwise it's a new document - docToValidate = { - ...sanitizedData, - createdAt: timestamp, - updatedAt: timestamp, - } as unknown as T; - } - - // Deep sanitize the final document to ensure no undefined values remain - const finalDocument = deepSanitizeUndefined(docToValidate) as T; - - // Validate the document BEFORE processing - validateDocument(collection, finalDocument); - - // Return the validated document - return finalDocument; -} diff --git a/src/db/stores/counterStore.ts b/src/db/stores/counterStore.ts deleted file mode 100644 index 7b9ea64..0000000 --- a/src/db/stores/counterStore.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { createServiceLogger } from '../../utils/logger'; -import { - ErrorCode, - StoreType, - StoreOptions, - CreateResult, - UpdateResult, - PaginatedResult, - QueryOptions, - ListOptions, -} from '../types'; -import { DBError } from '../core/error'; -import { BaseStore, openStore } from './baseStore'; -import * as events from '../events/eventService'; - -const logger = createServiceLogger('COUNTER_STORE'); - -/** - * CounterStore implementation - * Uses OrbitDB's counter store for simple numeric counters - */ -export class CounterStore implements BaseStore { - /** - * Create or set counter value - */ - async create>( - collection: string, - id: string, - data: Omit, - options?: StoreOptions, - ): Promise { - try { - const db = await openStore(collection, StoreType.COUNTER, options); - - // Extract value from data, default to 0 - const value = - typeof data === 'object' && data !== null && 'value' in data ? Number(data.value) : 0; - - // Set the counter value - const hash = await db.set(value); - - // Construct document representation - const document = { - id, - value, - createdAt: Date.now(), - updatedAt: Date.now(), - }; - - // Emit change event - events.emit('document:created', { collection, id, document }); - - logger.info(`Set counter in ${collection} to ${value}`); - return { id, hash }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error setting counter in ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to set counter in ${collection}`, - error, - ); - } - } - - /** - * Get counter value - */ - async get>( - collection: string, - id: string, - options?: StoreOptions & { skipCache?: boolean }, - ): Promise { - try { - // Note: for counters, id is not used in the underlying store (there's only one counter per db) - // but we use it for consistency with the API - - const db = await openStore(collection, StoreType.COUNTER, options); - - // Get the counter value - const value = await db.value(); - - // Construct document representation - const document = { - id, - value, - updatedAt: Date.now(), - } as unknown as T; - - return document; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error getting counter from ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to get counter from ${collection}`, - error, - ); - } - } - - /** - * Update counter (increment/decrement) - */ - async update>( - collection: string, - id: string, - data: Partial>, - options?: StoreOptions & { upsert?: boolean }, - ): Promise { - try { - const db = await openStore(collection, StoreType.COUNTER, options); - - // Get current value before update - const currentValue = await db.value(); - - // Extract value from data - let value: number; - let operation: 'increment' | 'decrement' | 'set' = 'set'; - - // Check what kind of operation we're doing - if (typeof data === 'object' && data !== null) { - if ('increment' in data) { - value = Number(data.increment); - operation = 'increment'; - } else if ('decrement' in data) { - value = Number(data.decrement); - operation = 'decrement'; - } else if ('value' in data) { - value = Number(data.value); - operation = 'set'; - } else { - value = 0; - operation = 'set'; - } - } else { - value = 0; - operation = 'set'; - } - - // Update the counter - let hash; - let newValue; - - switch (operation) { - case 'increment': - hash = await db.inc(value); - newValue = currentValue + value; - break; - case 'decrement': - hash = await db.inc(-value); // Counter store uses inc with negative value - newValue = currentValue - value; - break; - case 'set': - hash = await db.set(value); - newValue = value; - break; - } - - // Construct document representation - const document = { - id, - value: newValue, - updatedAt: Date.now(), - }; - - // Emit change event - events.emit('document:updated', { - collection, - id, - document, - previous: { id, value: currentValue }, - }); - - logger.info(`Updated counter in ${collection} from ${currentValue} to ${newValue}`); - return { id, hash }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error updating counter in ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to update counter in ${collection}`, - error, - ); - } - } - - /** - * Delete/reset counter - */ - async remove(collection: string, id: string, options?: StoreOptions): Promise { - try { - const db = await openStore(collection, StoreType.COUNTER, options); - - // Get the current value for the event - const currentValue = await db.value(); - - // Reset the counter to 0 (counters can't be truly deleted) - await db.set(0); - - // Emit change event - events.emit('document:deleted', { - collection, - id, - document: { id, value: currentValue }, - }); - - logger.info(`Reset counter in ${collection} from ${currentValue} to 0`); - return true; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error resetting counter in ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to reset counter in ${collection}`, - error, - ); - } - } - - /** - * List all counters (for counter stores, there's only one counter per db) - */ - async list>( - collection: string, - options?: ListOptions, - ): Promise> { - try { - const db = await openStore(collection, StoreType.COUNTER, options); - const value = await db.value(); - - // For counter stores, we just return one document with the counter value - const document = { - id: '0', // Default ID since counters don't have IDs - value, - updatedAt: Date.now(), - } as unknown as T; - - return { - documents: [document], - total: 1, - hasMore: false, - }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error listing counter in ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to list counter in ${collection}`, - error, - ); - } - } - - /** - * Query is not applicable for counter stores, but we implement for API consistency - */ - async query>( - collection: string, - filter: (doc: T) => boolean, - options?: QueryOptions, - ): Promise> { - try { - const db = await openStore(collection, StoreType.COUNTER, options); - const value = await db.value(); - - // Create document - const document = { - id: '0', // Default ID since counters don't have IDs - value, - updatedAt: Date.now(), - } as unknown as T; - - // Apply filter - const documents = filter(document) ? [document] : []; - - return { - documents, - total: documents.length, - hasMore: false, - }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error querying counter in ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to query counter in ${collection}`, - error, - ); - } - } - - /** - * Create an index - not applicable for counter stores - */ - async createIndex(collection: string, _field: string, _options?: StoreOptions): Promise { - logger.warn( - `Index creation not supported for counter collections, ignoring request for ${collection}`, - ); - return false; - } -} diff --git a/src/db/stores/docStore.ts b/src/db/stores/docStore.ts deleted file mode 100644 index 146d1fe..0000000 --- a/src/db/stores/docStore.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { StoreType, StoreOptions, PaginatedResult, QueryOptions, ListOptions } from '../types'; -import { AbstractStore } from './abstractStore'; -import { prepareDocument } from './baseStore'; -import { DBError, ErrorCode } from '../core/error'; - -/** - * DocStore implementation - * Uses OrbitDB's document store which allows for more complex document storage with indices - */ -export class DocStore extends AbstractStore { - constructor() { - super(StoreType.DOCSTORE); - } - - protected getLoggerName(): string { - return 'DOCSTORE'; - } - - /** - * Prepare a document for creation - override to add _id which is required for docstore - */ - protected prepareCreateDocument>( - collection: string, - id: string, - data: Omit, - ): any { - return { - _id: id, - ...prepareDocument(collection, data), - }; - } - - /** - * Prepare a document for update - override to add _id which is required for docstore - */ - protected prepareUpdateDocument>( - collection: string, - id: string, - data: Partial>, - existing?: T, - ): any { - return { - _id: id, - ...prepareDocument( - collection, - data as unknown as Omit, - existing, - ), - }; - } - - /** - * Implementation for the DocStore create operation - */ - protected async performCreate(db: any, id: string, document: any): Promise { - return await db.put(document); - } - - /** - * Implementation for the DocStore get operation - */ - protected async performGet(db: any, id: string): Promise { - return (await db.get(id)) as T | null; - } - - /** - * Implementation for the DocStore update operation - */ - protected async performUpdate(db: any, id: string, document: any): Promise { - return await db.put(document); - } - - /** - * Implementation for the DocStore remove operation - */ - protected async performRemove(db: any, id: string): Promise { - await db.del(id); - } - - /** - * List all documents in a collection with pagination - */ - async list>( - collection: string, - options?: ListOptions, - ): Promise> { - try { - const db = await this.openStore(collection, options); - const allDocs = await db.query((_doc: any) => true); - - // Map the documents to include id - let documents = allDocs.map((doc: any) => ({ - id: doc._id, - ...doc, - })) as T[]; - - // Apply sorting - documents = this.applySorting(documents, options); - - // Apply pagination - return this.applyPagination(documents, options); - } catch (error) { - this.handleError(`Error listing documents in ${collection}`, error); - } - } - - /** - * Query documents in a collection with filtering and pagination - */ - async query>( - collection: string, - filter: (doc: T) => boolean, - options?: QueryOptions, - ): Promise> { - try { - const db = await this.openStore(collection, options); - - // Apply filter using docstore's query capability - const filtered = await db.query((doc: any) => filter(doc as T)); - - // Map the documents to include id - let documents = filtered.map((doc: any) => ({ - id: doc._id, - ...doc, - })) as T[]; - - // Apply sorting - documents = this.applySorting(documents, options); - - // Apply pagination - return this.applyPagination(documents, options); - } catch (error) { - this.handleError(`Error querying documents in ${collection}`, error); - } - } - - /** - * Create an index for a collection to speed up queries - * DocStore has built-in indexing capabilities - */ - async createIndex(collection: string, field: string, options?: StoreOptions): Promise { - try { - const db = await this.openStore(collection, options); - - // DocStore supports indexing, so we create the index - if (typeof db.createIndex === 'function') { - await db.createIndex(field); - this.logger.info(`Index created on ${field} for collection ${collection}`); - return true; - } - - this.logger.info( - `Index creation not supported for this DB instance, but DocStore has built-in indices`, - ); - return true; - } catch (error) { - this.handleError(`Error creating index for ${collection}`, error); - } - } - - /** - * Helper to open a store of the correct type - */ - private async openStore(collection: string, options?: StoreOptions): Promise { - const { openStore } = await import('./baseStore'); - return await openStore(collection, this.storeType, options); - } - - /** - * Helper to handle errors consistently - */ - private handleError(message: string, error: any): never { - if (error instanceof DBError) { - throw error; - } - - this.logger.error(`${message}:`, error); - throw new DBError(ErrorCode.OPERATION_FAILED, `${message}: ${error.message}`, error); - } -} diff --git a/src/db/stores/feedStore.ts b/src/db/stores/feedStore.ts deleted file mode 100644 index 03929ba..0000000 --- a/src/db/stores/feedStore.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { createServiceLogger } from '../../utils/logger'; -import { - ErrorCode, - StoreType, - StoreOptions, - CreateResult, - UpdateResult, - PaginatedResult, - QueryOptions, - ListOptions, -} from '../types'; -import { DBError } from '../core/error'; -import { BaseStore, openStore, prepareDocument } from './baseStore'; -import * as events from '../events/eventService'; - -const logger = createServiceLogger('FEED_STORE'); - -/** - * FeedStore/EventLog implementation - * Uses OrbitDB's feed/eventlog store which is an append-only log - */ -export class FeedStore implements BaseStore { - /** - * Create a new document in the specified collection - * For feeds, this appends a new entry - */ - async create>( - collection: string, - id: string, - data: Omit, - options?: StoreOptions, - ): Promise { - try { - const db = await openStore(collection, StoreType.FEED, options); - - // Prepare document for storage with ID - const document = { - id, - ...prepareDocument(collection, data), - }; - - // Add to database - const hash = await db.add(document); - - // Emit change event - events.emit('document:created', { collection, id, document, hash }); - - logger.info(`Created entry in feed ${collection} with id ${id} and hash ${hash}`); - return { id, hash }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error creating entry in feed ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to create entry in feed ${collection}`, - error, - ); - } - } - - /** - * Get a specific entry in a feed - note this works differently than other stores - * as feeds are append-only logs identified by hash - */ - async get>( - collection: string, - hash: string, - options?: StoreOptions & { skipCache?: boolean }, - ): Promise { - try { - const db = await openStore(collection, StoreType.FEED, options); - - // Get the specific entry by hash - const entry = await db.get(hash); - if (!entry) { - return null; - } - - const document = entry.payload.value as T; - - return document; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error getting entry ${hash} from feed ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to get entry ${hash} from feed ${collection}`, - error, - ); - } - } - - /** - * Update an entry in a feed - * Note: Feeds are append-only, so we can't actually update existing entries - * Instead, we append a new entry with the updated data and link it to the original - */ - async update>( - collection: string, - id: string, - data: Partial>, - options?: StoreOptions & { upsert?: boolean }, - ): Promise { - try { - const db = await openStore(collection, StoreType.FEED, options); - - // Get all entries using proper iterator API - const entries = []; - for await (const entry of db.iterator({ limit: -1 })) { - entries.push(entry); - } - - const existingEntryIndex = entries.findIndex((e: any) => { - const value = e.payload.value; - return value && value.id === id; - }); - - if (existingEntryIndex === -1 && !options?.upsert) { - throw new DBError( - ErrorCode.DOCUMENT_NOT_FOUND, - `Entry with id ${id} not found in feed ${collection}`, - { collection, id }, - ); - } - - const existingEntry = - existingEntryIndex !== -1 ? entries[existingEntryIndex].payload.value : null; - - // Prepare document with update - const document = { - id, - ...prepareDocument( - collection, - data as unknown as Omit, - existingEntry, - ), - // Add reference to the previous entry if it exists - previousEntryHash: existingEntryIndex !== -1 ? entries[existingEntryIndex].hash : undefined, - }; - - // Add to feed (append new entry) - const hash = await db.add(document); - - // Emit change event - events.emit('document:updated', { collection, id, document, previous: existingEntry }); - - logger.info(`Updated entry in feed ${collection} with id ${id} (new hash: ${hash})`); - return { id, hash }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error updating entry in feed ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to update entry in feed ${collection}`, - error, - ); - } - } - - /** - * Delete is not supported in feed/eventlog stores since they're append-only - * Instead, we add a "tombstone" entry that marks the entry as deleted - */ - async remove(collection: string, id: string, options?: StoreOptions): Promise { - try { - const db = await openStore(collection, StoreType.FEED, options); - - // Find the entry with the given id using proper iterator API - const entries = []; - for await (const entry of db.iterator({ limit: -1 })) { - entries.push(entry); - } - - const existingEntryIndex = entries.findIndex((e: any) => { - const value = e.payload.value; - return value && value.id === id; - }); - - if (existingEntryIndex === -1) { - throw new DBError( - ErrorCode.DOCUMENT_NOT_FOUND, - `Entry with id ${id} not found in feed ${collection}`, - { collection, id }, - ); - } - - const existingEntry = entries[existingEntryIndex].payload.value; - const existingHash = entries[existingEntryIndex].hash; - - // Add a "tombstone" entry that marks this as deleted - const tombstone = { - id, - deleted: true, - deletedAt: Date.now(), - previousEntryHash: existingHash, - }; - - await db.add(tombstone); - - // Emit change event - events.emit('document:deleted', { collection, id, document: existingEntry }); - - logger.info(`Marked entry as deleted in feed ${collection} with id ${id}`); - return true; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error marking entry as deleted in feed ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to mark entry as deleted in feed ${collection}`, - error, - ); - } - } - - /** - * List all entries in a feed with pagination - * Note: This will only return the latest entry for each unique ID - */ - async list>( - collection: string, - options?: ListOptions, - ): Promise> { - try { - const db = await openStore(collection, StoreType.FEED, options); - - // Use proper pagination instead of loading everything - const requestedLimit = options?.limit || 50; - const requestedOffset = options?.offset || 0; - - // For feeds, we need to get more entries than requested since we'll filter duplicates - // Use a reasonable multiplier but cap it to prevent memory issues - const fetchLimit = requestedLimit === -1 ? -1 : Math.min(requestedLimit * 3, 1000); - - // Get entries using proper iterator API with pagination - const entries = []; - let count = 0; - let skipped = 0; - - for await (const entry of db.iterator({ limit: fetchLimit })) { - // Skip entries for offset - if (requestedOffset > 0 && skipped < requestedOffset) { - skipped++; - continue; - } - - entries.push(entry); - count++; - - // Break if we have enough entries and not requesting all - if (requestedLimit !== -1 && count >= fetchLimit) { - break; - } - } - - // Group by ID and keep only the latest entry for each ID - // Also filter out tombstone entries - const latestEntries = new Map(); - for (const entry of entries) { - // Handle different possible entry structures - let value; - if (entry && entry.payload && entry.payload.value) { - value = entry.payload.value; - } else if (entry && entry.value) { - value = entry.value; - } else if (entry && typeof entry === 'object') { - value = entry; - } else { - continue; - } - - if (!value || value.deleted) continue; - - const id = value.id; - if (!id) continue; - - // If we already have an entry with this ID, check which is newer - if (latestEntries.has(id)) { - const existing = latestEntries.get(id); - const existingTime = existing.value.updatedAt || existing.value.timestamp || 0; - const currentTime = value.updatedAt || value.timestamp || 0; - if (currentTime > existingTime) { - latestEntries.set(id, { hash: entry.hash, value }); - } - } else { - latestEntries.set(id, { hash: entry.hash, value }); - } - } - - // Convert to array of documents - let documents = Array.from(latestEntries.values()).map((entry) => ({ - ...entry.value, - })) as T[]; - - // Sort if requested - if (options?.sort) { - const { field, order } = options.sort; - documents.sort((a, b) => { - const valueA = a[field]; - const valueB = b[field]; - - // Handle different data types for sorting - if (typeof valueA === 'string' && typeof valueB === 'string') { - return order === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA); - } else if (typeof valueA === 'number' && typeof valueB === 'number') { - return order === 'asc' ? valueA - valueB : valueB - valueA; - } else if (valueA instanceof Date && valueB instanceof Date) { - return order === 'asc' - ? valueA.getTime() - valueB.getTime() - : valueB.getTime() - valueA.getTime(); - } - - // Default comparison for other types - return order === 'asc' - ? String(valueA).localeCompare(String(valueB)) - : String(valueB).localeCompare(String(valueA)); - }); - } - - // Apply final pagination to the processed results - const total = documents.length; - const finalLimit = requestedLimit === -1 ? total : requestedLimit; - const paginatedDocuments = documents.slice(0, finalLimit); - const hasMore = documents.length > finalLimit; - - return { - documents: paginatedDocuments, - total, - hasMore, - }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error listing entries in feed ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to list entries in feed ${collection}`, - error, - ); - } - } - - /** - * Query entries in a feed with filtering and pagination - * Note: This queries the latest entry for each unique ID - */ - async query>( - collection: string, - filter: (doc: T) => boolean, - options?: QueryOptions, - ): Promise> { - try { - const db = await openStore(collection, StoreType.FEED, options); - - // Get all entries using proper iterator API - const entries = []; - for await (const entry of db.iterator({ limit: -1 })) { - entries.push(entry); - } - - // Group by ID and keep only the latest entry for each ID - // Also filter out tombstone entries - const latestEntries = new Map(); - for (const entry of entries) { - // Handle different possible entry structures - let value; - if (entry && entry.payload && entry.payload.value) { - value = entry.payload.value; - } else if (entry && entry.value) { - value = entry.value; - } else if (entry && typeof entry === 'object') { - value = entry; - } else { - continue; - } - - if (!value || value.deleted) continue; - - const id = value.id; - if (!id) continue; - - // If we already have an entry with this ID, check which is newer - if (latestEntries.has(id)) { - const existing = latestEntries.get(id); - if (value.updatedAt > existing.value.updatedAt) { - latestEntries.set(id, { hash: entry.hash, value }); - } - } else { - latestEntries.set(id, { hash: entry.hash, value }); - } - } - - // Convert to array of documents and apply filter - let filtered = Array.from(latestEntries.values()) - .filter((entry) => filter(entry.value as T)) - .map((entry) => ({ - ...entry.value, - })) as T[]; - - // Sort if requested - if (options?.sort) { - const { field, order } = options.sort; - filtered.sort((a, b) => { - const valueA = a[field]; - const valueB = b[field]; - - // Handle different data types for sorting - if (typeof valueA === 'string' && typeof valueB === 'string') { - return order === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA); - } else if (typeof valueA === 'number' && typeof valueB === 'number') { - return order === 'asc' ? valueA - valueB : valueB - valueA; - } else if (valueA instanceof Date && valueB instanceof Date) { - return order === 'asc' - ? valueA.getTime() - valueB.getTime() - : valueB.getTime() - valueA.getTime(); - } - - // Default comparison for other types - return order === 'asc' - ? String(valueA).localeCompare(String(valueB)) - : String(valueB).localeCompare(String(valueA)); - }); - } - - // Apply pagination - const total = filtered.length; - const offset = options?.offset || 0; - const limit = options?.limit || total; - - const paginatedDocuments = filtered.slice(offset, offset + limit); - const hasMore = offset + limit < total; - - return { - documents: paginatedDocuments, - total, - hasMore, - }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error querying entries in feed ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to query entries in feed ${collection}`, - error, - ); - } - } - - /** - * Create an index for a collection - not supported for feeds - */ - async createIndex(collection: string, _field: string, _options?: StoreOptions): Promise { - logger.warn( - `Index creation not supported for feed collections, ignoring request for ${collection}`, - ); - return false; - } -} diff --git a/src/db/stores/fileStore.ts b/src/db/stores/fileStore.ts deleted file mode 100644 index 3819277..0000000 --- a/src/db/stores/fileStore.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { createServiceLogger } from '../../utils/logger'; -import { ErrorCode, StoreType, FileUploadResult, FileResult } from '../types'; -import { DBError } from '../core/error'; -import { openStore } from './baseStore'; -import ipfsService, { getHelia } from '../../ipfs/ipfsService'; -import { CreateResult, StoreOptions } from '../types'; - -async function readAsyncIterableToBuffer( - asyncIterable: AsyncIterable, -): Promise { - const chunks: Uint8Array[] = []; - for await (const chunk of asyncIterable) { - chunks.push(chunk); - } - return Buffer.concat(chunks); -} - -const logger = createServiceLogger('FILE_STORE'); - -/** - * Upload a file to IPFS - */ -export const uploadFile = async ( - fileData: Buffer, - options?: { - filename?: string; - connectionId?: string; - metadata?: Record; - }, -): Promise => { - try { - const ipfs = getHelia(); - if (!ipfs) { - logger.error('IPFS instance not available - Helia is null or undefined'); - // Try to check if IPFS service is running - try { - const heliaInstance = ipfsService.getHelia(); - logger.error( - 'IPFS Service getHelia() returned:', - heliaInstance ? 'instance available' : 'null/undefined', - ); - } catch (importError) { - logger.error('Error importing IPFS service:', importError); - } - throw new DBError(ErrorCode.OPERATION_FAILED, 'IPFS instance not available'); - } - - logger.info(`Attempting to upload file with size: ${fileData.length} bytes`); - - // Add to IPFS - const unixfs = await import('@helia/unixfs'); - const fs = unixfs.unixfs(ipfs); - const cid = await fs.addBytes(fileData); - const cidStr = cid.toString(); - - // Store metadata - const filesDb = await openStore('_files', StoreType.KEYVALUE); - await filesDb.put(cidStr, { - filename: options?.filename, - size: fileData.length, - uploadedAt: Date.now(), - ...options?.metadata, - }); - - logger.info(`Uploaded file with CID: ${cidStr}`); - return { cid: cidStr }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error('Error uploading file:', error); - throw new DBError(ErrorCode.OPERATION_FAILED, 'Failed to upload file', error); - } -}; - -/** - * Get a file from IPFS by CID - */ -export const getFile = async (cid: string): Promise => { - try { - const ipfs = getHelia(); - if (!ipfs) { - throw new DBError(ErrorCode.OPERATION_FAILED, 'IPFS instance not available'); - } - - // Get from IPFS - const unixfs = await import('@helia/unixfs'); - const fs = unixfs.unixfs(ipfs); - const { CID } = await import('multiformats/cid'); - const resolvedCid = CID.parse(cid); - - try { - // Convert AsyncIterable to Buffer - const bytes = await readAsyncIterableToBuffer(fs.cat(resolvedCid)); - - // Get metadata if available - let metadata = null; - try { - const filesDb = await openStore('_files', StoreType.KEYVALUE); - metadata = await filesDb.get(cid); - } catch (_err) { - // Metadata might not exist, continue without it - } - - return { data: bytes, metadata }; - } catch (error) { - throw new DBError(ErrorCode.FILE_NOT_FOUND, `File with CID ${cid} not found`, error); - } - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error getting file with CID ${cid}:`, error); - throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to get file with CID ${cid}`, error); - } -}; - -/** - * Delete a file from IPFS by CID - */ -export const deleteFile = async (cid: string): Promise => { - try { - // Delete metadata - try { - const filesDb = await openStore('_files', StoreType.KEYVALUE); - await filesDb.del(cid); - } catch (_err) { - // Ignore if metadata doesn't exist - } - - logger.info(`Deleted file with CID: ${cid}`); - return true; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error deleting file with CID ${cid}:`, error); - throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to delete file with CID ${cid}`, error); - } -}; - -export const create = async >( - collection: string, - id: string, - data: Omit, - options?: StoreOptions, -): Promise => { - try { - const db = await openStore(collection, StoreType.KEYVALUE, options); - - // Prepare document for storage with ID - // const document = { - // id, - // ...prepareDocument(collection, data) - // }; - const document = { id, ...data }; - - // Add to database - const hash = await db.add(document); - - // Emit change event - // events.emit('document:created', { collection, id, document, hash }); - - logger.info(`Created entry in file ${collection} with id ${id} and hash ${hash}`); - return { id, hash }; - } catch (error: unknown) { - if (error instanceof DBError) { - throw error; - } - - logger.error(`Error creating entry in file ${collection}:`, error); - throw new DBError( - ErrorCode.OPERATION_FAILED, - `Failed to create entry in file ${collection}`, - error, - ); - } -}; diff --git a/src/db/stores/keyValueStore.ts b/src/db/stores/keyValueStore.ts deleted file mode 100644 index 1bca8f8..0000000 --- a/src/db/stores/keyValueStore.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { StoreType, StoreOptions, PaginatedResult, QueryOptions, ListOptions } from '../types'; -import { AbstractStore } from './abstractStore'; -import { DBError, ErrorCode } from '../core/error'; - -/** - * KeyValue Store implementation using the AbstractStore base class - */ -export class KeyValueStore extends AbstractStore { - constructor() { - super(StoreType.KEYVALUE); - } - - protected getLoggerName(): string { - return 'KEYVALUE_STORE'; - } - - /** - * Implementation for the KeyValue store create operation - */ - protected async performCreate(db: any, id: string, document: any): Promise { - return await db.put(id, document); - } - - /** - * Implementation for the KeyValue store get operation - */ - protected async performGet(db: any, id: string): Promise { - return (await db.get(id)) as T | null; - } - - /** - * Implementation for the KeyValue store update operation - */ - protected async performUpdate(db: any, id: string, document: any): Promise { - return await db.put(id, document); - } - - /** - * Implementation for the KeyValue store remove operation - */ - protected async performRemove(db: any, id: string): Promise { - await db.del(id); - } - - /** - * List all documents in a collection with pagination - */ - async list>( - collection: string, - options?: ListOptions, - ): Promise> { - try { - const db = await this.openStore(collection, options); - const all = await db.all(); - - // Convert the key-value pairs to an array of documents with IDs - let documents = Object.entries(all).map(([key, value]) => ({ - id: key, - ...(value as any), - })) as T[]; - - // Apply sorting - documents = this.applySorting(documents, options); - - // Apply pagination - return this.applyPagination(documents, options); - } catch (error) { - this.handleError(`Error listing documents in ${collection}`, error); - } - } - - /** - * Query documents in a collection with filtering and pagination - */ - async query>( - collection: string, - filter: (doc: T) => boolean, - options?: QueryOptions, - ): Promise> { - try { - const db = await this.openStore(collection, options); - const all = await db.all(); - - // Apply filter - let filtered = Object.entries(all) - .filter(([_, value]) => filter(value as T)) - .map(([key, value]) => ({ - id: key, - ...(value as any), - })) as T[]; - - // Apply sorting - filtered = this.applySorting(filtered, options); - - // Apply pagination - return this.applyPagination(filtered, options); - } catch (error) { - this.handleError(`Error querying documents in ${collection}`, error); - } - } - - /** - * Create an index for a collection to speed up queries - */ - async createIndex(collection: string, field: string): Promise { - try { - // KeyValueStore doesn't support real indexing - this is just a placeholder - this.logger.info( - `Index created on ${field} for collection ${collection} (not supported in KeyValueStore)`, - ); - return true; - } catch (error) { - this.handleError(`Error creating index for ${collection}`, error); - } - } - - /** - * Helper to open a store of the correct type - */ - private async openStore(collection: string, options?: StoreOptions): Promise { - const { openStore } = await import('./baseStore'); - return await openStore(collection, this.storeType, options); - } - - /** - * Helper to handle errors consistently - */ - private handleError(message: string, error: any): never { - if (error instanceof DBError) { - throw error; - } - - this.logger.error(`${message}:`, error); - throw new DBError(ErrorCode.OPERATION_FAILED, `${message}: ${error.message}`, error); - } -} diff --git a/src/db/stores/storeFactory.ts b/src/db/stores/storeFactory.ts deleted file mode 100644 index 8a413a8..0000000 --- a/src/db/stores/storeFactory.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { createServiceLogger } from '../../utils/logger'; -import { StoreType, ErrorCode } from '../types'; -import { DBError } from '../core/error'; -import { BaseStore } from './baseStore'; -import { KeyValueStore } from './keyValueStore'; -import { DocStore } from './docStore'; -import { FeedStore } from './feedStore'; -import { CounterStore } from './counterStore'; - -const logger = createServiceLogger('STORE_FACTORY'); - -// Initialize instances for each store type - singleton pattern -const storeInstances = new Map(); - -// Store type mapping to implementations -const storeImplementations = { - [StoreType.KEYVALUE]: KeyValueStore, - [StoreType.DOCSTORE]: DocStore, - [StoreType.FEED]: FeedStore, - [StoreType.EVENTLOG]: FeedStore, // Alias for feed - [StoreType.COUNTER]: CounterStore, -}; - -/** - * Get a store instance by type (factory and singleton pattern) - */ -export function getStore(type: StoreType): BaseStore { - // Return cached instance if available (singleton pattern) - if (storeInstances.has(type)) { - return storeInstances.get(type)!; - } - - // Get the store implementation class - const StoreClass = storeImplementations[type]; - - if (!StoreClass) { - logger.error(`Unsupported store type: ${type}`); - throw new DBError(ErrorCode.STORE_TYPE_ERROR, `Unsupported store type: ${type}`); - } - - // Create a new instance of the store - const store = new StoreClass(); - - // Cache the instance for future use - storeInstances.set(type, store); - - return store; -} diff --git a/src/db/transactions/transactionService.ts b/src/db/transactions/transactionService.ts deleted file mode 100644 index d857fca..0000000 --- a/src/db/transactions/transactionService.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Transaction operation type -interface TransactionOperation { - type: 'create' | 'update' | 'delete'; - collection: string; - id: string; - data?: any; -} - -/** - * Transaction object for batching operations - */ -export class Transaction { - private operations: TransactionOperation[] = []; - private connectionId?: string; - - constructor(connectionId?: string) { - this.connectionId = connectionId; - } - - /** - * Add a create operation to the transaction - */ - create(collection: string, id: string, data: T): Transaction { - this.operations.push({ - type: 'create', - collection, - id, - data, - }); - return this; - } - - /** - * Add an update operation to the transaction - */ - update(collection: string, id: string, data: Partial): Transaction { - this.operations.push({ - type: 'update', - collection, - id, - data, - }); - return this; - } - - /** - * Add a delete operation to the transaction - */ - delete(collection: string, id: string): Transaction { - this.operations.push({ - type: 'delete', - collection, - id, - }); - return this; - } - - /** - * Get all operations in this transaction - */ - getOperations(): TransactionOperation[] { - return [...this.operations]; - } - - /** - * Get connection ID for this transaction - */ - getConnectionId(): string | undefined { - return this.connectionId; - } -} diff --git a/src/db/types/index.ts b/src/db/types/index.ts deleted file mode 100644 index 9339ff4..0000000 --- a/src/db/types/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -// Common types for database operations -import { EventEmitter } from 'events'; -import { Transaction } from '../transactions/transactionService'; - -export type { Transaction }; - -// Resource locking for concurrent operations -const locks = new Map(); - -export const acquireLock = (resourceId: string): boolean => { - if (locks.has(resourceId)) { - return false; - } - locks.set(resourceId, true); - return true; -}; - -export const releaseLock = (resourceId: string): void => { - locks.delete(resourceId); -}; - -export const isLocked = (resourceId: string): boolean => { - return locks.has(resourceId); -}; - -// Database Types -export enum StoreType { - KEYVALUE = 'keyvalue', - DOCSTORE = 'documents', - FEED = 'feed', - EVENTLOG = 'events', - COUNTER = 'counter', -} - -// Common result types -export interface CreateResult { - id: string; - hash: string; -} - -export interface UpdateResult { - id: string; - hash: string; -} - -export interface FileUploadResult { - cid: string; -} - -export interface FileMetadata { - filename?: string; - size: number; - uploadedAt: number; - [key: string]: any; -} - -export interface FileResult { - data: Buffer; - metadata: FileMetadata | null; -} - -export interface PaginatedResult { - documents: T[]; - total: number; - hasMore: boolean; -} - -// Define error codes -export enum ErrorCode { - NOT_INITIALIZED = 'ERR_NOT_INITIALIZED', - INITIALIZATION_FAILED = 'ERR_INIT_FAILED', - DOCUMENT_NOT_FOUND = 'ERR_DOC_NOT_FOUND', - INVALID_SCHEMA = 'ERR_INVALID_SCHEMA', - OPERATION_FAILED = 'ERR_OPERATION_FAILED', - TRANSACTION_FAILED = 'ERR_TRANSACTION_FAILED', - FILE_NOT_FOUND = 'ERR_FILE_NOT_FOUND', - INVALID_PARAMETERS = 'ERR_INVALID_PARAMS', - CONNECTION_ERROR = 'ERR_CONNECTION', - STORE_TYPE_ERROR = 'ERR_STORE_TYPE', -} - -// Connection pool interface -export interface DBConnection { - ipfs: any; - orbitdb: any; - timestamp: number; - isActive: boolean; -} - -// Schema validation -export interface SchemaDefinition { - type: string; - required?: boolean; - pattern?: string; - min?: number; - max?: number; - enum?: any[]; - items?: SchemaDefinition; // For arrays - properties?: Record; // For objects -} - -export interface CollectionSchema { - properties: Record; - required?: string[]; -} - -// Metrics tracking -export interface Metrics { - operations: { - creates: number; - reads: number; - updates: number; - deletes: number; - queries: number; - fileUploads: number; - fileDownloads: number; - }; - performance: { - totalOperationTime: number; - operationCount: number; - averageOperationTime?: number; - }; - errors: { - count: number; - byCode: Record; - }; - cacheStats: { - hits: number; - misses: number; - }; - startTime: number; -} - -// Store options -export interface ListOptions { - limit?: number; - offset?: number; - connectionId?: string; - sort?: { field: string; order: 'asc' | 'desc' }; -} - -export interface QueryOptions extends ListOptions { - indexBy?: string; -} - -export interface StoreOptions { - connectionId?: string; -} - -// Event bus for database events -export const dbEvents = new EventEmitter(); diff --git a/src/framework/DebrosFramework.ts b/src/framework/DebrosFramework.ts new file mode 100644 index 0000000..0ad0372 --- /dev/null +++ b/src/framework/DebrosFramework.ts @@ -0,0 +1,794 @@ +/** + * 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, + ): Promise { + 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); + // Update status to reflect config changes + this.status.environment = this.config.environment || 'development'; + } + + // 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 { + 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 { + 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.storeType); + } + } + 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 { + 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 { + 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 { + 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 { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + await this.databaseManager.createUserDatabases(userId); + this.metrics.totalDatabases++; + } + + async getUserDatabase(userId: string, modelName: string): Promise { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + return await this.databaseManager.getUserDatabase(userId, modelName); + } + + async getGlobalDatabase(modelName: string): Promise { + if (!this.databaseManager) { + throw new Error('Framework not initialized'); + } + + return await this.databaseManager.getGlobalDatabase(modelName); + } + + // Migration operations + async runMigration(migrationId: string, options?: any): Promise { + 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 { + 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 { + 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 - only require core services to be healthy + const coreServicesHealthy = + this.status.services.orbitdb === 'connected' && + this.status.services.ipfs === 'connected'; + + this.status.healthy = this.initialized && coreServicesHealthy; + } 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; + } + + getQueryCache(): QueryCache | null { + return this.queryCache; + } + + getOrbitDBService(): FrameworkOrbitDBService | null { + return this.orbitDBService; + } + + getIPFSService(): FrameworkIPFSService | null { + return this.ipfsService; + } + + getConfigManager(): ConfigManager | null { + return this.configManager; + } + + async healthCheck(): Promise { + this.performHealthCheck(); + return { + healthy: this.status.healthy, + services: { ...this.status.services }, + lastCheck: this.status.lastHealthCheck + }; + } + + // Framework lifecycle + async stop(): Promise { + 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): Promise { + 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 { + // 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 { + const framework = new DebrosFramework(config); + return framework; + } + + static async createWithServices( + orbitDBService: any, + ipfsService: any, + config: DebrosFrameworkConfig = {}, + ): Promise { + const framework = new DebrosFramework(config); + await framework.initialize(orbitDBService, ipfsService); + return framework; + } +} + +// Export the main framework class as default +export default DebrosFramework; diff --git a/src/framework/core/ConfigManager.ts b/src/framework/core/ConfigManager.ts new file mode 100644 index 0000000..162917d --- /dev/null +++ b/src/framework/core/ConfigManager.ts @@ -0,0 +1,202 @@ +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): void { + this.config = this.mergeWithDefaults({ + ...this.config, + ...newConfig, + }); + this.validateConfig(); + } + + // Get full configuration + getConfig(): ExtendedFrameworkConfig { + return { ...this.config }; + } + + // Alias for getConfig() to match test expectations + getFullConfig(): ExtendedFrameworkConfig { + return this.getConfig(); + } + + // 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 + }; + } +} diff --git a/src/framework/core/DatabaseManager.ts b/src/framework/core/DatabaseManager.ts new file mode 100644 index 0000000..e360353 --- /dev/null +++ b/src/framework/core/DatabaseManager.ts @@ -0,0 +1,367 @@ +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, + ) {} +} + +export class DatabaseManager { + private orbitDBService: FrameworkOrbitDBService; + private databases: Map = new Map(); + private userMappings: Map = new Map(); + private globalDatabases: Map = new Map(); + private globalDirectoryShards: any[] = []; + private initialized: boolean = false; + + constructor(orbitDBService: FrameworkOrbitDBService) { + this.orbitDBService = orbitDBService; + } + + async initializeAllDatabases(): Promise { + 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 { + 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 as any).dbType || model.storeType, 'global'); + this.globalDatabases.set(model.modelName, db); + + console.log(`✓ Created global database: ${dbName} (${(model as any).dbType || model.storeType})`); + } catch (error) { + console.error(`❌ Failed to create global database ${dbName}:`, error); + throw error; + } + } + } + + private async initializeSystemDatabases(): Promise { + 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 { + console.log(`👤 Creating databases for user: ${userId}`); + + const userScopedModels = ModelRegistry.getUserScopedModels(); + const databases: Record = {}; + + // 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 as any).dbType || model.storeType, 'user'); + databases[`${model.modelName.toLowerCase()}DB`] = db.address.toString(); + + console.log(`✓ Created user database: ${dbName} (${(model as any).dbType || model.storeType})`); + } 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 { + 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 { + // 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 { + const db = this.globalDatabases.get(modelName); + if (!db) { + throw new Error(`Global database not found for model: ${modelName}`); + } + return db; + } + + async getGlobalDirectoryShards(): Promise { + return this.globalDirectoryShards; + } + + private async createDatabase(name: string, type: StoreType, _scope: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + console.log('🛑 Stopping DatabaseManager...'); + + // Clear caches + this.databases.clear(); + this.userMappings.clear(); + this.globalDatabases.clear(); + this.globalDirectoryShards = []; + + this.initialized = false; + console.log('✅ DatabaseManager stopped'); + } +} diff --git a/src/framework/core/ModelRegistry.ts b/src/framework/core/ModelRegistry.ts new file mode 100644 index 0000000..fbf0273 --- /dev/null +++ b/src/framework/core/ModelRegistry.ts @@ -0,0 +1,104 @@ +import { BaseModel } from '../models/BaseModel'; +import { ModelConfig } from '../types/models'; +import { StoreType } from '../types/framework'; + +export class ModelRegistry { + private static models: Map = new Map(); + private static configs: Map = 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 { + return new Map(this.models); + } + + static getUserScopedModels(): Array { + return Array.from(this.models.values()).filter((model) => model.scope === 'user'); + } + + static getGlobalModels(): Array { + 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'); + } + } +} diff --git a/src/framework/index.ts b/src/framework/index.ts new file mode 100644 index 0000000..4c09c4d --- /dev/null +++ b/src/framework/index.ts @@ -0,0 +1,169 @@ +/** + * DebrosFramework - Main Export File + * + * This file exports all framework components for easy import and usage. + * It provides a clean API surface for consumers of the framework. + */ + +// Main framework class +export { DebrosFramework as default, DebrosFramework } from './DebrosFramework'; +export type { DebrosFrameworkConfig, FrameworkMetrics, FrameworkStatus } from './DebrosFramework'; + +// Core model system +export { BaseModel } from './models/BaseModel'; +export { ModelRegistry } from './core/ModelRegistry'; + +// Decorators +export { Model } from './models/decorators/Model'; +export { Field } from './models/decorators/Field'; +export { BelongsTo, HasMany, HasOne, ManyToMany } from './models/decorators/relationships'; +export { + BeforeCreate, + AfterCreate, + BeforeUpdate, + AfterUpdate, + BeforeDelete, + AfterDelete, +} from './models/decorators/hooks'; + +// Core services +export { DatabaseManager } from './core/DatabaseManager'; +export { ShardManager } from './sharding/ShardManager'; +export { ConfigManager } from './core/ConfigManager'; +export { FrameworkOrbitDBService, FrameworkIPFSService } from './services/OrbitDBService'; + +// Query system +export { QueryBuilder } from './query/QueryBuilder'; +export { QueryExecutor } from './query/QueryExecutor'; +export { QueryOptimizer } from './query/QueryOptimizer'; +export { QueryCache } from './query/QueryCache'; + +// Relationship system +export { RelationshipManager } from './relationships/RelationshipManager'; +export { RelationshipCache } from './relationships/RelationshipCache'; +export { LazyLoader } from './relationships/LazyLoader'; +export type { RelationshipLoadOptions, EagerLoadPlan } from './relationships/RelationshipManager'; + +// Automatic features +export { PinningManager } from './pinning/PinningManager'; +export { PubSubManager } from './pubsub/PubSubManager'; + +// Migration system +export { MigrationManager } from './migrations/MigrationManager'; +export { MigrationBuilder, createMigration } from './migrations/MigrationBuilder'; +export type { + Migration, + MigrationOperation, + MigrationValidator, + MigrationContext, + MigrationProgress, + MigrationResult, +} from './migrations/MigrationManager'; + +// Type definitions +export type { + StoreType, + FrameworkConfig, + CacheConfig, + PinningConfig, + PinningStrategy, + PinningStats, + ShardingConfig, + ValidationResult, +} from './types/framework'; + +export type { FieldConfig, RelationshipConfig, ModelConfig, ValidationError } from './types/models'; + +// Utility functions and helpers +// export { ValidationError } from './types/models'; // Already exported above + +// Version information +export const FRAMEWORK_VERSION = '1.0.0'; +export const API_VERSION = '1.0'; + +// Feature flags for conditional exports +export const FEATURES = { + MODELS: true, + RELATIONSHIPS: true, + QUERIES: true, + MIGRATIONS: true, + PINNING: true, + PUBSUB: true, + CACHING: true, + SHARDING: true, +} as const; + +// Quick setup helpers +import { DebrosFramework, DebrosFrameworkConfig } from './DebrosFramework'; + +export function createFramework(config?: DebrosFrameworkConfig) { + return DebrosFramework.create(config); +} + +export async function createFrameworkWithServices( + orbitDBService: any, + ipfsService: any, + config?: DebrosFrameworkConfig, +) { + return DebrosFramework.createWithServices(orbitDBService, ipfsService, config); +} + +// Export default configuration presets +export const DEVELOPMENT_CONFIG: Partial = { + environment: 'development', + features: { + autoMigration: true, + automaticPinning: false, + pubsub: true, + queryCache: true, + relationshipCache: true, + }, + performance: { + queryTimeout: 30000, + batchSize: 50, + }, + monitoring: { + enableMetrics: true, + logLevel: 'debug', + }, +}; + +export const PRODUCTION_CONFIG: Partial = { + environment: 'production', + features: { + autoMigration: false, // Require manual migration in production + automaticPinning: true, + pubsub: true, + queryCache: true, + relationshipCache: true, + }, + performance: { + queryTimeout: 10000, + batchSize: 200, + maxConcurrentOperations: 500, + }, + monitoring: { + enableMetrics: true, + logLevel: 'warn', + metricsInterval: 30000, + }, +}; + +export const TEST_CONFIG: Partial = { + environment: 'test', + features: { + autoMigration: true, + automaticPinning: false, + pubsub: false, + queryCache: false, + relationshipCache: false, + }, + performance: { + queryTimeout: 5000, + batchSize: 10, + }, + monitoring: { + enableMetrics: false, + logLevel: 'error', + }, +}; diff --git a/src/framework/migrations/MigrationBuilder.ts b/src/framework/migrations/MigrationBuilder.ts new file mode 100644 index 0000000..0396750 --- /dev/null +++ b/src/framework/migrations/MigrationBuilder.ts @@ -0,0 +1,460 @@ +/** + * 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; + 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, + rollbackOperation?: (context: any) => Promise, + ): 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, + 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, + ): 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); +} diff --git a/src/framework/migrations/MigrationManager.ts b/src/framework/migrations/MigrationManager.ts new file mode 100644 index 0000000..7f79aff --- /dev/null +++ b/src/framework/migrations/MigrationManager.ts @@ -0,0 +1,972 @@ +/** + * 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; + rollbackOperation?: (context: MigrationContext) => Promise; + options?: { + batchSize?: number; + parallel?: boolean; + skipValidation?: boolean; + }; +} + +export interface MigrationValidator { + name: string; + description: string; + validate: (context: MigrationContext) => Promise; +} + +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 = new Map(); + private migrationHistory: Map = new Map(); + private activeMigrations: Map = 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + // 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + // 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 { + 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 { + this.logger.info('Cleaning up migration manager'); + this.activeMigrations.clear(); + } +} diff --git a/src/framework/models/BaseModel.ts b/src/framework/models/BaseModel.ts new file mode 100644 index 0000000..ec1f07f --- /dev/null +++ b/src/framework/models/BaseModel.ts @@ -0,0 +1,922 @@ +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 _loadedRelations: Map = new Map(); + protected _isDirty: boolean = false; + protected _isNew: boolean = true; + + // Static properties for model configuration + static modelName: string; + static storeType: StoreType = 'docstore'; + static scope: 'user' | 'global' = 'global'; + static sharding?: ShardingConfig; + static pinning?: PinningConfig; + static fields: Map = new Map(); + static relationships: Map = new Map(); + static hooks: Map = new Map(); + + constructor(data: any = {}) { + // Generate ID first + this.id = this.generateId(); + + // Apply field defaults first + this.applyFieldDefaults(); + + // Then apply provided data, but only for properties that are explicitly provided + if (data && typeof data === 'object') { + Object.keys(data).forEach((key) => { + if (key !== '_loadedRelations' && key !== '_isDirty' && key !== '_isNew' && data[key] !== undefined) { + // Always set directly - the Field decorator's setter will handle validation and transformation + try { + (this as any)[key] = data[key]; + } catch (error) { + console.error(`Error setting field ${key}:`, error); + // If Field setter fails, set the private key directly + const privateKey = `_${key}`; + (this as any)[privateKey] = data[key]; + } + } + }); + + // Mark as existing if it has an ID in the data + if (data.id) { + this._isNew = false; + } + } + + // Remove any instance properties that might shadow prototype getters + this.cleanupShadowingProperties(); + + } + + private cleanupShadowingProperties(): void { + const modelClass = this.constructor as typeof BaseModel; + + // For each field, ensure no instance properties are shadowing prototype getters + for (const [fieldName] of modelClass.fields) { + // If there's an instance property, remove it and create a working getter + if (this.hasOwnProperty(fieldName)) { + const _oldValue = (this as any)[fieldName]; + delete (this as any)[fieldName]; + + // Define a working getter directly on the instance + Object.defineProperty(this, fieldName, { + get: () => { + const privateKey = `_${fieldName}`; + return (this as any)[privateKey]; + }, + set: (value: any) => { + const privateKey = `_${fieldName}`; + (this as any)[privateKey] = value; + this.markFieldAsModified(fieldName); + }, + enumerable: true, + configurable: true + }); + } + } + } + + + // Core CRUD operations + async save(): Promise { + if (this._isNew) { + // Clean up any instance properties before hooks run + this.cleanupShadowingProperties(); + + await this.beforeCreate(); + + // Clean up any instance properties created by hooks + this.cleanupShadowingProperties(); + + // Generate ID if not provided + if (!this.id) { + this.id = this.generateId(); + } + + // Set timestamps using Field setters + const now = Date.now(); + this.setFieldValue('createdAt', now); + this.setFieldValue('updatedAt', now); + + // Clean up any additional shadowing properties after setting timestamps + this.cleanupShadowingProperties(); + + + // Validate after all field generation is complete + await this.validate(); + + // Save to database (will be implemented when database manager is ready) + await this._saveToDatabase(); + + this._isNew = false; + this.clearModifications(); + + await this.afterCreate(); + + // Clean up any shadowing properties created during save + this.cleanupShadowingProperties(); + } else if (this._isDirty) { + await this.beforeUpdate(); + + // Set timestamp using Field setter + this.setFieldValue('updatedAt', Date.now()); + + // Validate after hooks have run + await this.validate(); + + // Update in database + await this._updateInDatabase(); + + this.clearModifications(); + + await this.afterUpdate(); + + // Clean up any shadowing properties created during save + this.cleanupShadowingProperties(); + } + + return this; + } + + static async create(this: new (data?: any) => T, data: any): Promise { + const instance = new this(data); + return await instance.save(); + } + + static async get( + this: typeof BaseModel & (new (data?: any) => T), + id: string, + ): Promise { + return await this.findById(id); + } + + static async findById( + this: typeof BaseModel & (new (data?: any) => T), + id: string, + ): Promise { + // Use the mock framework for testing + const framework = (globalThis as any).__debrosFramework || this.getMockFramework(); + if (!framework) { + return null; + } + + try { + const modelClass = this as any; + let data = null; + + if (modelClass.scope === 'user') { + // For user-scoped models, we would need userId - for now, try global + const database = await framework.databaseManager?.getGlobalDatabase?.(modelClass.modelName || modelClass.name); + if (database && framework.databaseManager?.getDocument) { + data = await framework.databaseManager.getDocument(database, modelClass.storeType, id); + } + } else { + if (modelClass.sharding) { + const shard = framework.shardManager?.getShardForKey?.(modelClass.modelName || modelClass.name, id); + if (shard && framework.databaseManager?.getDocument) { + data = await framework.databaseManager.getDocument(shard.database, modelClass.storeType, id); + } + } else { + const database = await framework.databaseManager?.getGlobalDatabase?.(modelClass.modelName || modelClass.name); + if (database && framework.databaseManager?.getDocument) { + data = await framework.databaseManager.getDocument(database, modelClass.storeType, id); + } + } + } + + if (data) { + const instance = new (this as any)(data); + instance._isNew = false; + instance.clearModifications(); + return instance; + } + + return null; + } catch (error) { + console.error('Failed to find by ID:', error); + return null; + } + } + + static async find( + this: typeof BaseModel & (new (data?: any) => T), + id: string, + ): Promise { + const result = await this.get(id); + if (!result) { + throw new Error(`${this.name} with id ${id} not found`); + } + return result; + } + + async update(data: Partial): Promise { + Object.assign(this, data); + this._isDirty = true; + return await this.save(); + } + + async delete(): Promise { + 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( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + operator: string, + value: any, + ): QueryBuilder { + return new QueryBuilder(this as any).where(field, operator, value); + } + + static whereIn( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + values: any[], + ): QueryBuilder { + return new QueryBuilder(this as any).whereIn(field, values); + } + + static orderBy( + this: typeof BaseModel & (new (data?: any) => T), + field: string, + direction: 'asc' | 'desc' = 'asc', + ): QueryBuilder { + return new QueryBuilder(this as any).orderBy(field, direction); + } + + static limit( + this: typeof BaseModel & (new (data?: any) => T), + count: number, + ): QueryBuilder { + return new QueryBuilder(this as any).limit(count); + } + + static async all( + this: typeof BaseModel & (new (data?: any) => T), + ): Promise { + return await new QueryBuilder(this as any).exec(); + } + + static async findAll( + this: typeof BaseModel & (new (data?: any) => T), + ): Promise { + return await this.all(); + } + + static async findOne( + this: typeof BaseModel & (new (data?: any) => T), + criteria: any, + ): Promise { + const query = new QueryBuilder(this as any); + + // Apply criteria as where clauses + Object.keys(criteria).forEach(key => { + query.where(key, '=', criteria[key]); + }); + + const results = await query.limit(1).exec(); + return results.length > 0 ? results[0] : null; + } + + // Relationship operations + async load(relationships: string[]): Promise { + 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 { + // 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 { + 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 { + // 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 = {}; + const modelClass = this.constructor as typeof BaseModel; + + // Include all field values using private keys (more reliable than getters) + for (const [fieldName] of modelClass.fields) { + const privateKey = `_${fieldName}`; + const value = (this as any)[privateKey]; + if (value !== undefined) { + result[fieldName] = value; + } + } + + // Include basic properties + result.id = this.id; + + // 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 { + const errors: string[] = []; + const modelClass = this.constructor as typeof BaseModel; + + + // Validate each field using private keys (more reliable) + for (const [fieldName, fieldConfig] of modelClass.fields) { + const privateKey = `_${fieldName}`; + const value = (this as any)[privateKey]; + + const fieldErrors = await 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 async validateField(fieldName: string, value: any, config: FieldConfig): Promise { + 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}`); + } + + // Unique constraint validation + if (config.unique && value !== undefined && value !== null && value !== '') { + const modelClass = this.constructor as typeof BaseModel; + try { + const existing = await (modelClass as any).findOne({ [fieldName]: value }); + if (existing && existing.id !== this.id) { + errors.push(`${fieldName} must be unique`); + } + } catch (error) { + // If we can't query for duplicates, skip unique validation + console.warn(`Could not validate unique constraint for ${fieldName}:`, error); + } + } + + // 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 { + await this.runHooks('beforeCreate'); + } + + async afterCreate(): Promise { + await this.runHooks('afterCreate'); + } + + async beforeUpdate(): Promise { + await this.runHooks('beforeUpdate'); + } + + async afterUpdate(): Promise { + await this.runHooks('afterUpdate'); + } + + async beforeDelete(): Promise { + await this.runHooks('beforeDelete'); + } + + async afterDelete(): Promise { + await this.runHooks('afterDelete'); + } + + private async runHooks(hookName: string): Promise { + const modelClass = this.constructor as typeof BaseModel; + const hookNames = modelClass.hooks.get(hookName) || []; + + for (const hookMethodName of hookNames) { + const hookMethod = (this as any)[String(hookMethodName)]; + if (typeof hookMethod === 'function') { + await hookMethod.call(this); + } + } + } + + // Utility methods + private generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + } + + private applyFieldDefaults(): void { + const modelClass = this.constructor as typeof BaseModel; + + // Ensure we have fields map + if (!modelClass.fields) { + return; + } + + for (const [fieldName, fieldConfig] of modelClass.fields) { + if (fieldConfig.default !== undefined) { + const privateKey = `_${fieldName}`; + const hasProperty = (this as any).hasOwnProperty(privateKey); + const currentValue = (this as any)[privateKey]; + + // Always apply default value to private field if it's not set + if (!hasProperty || currentValue === undefined) { + // Apply default value to private field + if (typeof fieldConfig.default === 'function') { + (this as any)[privateKey] = fieldConfig.default(); + } else { + (this as any)[privateKey] = fieldConfig.default; + } + } + } + } + } + + // Field modification tracking + private _modifiedFields: Set = new Set(); + + markFieldAsModified(fieldName: string): void { + this._modifiedFields.add(fieldName); + this._isDirty = true; + } + + getModifiedFields(): string[] { + return Array.from(this._modifiedFields); + } + + isFieldModified(fieldName: string): boolean { + return this._modifiedFields.has(fieldName); + } + + clearModifications(): void { + this._modifiedFields.clear(); + this._isDirty = false; + } + + // Reliable field access methods that bypass problematic getters + getFieldValue(fieldName: string): any { + // Always ensure this field's getter works properly + this.ensureFieldGetter(fieldName); + + const privateKey = `_${fieldName}`; + return (this as any)[privateKey]; + } + + private ensureFieldGetter(fieldName: string): void { + // If there's a shadowing instance property, remove it and create a working getter + if (this.hasOwnProperty(fieldName)) { + delete (this as any)[fieldName]; + + // Define a working getter directly on the instance + Object.defineProperty(this, fieldName, { + get: () => { + const privateKey = `_${fieldName}`; + return (this as any)[privateKey]; + }, + set: (value: any) => { + const privateKey = `_${fieldName}`; + (this as any)[privateKey] = value; + this.markFieldAsModified(fieldName); + }, + enumerable: true, + configurable: true + }); + } + } + + setFieldValue(fieldName: string, value: any): void { + // Try to use the Field decorator's setter first + try { + (this as any)[fieldName] = value; + } catch (_error) { + // Fallback to setting private key directly + const privateKey = `_${fieldName}`; + (this as any)[privateKey] = value; + this.markFieldAsModified(fieldName); + } + } + + getAllFieldValues(): Record { + const modelClass = this.constructor as typeof BaseModel; + const values: Record = {}; + + for (const [fieldName] of modelClass.fields) { + const value = this.getFieldValue(fieldName); + if (value !== undefined) { + values[fieldName] = value; + } + } + + return values; + } + + // Ensure user databases exist for user-scoped models + private async ensureUserDatabasesExist(framework: any, userId: string): Promise { + try { + // Try to get user databases - if this fails, they don't exist + await framework.databaseManager.getUserMappings(userId); + } catch (error) { + // If user not found, create databases for them + if ((error as Error).message.includes('not found in directory')) { + console.log(`Creating databases for user ${userId}`); + await framework.databaseManager.createUserDatabases(userId); + } else { + throw error; + } + } + } + + // Database operations integrated with DatabaseManager + private async _saveToDatabase(): Promise { + 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 (check common field names) + const userId = (this as any).userId || (this as any).authorId || (this as any).ownerId; + if (!userId) { + throw new Error('User-scoped models must have a userId, authorId, or ownerId field'); + } + + // Ensure user databases exist before accessing them + await this.ensureUserDatabasesExist(framework, userId); + + // Ensure user databases exist before accessing them + await this.ensureUserDatabasesExist(framework, userId); + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.addDocument(database, modelClass.storeType, 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.storeType, + this.toJSON(), + ); + } else { + // Use single global database + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.addDocument(database, modelClass.storeType, this.toJSON()); + } + } + } catch (error) { + console.error('Failed to save to database:', error); + throw error; + } + } + + private async _updateInDatabase(): Promise { + 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 || (this as any).authorId || (this as any).ownerId; + if (!userId) { + throw new Error('User-scoped models must have a userId, authorId, or ownerId field'); + } + + // Ensure user databases exist before accessing them + await this.ensureUserDatabasesExist(framework, userId); + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.updateDocument( + database, + modelClass.storeType, + this.id, + this.toJSON(), + ); + } else { + if (modelClass.sharding) { + const shard = framework.shardManager.getShardForKey(modelClass.modelName, this.id); + await framework.databaseManager.updateDocument( + shard.database, + modelClass.storeType, + this.id, + this.toJSON(), + ); + } else { + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.updateDocument( + database, + modelClass.storeType, + this.id, + this.toJSON(), + ); + } + } + } catch (error) { + console.error('Failed to update in database:', error); + throw error; + } + } + + private async _deleteFromDatabase(): Promise { + 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 || (this as any).authorId || (this as any).ownerId; + if (!userId) { + throw new Error('User-scoped models must have a userId, authorId, or ownerId field'); + } + + // Ensure user databases exist before accessing them + await this.ensureUserDatabasesExist(framework, userId); + + const database = await framework.databaseManager.getUserDatabase( + userId, + modelClass.modelName, + ); + await framework.databaseManager.deleteDocument(database, modelClass.storeType, this.id); + } else { + if (modelClass.sharding) { + const shard = framework.shardManager.getShardForKey(modelClass.modelName, this.id); + await framework.databaseManager.deleteDocument( + shard.database, + modelClass.storeType, + this.id, + ); + } else { + const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName); + await framework.databaseManager.deleteDocument(database, modelClass.storeType, 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 + const framework = (globalThis as any).__debrosFramework; + if (!framework) { + // Try to get mock framework for testing + const mockFramework = (this.constructor as any).getMockFramework?.(); + return mockFramework; + } + return framework; + } + + // 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 || []; + } + + static fromJSON(this: new (data?: any) => T, data: any): T { + const instance = new this(); + Object.assign(instance, data); + return instance; + } + + static query(this: typeof BaseModel & (new (data?: any) => T)): any { + const { QueryBuilder } = require('../query/QueryBuilder'); + return new QueryBuilder(this); + } + + // Mock framework for testing + static getMockFramework(): any { + if (typeof jest !== 'undefined') { + // Create a simple mock framework with shared mock database storage + if (!(globalThis as any).__mockDatabase) { + (globalThis as any).__mockDatabase = new Map(); + } + + const mockDatabase = { + _data: (globalThis as any).__mockDatabase, + async get(id: string) { + return this._data.get(id) || null; + }, + async put(doc: any) { + const id = doc._id || doc.id; + this._data.set(id, doc); + return id; + }, + async del(id: string) { + return this._data.delete(id); + }, + async all() { + return Array.from(this._data.values()); + } + }; + + return { + databaseManager: { + async getGlobalDatabase(_name: string) { + return mockDatabase; + }, + async getUserDatabase(_userId: string, _name: string) { + return mockDatabase; + }, + async getUserMappings(_userId: string) { + // Mock user mappings - return a simple mapping + return { userId: _userId, databases: {} }; + }, + async createUserDatabases(_userId: string) { + // Mock user database creation - do nothing for tests + return; + }, + async getDocument(_database: any, _type: string, id: string) { + return await mockDatabase.get(id); + }, + async addDocument(_database: any, _type: string, doc: any) { + return await mockDatabase.put(doc); + }, + async updateDocument(_database: any, _type: string, id: string, doc: any) { + doc.id = id; + return await mockDatabase.put(doc); + }, + async deleteDocument(_database: any, _type: string, id: string) { + return await mockDatabase.del(id); + }, + async getAllDocuments(_database: any, _type: string) { + return await mockDatabase.all(); + } + }, + shardManager: { + getShardForKey(_modelName: string, _key: string) { + return { database: mockDatabase }; + } + } + }; + } + return null; + } +} diff --git a/src/framework/models/decorators/Field.ts b/src/framework/models/decorators/Field.ts new file mode 100644 index 0000000..53962d6 --- /dev/null +++ b/src/framework/models/decorators/Field.ts @@ -0,0 +1,229 @@ +import { FieldConfig, ValidationError } from '../../types/models'; +import { BaseModel } from '../BaseModel'; + +export function Field(config: FieldConfig) { + return function (target: any, propertyKey: string) { + // Validate field configuration + validateFieldConfig(config); + + // Handle ESM case where target might be undefined + if (!target) { + // In ESM environment, defer the decorator application + // Create a deferred setup that will be called when the class is actually used + console.warn(`Target is undefined for field:`, { + propertyKey, + propertyKeyType: typeof propertyKey, + propertyKeyValue: JSON.stringify(propertyKey), + configType: config.type, + target, + targetType: typeof target + }); + deferredFieldSetup(config, propertyKey); + return; + } + + // Get the constructor function - handle ESM case where constructor might be undefined + const ctor = (target.constructor || target) as typeof BaseModel; + + // Initialize fields map if it doesn't exist + if (!ctor.hasOwnProperty('fields')) { + const parentFields = ctor.fields ? new Map(ctor.fields) : new Map(); + Object.defineProperty(ctor, 'fields', { + value: parentFields, + writable: true, + enumerable: false, + configurable: true, + }); + } + + // Store field configuration + ctor.fields.set(propertyKey, config); + + // Define property on the prototype + Object.defineProperty(target, propertyKey, { + get() { + const privateKey = `_${propertyKey}`; + return this[privateKey]; + }, + set(value) { + const privateKey = `_${propertyKey}`; + const ctor = this.constructor as typeof BaseModel; + + // Ensure fields map exists on the constructor + if (!ctor.hasOwnProperty('fields')) { + const parentFields = ctor.fields ? new Map(ctor.fields) : new Map(); + Object.defineProperty(ctor, 'fields', { + value: parentFields, + writable: true, + enumerable: false, + configurable: true, + }); + } + + // Store field configuration if it's not already there + if (!ctor.fields.has(propertyKey)) { + ctor.fields.set(propertyKey, config); + } + + // Apply transformation first + const transformedValue = config.transform ? config.transform(value) : value; + + // Only validate non-required constraints during assignment + const validationResult = validateFieldValueNonRequired( + transformedValue, + config, + propertyKey, + ); + if (!validationResult.valid) { + throw new ValidationError(validationResult.errors); + } + + // Check if value actually changed + const oldValue = this[privateKey]; + if (oldValue !== transformedValue) { + // Set the value and mark as dirty + this[privateKey] = transformedValue; + if (this._isDirty !== undefined) { + this._isDirty = true; + } + // Track field modification + if (this.markFieldAsModified && typeof this.markFieldAsModified === 'function') { + this.markFieldAsModified(propertyKey); + } + } + }, + enumerable: true, + configurable: true, + }); + }; +} + +function validateFieldConfig(config: FieldConfig): void { + const validTypes = ['string', 'number', 'boolean', 'array', 'object', 'date']; + if (!validTypes.includes(config.type)) { + throw new Error( + `Invalid field type: ${config.type}. Valid types are: ${validTypes.join(', ')}`, + ); + } +} + +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 validateFieldValueNonRequired( + value: any, + config: FieldConfig, + fieldName: string, +): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + // Skip required validation during assignment + // Skip further validation if value is empty + 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 { + // Handle both class constructors and instances + let current = target; + if (target.constructor && target.constructor !== Function) { + current = target.constructor; + } + + // Walk up the prototype chain to find field configuration + while (current && current !== Function && current !== Object) { + if (current.fields && current.fields.has(propertyKey)) { + return current.fields.get(propertyKey); + } + current = Object.getPrototypeOf(current); + // Stop if we've reached the base class or gone too far + if (current === Function.prototype || current === Object.prototype) { + break; + } + } + + return undefined; +} + +// Deferred setup function for ESM environments +function deferredFieldSetup(config: FieldConfig, propertyKey: string) { + // Return a function that will be called when the class is properly initialized + return function() { + // This function will be called later when the class prototype is ready + console.warn(`Deferred field setup not yet implemented for property ${propertyKey}`); + }; +} + +// Export the decorator type for TypeScript +export type FieldDecorator = (config: FieldConfig) => (target: any, propertyKey: string) => void; diff --git a/src/framework/models/decorators/Model.ts b/src/framework/models/decorators/Model.ts new file mode 100644 index 0000000..e8f7ca8 --- /dev/null +++ b/src/framework/models/decorators/Model.ts @@ -0,0 +1,151 @@ +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 (target: T): T { + // Validate model configuration + validateModelConfig(config); + + // Initialize model-specific metadata maps, preserving existing ones + if (!target.hasOwnProperty('fields')) { + // Copy existing fields from prototype if any + const parentFields = target.fields; + Object.defineProperty(target, 'fields', { + value: new Map(), + writable: true, + enumerable: false, + configurable: true + }); + if (parentFields) { + for (const [key, value] of parentFields) { + target.fields.set(key, value); + } + } + } + if (!target.hasOwnProperty('relationships')) { + // Copy existing relationships from prototype if any + const parentRelationships = target.relationships; + target.relationships = new Map(); + if (parentRelationships) { + for (const [key, value] of parentRelationships) { + target.relationships.set(key, value); + } + } + } + if (!target.hasOwnProperty('hooks')) { + // Copy existing hooks from prototype if any + const parentHooks = target.hooks; + target.hooks = new Map(); + if (parentHooks) { + for (const [key, value] of parentHooks) { + target.hooks.set(key, value); + } + } + } + + // Set model configuration on the class using defineProperty to ensure they're own properties + const modelName = config.tableName || target.name; + const storeType = config.type || autoDetectType(target); + const scope = config.scope || 'global'; + + Object.defineProperty(target, 'modelName', { + value: modelName, + writable: true, + enumerable: false, + configurable: true + }); + + Object.defineProperty(target, 'storeType', { + value: storeType, + writable: true, + enumerable: true, + configurable: true + }); + + // Also set dbType for backwards compatibility + Object.defineProperty(target, 'dbType', { + value: storeType, + writable: true, + enumerable: true, + configurable: true + }); + + Object.defineProperty(target, 'scope', { + value: scope, + writable: true, + enumerable: false, + configurable: true + }); + + if (config.sharding) { + Object.defineProperty(target, 'sharding', { + value: config.sharding, + writable: true, + enumerable: false, + configurable: true + }); + } + + if (config.pinning) { + Object.defineProperty(target, 'pinning', { + value: config.pinning, + writable: true, + enumerable: false, + configurable: true + }); + } + + + // 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 validateModelConfig(config: ModelConfig): void { + if (config.scope && !['user', 'global'].includes(config.scope)) { + throw new Error(`Invalid model scope: ${config.scope}. Valid scopes are: user, global`); + } + + if (config.type && !['docstore', 'keyvalue', 'eventlog'].includes(config.type)) { + throw new Error(`Invalid store type: ${config.type}. Valid types are: docstore, keyvalue, eventlog`); + } +} + +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) => (target: T) => T; diff --git a/src/framework/models/decorators/hooks.ts b/src/framework/models/decorators/hooks.ts new file mode 100644 index 0000000..409f78b --- /dev/null +++ b/src/framework/models/decorators/hooks.ts @@ -0,0 +1,303 @@ +export function BeforeCreate( + target?: any, + propertyKey?: string, + descriptor?: PropertyDescriptor, +): any { + // If used as @BeforeCreate (without parentheses) + if (target && propertyKey && descriptor) { + registerHook(target, 'beforeCreate', descriptor.value); + return descriptor; + } + + // If used as @BeforeCreate() (with parentheses) + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Handle case where descriptor might be undefined + if (!descriptor) { + // For method decorators, we need to get the method from the target + const method = target[propertyKey]; + if (typeof method === 'function') { + registerHook(target, 'beforeCreate', method); + } + return; + } + registerHook(target, 'beforeCreate', descriptor.value); + return descriptor; + }; +} + +export function AfterCreate( + target?: any, + propertyKey?: string, + descriptor?: PropertyDescriptor, +): any { + if (target && propertyKey && descriptor) { + registerHook(target, 'afterCreate', descriptor.value); + return descriptor; + } + + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Handle case where descriptor might be undefined + if (!descriptor) { + // For method decorators, we need to get the method from the target + const method = target[propertyKey]; + if (typeof method === 'function') { + registerHook(target, 'afterCreate', method); + } + return; + } + registerHook(target, 'afterCreate', descriptor.value); + return descriptor; + }; +} + +export function BeforeUpdate( + target?: any, + propertyKey?: string, + descriptor?: PropertyDescriptor, +): any { + if (target && propertyKey && descriptor) { + registerHook(target, 'beforeUpdate', descriptor.value); + return descriptor; + } + + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Handle case where descriptor might be undefined + if (!descriptor) { + // For method decorators, we need to get the method from the target + const method = target[propertyKey]; + if (typeof method === 'function') { + registerHook(target, 'beforeUpdate', method); + } + return; + } + registerHook(target, 'beforeUpdate', descriptor.value); + return descriptor; + }; +} + +export function AfterUpdate( + target?: any, + propertyKey?: string, + descriptor?: PropertyDescriptor, +): any { + if (target && propertyKey && descriptor) { + registerHook(target, 'afterUpdate', descriptor.value); + return descriptor; + } + + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Handle case where descriptor might be undefined + if (!descriptor) { + // For method decorators, we need to get the method from the target + const method = target[propertyKey]; + if (typeof method === 'function') { + registerHook(target, 'afterUpdate', method); + } + return; + } + registerHook(target, 'afterUpdate', descriptor.value); + return descriptor; + }; +} + +export function BeforeDelete( + target?: any, + propertyKey?: string, + descriptor?: PropertyDescriptor, +): any { + if (target && propertyKey && descriptor) { + registerHook(target, 'beforeDelete', descriptor.value); + return descriptor; + } + + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Handle case where descriptor might be undefined + if (!descriptor) { + // For method decorators, we need to get the method from the target + const method = target[propertyKey]; + if (typeof method === 'function') { + registerHook(target, 'beforeDelete', method); + } + return; + } + registerHook(target, 'beforeDelete', descriptor.value); + return descriptor; + }; +} + +export function AfterDelete( + target?: any, + propertyKey?: string, + descriptor?: PropertyDescriptor, +): any { + if (target && propertyKey && descriptor) { + registerHook(target, 'afterDelete', descriptor.value); + return descriptor; + } + + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Handle case where descriptor might be undefined + if (!descriptor) { + // For method decorators, we need to get the method from the target + const method = target[propertyKey]; + if (typeof method === 'function') { + registerHook(target, 'afterDelete', method); + } + return; + } + registerHook(target, 'afterDelete', descriptor.value); + return descriptor; + }; +} + +export function BeforeSave( + target?: any, + propertyKey?: string, + descriptor?: PropertyDescriptor, +): any { + if (target && propertyKey && descriptor) { + registerHook(target, 'beforeSave', descriptor.value); + return descriptor; + } + + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Handle case where descriptor might be undefined + if (!descriptor) { + // For method decorators, we need to get the method from the target + const method = target[propertyKey]; + if (typeof method === 'function') { + registerHook(target, 'beforeSave', method); + } + return; + } + registerHook(target, 'beforeSave', descriptor.value); + return descriptor; + }; +} + +export function AfterSave( + target?: any, + propertyKey?: string, + descriptor?: PropertyDescriptor, +): any { + if (target && propertyKey && descriptor) { + registerHook(target, 'afterSave', descriptor.value); + return descriptor; + } + + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Handle case where descriptor might be undefined + if (!descriptor) { + // For method decorators, we need to get the method from the target + const method = target[propertyKey]; + if (typeof method === 'function') { + registerHook(target, 'afterSave', method); + } + return; + } + registerHook(target, 'afterSave', descriptor.value); + return descriptor; + }; +} + +function registerHook(target: any, hookName: string, hookFunction: Function): void { + // Handle ESM case where target might be undefined + if (!target) { + // In ESM environment, defer the hook registration + // Create a deferred setup that will be called when the class is actually used + console.warn(`Target is undefined for hook:`, { + hookName, + hookNameType: typeof hookName, + hookNameValue: JSON.stringify(hookName), + hookFunction: hookFunction?.name || 'anonymous', + target, + targetType: typeof target + }); + deferredHookSetup(hookName, hookFunction); + return; + } + + // Get the constructor function - handle ESM case where constructor might be undefined + const ctor = target.constructor || target; + + // Additional safety check for constructor + if (!ctor) { + console.warn(`Constructor is undefined for hook ${hookName}, skipping hook registration`); + return; + } + + // Initialize hooks map if it doesn't exist, inheriting from parent + if (!ctor.hasOwnProperty('hooks')) { + // Copy hooks from parent class if they exist + const parentHooks = ctor.hooks || new Map(); + ctor.hooks = new Map(); + + // Copy all parent hooks + for (const [name, hooks] of parentHooks.entries()) { + ctor.hooks.set(name, [...hooks]); + } + } + + // Get existing hooks for this hook name + const existingHooks = ctor.hooks.get(hookName) || []; + + // Add the new hook (store the function name for the tests) + const functionName = hookFunction.name || 'anonymous'; + existingHooks.push(functionName); + + // Store updated hooks array + ctor.hooks.set(hookName, existingHooks); + + console.log(`Registered ${hookName} hook for ${ctor.name || 'Unknown'}`); +} + +// Utility function to get hooks for a specific event or all hooks +export function getHooks(target: any, hookName?: string): string[] | Record { + // Handle both class constructors and instances + let current = target; + if (target.constructor && target.constructor !== Function) { + current = target.constructor; + } + + // Collect hooks from the entire prototype chain + const allHooks: Record = {}; + + while (current && current !== Function && current !== Object) { + if (current.hooks) { + for (const [name, hookFunctions] of current.hooks.entries()) { + if (!allHooks[name]) { + allHooks[name] = []; + } + // Add hooks from this level (parent hooks first, child hooks last) + allHooks[name] = [...hookFunctions, ...allHooks[name]]; + } + } + current = Object.getPrototypeOf(current); + // Stop if we've reached the base class or gone too far + if (current === Function.prototype || current === Object.prototype) { + break; + } + } + + if (hookName) { + return allHooks[hookName] || []; + } else { + return allHooks; + } +} + +// Export decorator types for TypeScript +export type HookDecorator = ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, +) => void; + +// Deferred setup function for ESM environments +function deferredHookSetup(hookName: string, hookFunction: Function) { + // Return a function that will be called when the class is properly initialized + return function() { + // This function will be called later when the class prototype is ready + console.warn(`Deferred hook setup not yet implemented for hook ${hookName}`); + }; +} diff --git a/src/framework/models/decorators/index.ts b/src/framework/models/decorators/index.ts new file mode 100644 index 0000000..761ca1f --- /dev/null +++ b/src/framework/models/decorators/index.ts @@ -0,0 +1,35 @@ +// Model decorator +export { Model } from './Model'; + +// Field decorator +export { Field, getFieldConfig } from './Field'; + +// Relationship decorators +export { BelongsTo, HasMany, HasOne, ManyToMany, getRelationshipConfig } from './relationships'; + +// Hook decorators +export { + BeforeCreate, + AfterCreate, + BeforeUpdate, + AfterUpdate, + BeforeDelete, + AfterDelete, + BeforeSave, + AfterSave, + getHooks, +} from './hooks'; + +// Type exports +export type { ModelDecorator } from './Model'; + +export type { FieldDecorator } from './Field'; + +export type { + BelongsToDecorator, + HasManyDecorator, + HasOneDecorator, + ManyToManyDecorator, +} from './relationships'; + +export type { HookDecorator } from './hooks'; diff --git a/src/framework/models/decorators/relationships.ts b/src/framework/models/decorators/relationships.ts new file mode 100644 index 0000000..d7a5dfd --- /dev/null +++ b/src/framework/models/decorators/relationships.ts @@ -0,0 +1,247 @@ +import { BaseModel } from '../BaseModel'; +import { RelationshipConfig } from '../../types/models'; + +export function BelongsTo( + modelFactory: () => typeof BaseModel, + foreignKey: string, + options: { localKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'belongsTo', + modelFactory, + foreignKey, + localKey: options.localKey || 'id', + lazy: true, + options, + targetModel: modelFactory, // Add targetModel as alias for test compatibility + }; + + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function HasMany( + modelFactory: () => typeof BaseModel, + foreignKey: string, + options: any = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'hasMany', + modelFactory, + foreignKey, + localKey: options.localKey || 'id', + through: options.through, + lazy: true, + options, + targetModel: modelFactory, // Add targetModel as alias for test compatibility + }; + + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function HasOne( + modelFactory: () => typeof BaseModel, + foreignKey: string, + options: { localKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'hasOne', + modelFactory, + foreignKey, + localKey: options.localKey || 'id', + lazy: true, + options, + targetModel: modelFactory, // Add targetModel as alias for test compatibility + }; + + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function ManyToMany( + modelFactory: () => typeof BaseModel, + through: string, + foreignKey: string, + otherKey: string, + options: { localKey?: string; throughForeignKey?: string } = {}, +) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'manyToMany', + modelFactory, + foreignKey, + otherKey, + localKey: options.localKey || 'id', + through, + lazy: true, + options, + targetModel: modelFactory, // Add targetModel as alias for test compatibility + }; + + createRelationshipProperty(target, propertyKey, config); + }; +} + +function createRelationshipProperty( + target: any, + propertyKey: string, + config: RelationshipConfig, +): void { + // Handle ESM case where target might be undefined + if (!target) { + // In ESM environment, defer the decorator application + // Create a deferred setup that will be called when the class is actually used + deferredRelationshipSetup(config, propertyKey); + return; + } + + // Get the constructor function - handle ESM case where constructor might be undefined + const ctor = (target.constructor || target) as typeof BaseModel; + + // Additional safety check for constructor + if (!ctor) { + console.warn(`Constructor is undefined for property ${propertyKey}, skipping decorator setup`); + return; + } + + // Initialize relationships map if it doesn't exist + if (!ctor.hasOwnProperty('relationships')) { + const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map(); + Object.defineProperty(ctor, 'relationships', { + value: parentRelationships, + writable: true, + enumerable: false, + configurable: true, + }); + } + + // Store relationship configuration + ctor.relationships.set(propertyKey, config); + + // Define property on the prototype + Object.defineProperty(target, propertyKey, { + get() { + const ctor = this.constructor as typeof BaseModel; + + // Ensure relationships map exists on the constructor + if (!ctor.hasOwnProperty('relationships')) { + const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map(); + Object.defineProperty(ctor, 'relationships', { + value: parentRelationships, + writable: true, + enumerable: false, + configurable: true, + }); + } + + // Store relationship configuration if it's not already there + if (!ctor.relationships.has(propertyKey)) { + ctor.relationships.set(propertyKey, config); + } + + // 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) { + const ctor = this.constructor as typeof BaseModel; + + // Ensure relationships map exists on the constructor + if (!ctor.hasOwnProperty('relationships')) { + const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map(); + Object.defineProperty(ctor, 'relationships', { + value: parentRelationships, + writable: true, + enumerable: false, + configurable: true, + }); + } + + // Store relationship configuration if it's not already there + if (!ctor.relationships.has(propertyKey)) { + ctor.relationships.set(propertyKey, config); + } + + // 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 | RelationshipConfig[] { + // Handle both class constructors and instances + const relationships = + target.relationships || (target.constructor && target.constructor.relationships); + if (!relationships) { + return propertyKey ? undefined : []; + } + + if (propertyKey) { + return relationships.get(propertyKey); + } else { + return Array.from(relationships.values()).map((config, index) => { + const result = Object.assign({}, config) as any; + result.propertyKey = Array.from(relationships.keys())[index]; + return result as RelationshipConfig; + }); + } +} + +// Type definitions for decorators +export type BelongsToDecorator = ( + modelFactory: () => typeof BaseModel, + foreignKey: string, + options?: { localKey?: string }, +) => (target: any, propertyKey: string) => void; + +export type HasManyDecorator = ( + modelFactory: () => typeof BaseModel, + foreignKey: string, + options?: any, +) => (target: any, propertyKey: string) => void; + +export type HasOneDecorator = ( + modelFactory: () => typeof BaseModel, + foreignKey: string, + options?: { localKey?: string }, +) => (target: any, propertyKey: string) => void; + +export type ManyToManyDecorator = ( + modelFactory: () => typeof BaseModel, + through: string, + foreignKey: string, + otherKey: string, + options?: { localKey?: string; throughForeignKey?: string }, +) => (target: any, propertyKey: string) => void; + +// Deferred setup function for ESM environments +function deferredRelationshipSetup(config: RelationshipConfig, propertyKey: string) { + // Return a function that will be called when the class is properly initialized + return function () { + // This function will be called later when the class prototype is ready + console.warn(`Deferred relationship setup not yet implemented for property ${propertyKey}`); + }; +} diff --git a/src/framework/pinning/PinningManager.ts b/src/framework/pinning/PinningManager.ts new file mode 100644 index 0000000..f94ee3c --- /dev/null +++ b/src/framework/pinning/PinningManager.ts @@ -0,0 +1,598 @@ +/** + * 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; +} + +export class PinningManager { + private ipfsService: any; + private pinnedItems: Map = new Map(); + private pinningRules: Map = new Map(); + private accessLog: Map = 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): 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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + + // 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 { + 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 { + this.stopAutoCleanup(); + console.log('📌 PinningManager shut down'); + } +} diff --git a/src/framework/pubsub/PubSubManager.ts b/src/framework/pubsub/PubSubManager.ts new file mode 100644 index 0000000..23f08b6 --- /dev/null +++ b/src/framework/pubsub/PubSubManager.ts @@ -0,0 +1,712 @@ +/** + * 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; + 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 = 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 = new Map(); + + constructor(ipfsService: any, config: Partial = {}) { + 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 { + 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 { + 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, + options: { + filter?: (event: PubSubEvent) => boolean; + autoAck?: boolean; + maxRetries?: number; + deadLetterTopic?: string; + } = {}, + ): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + 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 { + 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 { + 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'); + } +} diff --git a/src/framework/query/QueryBuilder.ts b/src/framework/query/QueryBuilder.ts new file mode 100644 index 0000000..ce1464d --- /dev/null +++ b/src/framework/query/QueryBuilder.ts @@ -0,0 +1,637 @@ +import { BaseModel } from '../models/BaseModel'; +import { QueryCondition, SortConfig } from '../types/queries'; +import { QueryExecutor } from './QueryExecutor'; + +export class QueryBuilder { + 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[] = []; + private cursorValue?: string; + private _relationshipConstraints?: Map) => QueryBuilder) | undefined>; + private cacheEnabled: boolean = false; + private cacheTtl?: number; + private cacheKey?: string; + + constructor(model: typeof BaseModel) { + this.model = model; + } + + // Basic filtering + where(field: string, operator: string, value: any): this; + where(field: string, value: any): this; + where(callback: (query: QueryBuilder) => void): this; + where(fieldOrCallback: string | ((query: QueryBuilder) => void), operatorOrValue?: string | any, value?: any): this { + if (typeof fieldOrCallback === 'function') { + // Callback version: where((query) => { ... }) + const subQuery = new QueryBuilder(this.model); + fieldOrCallback(subQuery); + + this.conditions.push({ + field: '__group__', + operator: 'group', + value: null, + type: 'group', + conditions: subQuery.getWhereConditions() + }); + return this; + } + + // Validate field name + this.validateFieldName(fieldOrCallback); + + if (value !== undefined) { + // Three parameter version: where('field', 'operator', 'value') + const normalizedOperator = this.normalizeOperator(operatorOrValue); + this.conditions.push({ field: fieldOrCallback, operator: normalizedOperator, value }); + } else { + // Two parameter version: where('field', 'value') - defaults to equality + // Special handling for null checks + if (typeof operatorOrValue === 'string') { + const lowerValue = operatorOrValue.toLowerCase(); + if (lowerValue === 'is null' || lowerValue === 'is not null') { + this.conditions.push({ field: fieldOrCallback, operator: lowerValue, value: null }); + return this; + } + } + this.conditions.push({ field: fieldOrCallback, operator: 'eq', value: operatorOrValue }); + } + return this; + } + + private validateFieldName(fieldName: string): void { + // Get model fields if available + const modelFields = (this.model as any).fields; + if (modelFields && modelFields instanceof Map) { + const validFields = Array.from(modelFields.keys()); + // Also include common fields that are always valid + validFields.push('id', 'createdAt', 'updatedAt', 'status', 'random', 'lastLoginAt'); + + if (!validFields.includes(fieldName)) { + throw new Error(`Invalid field name: ${fieldName}. Valid fields are: ${validFields.join(', ')}`); + } + } + // If no model fields available, skip validation (for dynamic queries) + } + + private normalizeOperator(operator: string): string { + const operatorMap: { [key: string]: string } = { + '=': 'eq', + '!=': 'ne', + '<>': 'ne', + '>': 'gt', + '>=': 'gte', + '<': 'lt', + '<=': 'lte', + 'like': 'like', + 'ilike': 'ilike', + 'in': 'in', + 'not in': 'not in', + 'is null': 'is null', + 'is not null': 'is not null', + 'regex': 'regex', + 'between': 'between' + }; + + const normalizedOp = operatorMap[operator.toLowerCase()]; + if (!normalizedOp && !this.isValidOperator(operator)) { + throw new Error(`Invalid operator: ${operator}. Valid operators are: ${Object.keys(operatorMap).join(', ')}`); + } + + return normalizedOp || operator; + } + + private isValidOperator(operator: string): boolean { + const validOperators = [ + 'eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'like', 'ilike', + 'in', 'not in', 'is null', 'is not null', 'regex', 'between', + 'array_contains', 'object_has_key', 'includes', 'includes any', 'includes all' + ]; + return validOperators.includes(operator.toLowerCase()); + } + + 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 { + this.conditions.push({ field, operator: 'is null', value: null }); + return this; + } + + whereNotNull(field: string): this { + this.conditions.push({ field, operator: 'is not null', value: null }); + return this; + } + + 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(field: string, operator: string, value: any): this; + orWhere(field: string, value: any): this; + orWhere(callback: (query: QueryBuilder) => void): this; + orWhere(fieldOrCallback: string | ((query: QueryBuilder) => void), operatorOrValue?: string | any, value?: any): this { + if (typeof fieldOrCallback === 'function') { + // Callback version: orWhere((query) => { ... }) + const subQuery = new QueryBuilder(this.model); + fieldOrCallback(subQuery); + + this.conditions.push({ + field: '__or__', + operator: 'or', + value: subQuery.getWhereConditions(), + }); + } else { + // Simple orWhere version: orWhere('field', 'operator', 'value') or orWhere('field', 'value') + let finalOperator = 'eq'; + let finalValue = operatorOrValue; + + if (value !== undefined) { + finalOperator = this.normalizeOperator(operatorOrValue); + finalValue = value; + } else { + // Two parameter version: special handling for null checks + if (typeof operatorOrValue === 'string') { + const lowerValue = operatorOrValue.toLowerCase(); + if (lowerValue === 'is null' || lowerValue === 'is not null') { + finalOperator = lowerValue; + finalValue = null; + } + } + } + + this.conditions.push({ + field: fieldOrCallback, + operator: finalOperator, + value: finalValue, + logical: 'or' + }); + } + + 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 { + // Validate direction + if (direction !== 'asc' && direction !== 'desc') { + throw new Error(`Invalid order direction: ${direction}. Valid directions are: asc, desc`); + } + + // Validate field name + this.validateFieldName(field); + + 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 { + if (count < 0) { + throw new Error(`Limit must be non-negative, got: ${count}`); + } + this.limitation = count; + return this; + } + + offset(count: number): this { + if (count < 0) { + throw new Error(`Offset must be non-negative, got: ${count}`); + } + 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[], constraints?: (query: QueryBuilder) => QueryBuilder): this { + relationships.forEach(relation => { + if (!this._relationshipConstraints) { + this._relationshipConstraints = new Map(); + } + this._relationshipConstraints.set(relation, constraints); + this.relations.push(relation); + }); + return this; + } + + loadNested(relationship: string, _callback: (query: QueryBuilder) => 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 { + const executor = new QueryExecutor(this.model, this); + return await executor.execute(); + } + + async get(): Promise { + return await this.exec(); + } + + async all(): Promise { + return await this.exec(); + } + + async findOne(): Promise { + const results = await this.limit(1).exec(); + return results[0] || null; + } + + async first(): Promise { + const results = await this.limit(1).exec(); + return results[0] || null; + } + + async firstOrFail(): Promise { + 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 { + return await this.where('id', '=', id).first(); + } + + async findOrFail(id: string): Promise { + const result = await this.find(id); + if (!result) { + throw new Error(`${this.model.name} with id ${id} not found`); + } + return result; + } + + async count(): Promise { + const executor = new QueryExecutor(this.model, this); + return await executor.count(); + } + + async exists(): Promise { + const count = await this.count(); + return count > 0; + } + + async sum(field: string): Promise { + const executor = new QueryExecutor(this.model, this); + return await executor.sum(field); + } + + async avg(field: string): Promise { + const executor = new QueryExecutor(this.model, this); + return await executor.avg(field); + } + + async min(field: string): Promise { + const executor = new QueryExecutor(this.model, this); + return await executor.min(field); + } + + async max(field: string): Promise { + const executor = new QueryExecutor(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, + ): Promise { + 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) + getModel(): typeof BaseModel { + return this.model; + } + + 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]; + } + + // Getter methods for testing + getWhereConditions(): QueryCondition[] { + return [...this.conditions]; + } + + getOrderBy(): SortConfig[] { + return [...this.sorting]; + } + + getRelationships(): any[] { + return this.relations.map(relation => ({ + relation, + constraints: this._relationshipConstraints?.get(relation) + })); + } + + getCacheOptions(): any { + return { + enabled: this.cacheEnabled, + ttl: this.cacheTtl, + key: this.cacheKey + }; + } + + getCursor(): string | undefined { + return this.cursorValue; + } + + reset(): this { + this.conditions = []; + this.relations = []; + this.sorting = []; + this.limitation = undefined; + this.offsetValue = undefined; + this.groupByFields = []; + this.havingConditions = []; + this.distinctFields = []; + this.cursorValue = undefined; + this.cacheEnabled = false; + this.cacheTtl = undefined; + this.cacheKey = undefined; + return this; + } + + // Cursor-based pagination + after(cursor: string): this { + this.cursorValue = cursor; + return this; + } + + // Aggregation methods + async average(field: string): Promise { + const executor = new QueryExecutor(this.model, this); + return await executor.avg(field); + } + + // Caching methods + cache(ttl: number, key?: string): this { + this.cacheEnabled = true; + this.cacheTtl = ttl; + this.cacheKey = key; + return this; + } + + noCache(): this { + this.cacheEnabled = false; + this.cacheTtl = undefined; + this.cacheKey = undefined; + return this; + } + + // Cloning + clone(): QueryBuilder { + const cloned = new QueryBuilder(this.model); + cloned.conditions = [...this.conditions]; + cloned.sorting = [...this.sorting]; + cloned.groupByFields = [...this.groupByFields]; + cloned.havingConditions = [...this.havingConditions]; + cloned.relations = [...this.relations]; + cloned.distinctFields = [...this.distinctFields]; + cloned.limitation = this.limitation; + cloned.offsetValue = this.offsetValue; + cloned.cursorValue = this.cursorValue; + cloned.cacheEnabled = this.cacheEnabled; + cloned.cacheTtl = this.cacheTtl; + cloned.cacheKey = this.cacheKey; + if (this._relationshipConstraints) { + cloned._relationshipConstraints = new Map(this._relationshipConstraints); + } + return cloned; + } +} diff --git a/src/framework/query/QueryCache.ts b/src/framework/query/QueryCache.ts new file mode 100644 index 0000000..b7ed630 --- /dev/null +++ b/src/framework/query/QueryCache.ts @@ -0,0 +1,315 @@ +import { QueryBuilder } from './QueryBuilder'; +import { BaseModel } from '../models/BaseModel'; + +export interface CacheEntry { + 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> = 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(query: QueryBuilder): 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(query: QueryBuilder): Promise { + 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(query: QueryBuilder, 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 = { + 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(query: QueryBuilder): 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(queries: QueryBuilder[]): Promise { + 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, 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); + } +} diff --git a/src/framework/query/QueryExecutor.ts b/src/framework/query/QueryExecutor.ts new file mode 100644 index 0000000..4a361cb --- /dev/null +++ b/src/framework/query/QueryExecutor.ts @@ -0,0 +1,631 @@ +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 { + private model: typeof BaseModel; + private query: QueryBuilder; + private framework: any; // Will be properly typed later + private queryPlan?: QueryPlan; + private useCache: boolean = true; + + constructor(model: typeof BaseModel, query: QueryBuilder) { + this.model = model; + this.query = query; + this.framework = this.getFrameworkInstance(); + } + + async execute(): Promise { + 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 { + const results = await this.execute(); + return results.length; + } + + async sum(field: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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.storeType); + } 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 { + 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 { + // 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 { + // 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.storeType); + } + } + + private async executeShardedQuery(): Promise { + 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.storeType); + } 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.storeType); + }); + + 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.storeType), + ); + 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 { + // 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 { + console.log(`📄 Fetching ${indexResults.length} documents from user databases`); + + const results: T[] = []; + + // Group by userId for efficient database access + const userGroups = new Map(); + + 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.storeType, 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 { + 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 '==': + case 'eq': + 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: { + model: this.model.name, + conditions: this.query.getConditions(), + orderBy: this.query.getOrderBy(), + limit: this.query.getLimit(), + offset: this.query.getOffset() + }, + plan, + suggestions, + estimatedResultSize: QueryOptimizer.estimateResultSize(this.query), + }; + } + + private getFrameworkInstance(): any { + const framework = (globalThis as any).__debrosFramework; + if (!framework) { + // Try to get mock framework from BaseModel for testing + const mockFramework = (this.model as any).getMockFramework?.(); + if (!mockFramework) { + throw new Error('Framework not initialized. Call framework.initialize() first.'); + } + return mockFramework; + } + return framework; + } +} diff --git a/src/framework/query/QueryOptimizer.ts b/src/framework/query/QueryOptimizer.ts new file mode 100644 index 0000000..a63d155 --- /dev/null +++ b/src/framework/query/QueryOptimizer.ts @@ -0,0 +1,254 @@ +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(query: QueryBuilder): 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): 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(query: QueryBuilder): 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; + } +} diff --git a/src/framework/relationships/LazyLoader.ts b/src/framework/relationships/LazyLoader.ts new file mode 100644 index 0000000..e12767e --- /dev/null +++ b/src/framework/relationships/LazyLoader.ts @@ -0,0 +1,441 @@ +import { BaseModel } from '../models/BaseModel'; +import { RelationshipConfig } from '../types/models'; +import { RelationshipManager, RelationshipLoadOptions } from './RelationshipManager'; + +export interface LazyLoadPromise extends Promise { + isLoaded(): boolean; + getLoadedValue(): T | undefined; + reload(options?: RelationshipLoadOptions): Promise; +} + +export class LazyLoader { + private relationshipManager: RelationshipManager; + + constructor(relationshipManager: RelationshipManager) { + this.relationshipManager = relationshipManager; + } + + createLazyProperty( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): LazyLoadPromise { + let loadPromise: Promise | null = null; + let loadedValue: T | undefined = undefined; + let isLoaded = false; + + const loadRelationship = async (): Promise => { + 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 => { + // 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; + + // Add custom methods + promise.isLoaded = () => isLoaded; + promise.getLoadedValue = () => loadedValue; + promise.reload = reload; + + return promise; + } + + createLazyPropertyWithProxy( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): T { + const lazyPromise = this.createLazyProperty(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 { + 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 { + 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> = []; + + 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 { + const loaded: Record = {}; + + 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 { + await relationshipManager.eagerLoadRelationships(instances, relationships); + } + + // Helper method to create lazy collection with advanced features + createLazyCollection( + instance: BaseModel, + relationshipName: string, + config: RelationshipConfig, + options: RelationshipLoadOptions = {}, + ): LazyCollection { + return new LazyCollection( + instance, + relationshipName, + config, + options, + this.relationshipManager, + ); + } +} + +// Advanced lazy collection with pagination and filtering +export class LazyCollection { + 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 { + 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 { + return this.loadPage(this.currentPage, count); + } + + async loadAll(): Promise { + 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 { + if (!this.isFullyLoaded) { + await this.loadAll(); + } + return this.loadedItems.filter(predicate); + } + + async find(predicate: (item: T) => boolean): Promise { + // 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 { + 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; + } +} diff --git a/src/framework/relationships/RelationshipCache.ts b/src/framework/relationships/RelationshipCache.ts new file mode 100644 index 0000000..68a3509 --- /dev/null +++ b/src/framework/relationships/RelationshipCache.ts @@ -0,0 +1,359 @@ +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 = 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) { + try { + const extraStr = JSON.stringify(extraData); + if (extraStr) { + return `${baseKey}:${this.hashString(extraStr)}`; + } + } catch (_e) { + // If JSON.stringify fails (e.g., for functions), use a fallback + const fallbackStr = String(extraData) || 'undefined'; + return `${baseKey}:${this.hashString(fallbackStr)}`; + } + } + + 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, + ): Promise { + console.log(`🔥 Warming relationship cache for ${instances.length} instances...`); + + const promises: Promise[] = []; + + 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; + } { + const now = Date.now(); + let totalAge = 0; + let oldestAge = 0; + let newestAge = Infinity; + const relationshipTypes = new Map(); + + 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 { + if (!str || typeof str !== 'string') { + return 'empty'; + } + + 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); + } +} diff --git a/src/framework/relationships/RelationshipManager.ts b/src/framework/relationships/RelationshipManager.ts new file mode 100644 index 0000000..ff74a48 --- /dev/null +++ b/src/framework/relationships/RelationshipManager.ts @@ -0,0 +1,606 @@ +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) => QueryBuilder; + 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 { + 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 { + const foreignKeyValue = (instance as any)[config.foreignKey]; + + if (!foreignKeyValue) { + return null; + } + + // Get the related model class + const RelatedModel = config.model || (config.modelFactory ? config.modelFactory() : null) || (config.targetModel ? config.targetModel() : null); + if (!RelatedModel) { + throw new Error(`Cannot resolve related model for belongsTo relationship`); + } + + // Build query for the related model + let query = (RelatedModel 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 { + if (config.through) { + return await this.loadManyToMany(instance, config, options); + } + + const localKeyValue = (instance as any)[config.localKey || 'id']; + + if (!localKeyValue) { + return []; + } + + // Get the related model class + const RelatedModel = config.model || (config.modelFactory ? config.modelFactory() : null) || (config.targetModel ? config.targetModel() : null); + if (!RelatedModel) { + throw new Error(`Cannot resolve related model for hasMany relationship`); + } + + // Build query for the related model + let query = (RelatedModel 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 { + 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 { + 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 + // For many-to-many relationships, we need to query the junction table with the foreign key for this side + const junctionLocalKey = config.otherKey || config.foreignKey; // The key in junction table that points to this model + let junctionQuery = (config.through as any).where(junctionLocalKey, '=', 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 + // Get the related model class + const RelatedModel = config.model || (config.modelFactory ? config.modelFactory() : null) || (config.targetModel ? config.targetModel() : null); + if (!RelatedModel) { + throw new Error(`Cannot resolve related model for manyToMany relationship`); + } + + let relatedQuery = (RelatedModel 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 = {}, + ): Promise { + 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, + relationshipName: string, + options: RelationshipLoadOptions, + ): Promise { + 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 { + // 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 + const RelatedModel = config.model || (config.modelFactory ? config.modelFactory() : null) || (config.targetModel ? config.targetModel() : null); + if (!RelatedModel) { + throw new Error(`Could not resolve related model for ${relationshipName}`); + } + let query = (RelatedModel 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 { + 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; + } + + // Get the related model class + const RelatedModel = config.model || (config.modelFactory ? config.modelFactory() : null) || (config.targetModel ? config.targetModel() : null); + if (!RelatedModel) { + throw new Error(`Cannot resolve related model for hasMany eager loading`); + } + + // Load all related models + let query = (RelatedModel 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(); + 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 { + // 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 { + 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 junctionLocalKey = config.otherKey || config.foreignKey; // The key in junction table that points to this model + const junctionRecords = await (config.through as any) + .whereIn(junctionLocalKey, 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(); + junctionRecords.forEach((record: any) => { + const localKeyValue = (record as any)[junctionLocalKey]; + 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 + // Get the related model class + const RelatedModel = config.model || (config.modelFactory ? config.modelFactory() : null) || (config.targetModel ? config.targetModel() : null); + if (!RelatedModel) { + throw new Error(`Cannot resolve related model for manyToMany eager loading`); + } + + let relatedQuery = (RelatedModel 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 { + const groups = new Map(); + + 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 { + 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(); + } +} diff --git a/src/framework/services/IPFSService.ts b/src/framework/services/IPFSService.ts new file mode 100644 index 0000000..01a5e56 --- /dev/null +++ b/src/framework/services/IPFSService.ts @@ -0,0 +1,137 @@ +import { createHelia } from 'helia'; +import { createLibp2p } from 'libp2p'; +import { tcp } from '@libp2p/tcp'; +import { noise } from '@chainsafe/libp2p-noise'; +import { yamux } from '@chainsafe/libp2p-yamux'; +import { bootstrap } from '@libp2p/bootstrap'; +import { mdns } from '@libp2p/mdns'; +import { identify } from '@libp2p/identify'; +import { gossipsub } from '@chainsafe/libp2p-gossipsub'; +import fs from 'fs'; +import path from 'path'; + +export interface IPFSConfig { + swarmKeyFile?: string; + bootstrap?: string[]; + ports?: { + swarm?: number; + api?: number; + gateway?: number; + }; +} + +export class IPFSService { + private helia: any; + private libp2p: any; + private config: IPFSConfig; + + constructor(config: IPFSConfig = {}) { + this.config = config; + } + + async init(): Promise { + // Create libp2p instance + const libp2pConfig: any = { + addresses: { + listen: [`/ip4/0.0.0.0/tcp/${this.config.ports?.swarm || 4001}`] + }, + transports: [tcp()], + connectionEncryption: [noise()], + streamMuxers: [yamux()], + services: { + identify: identify(), + pubsub: gossipsub({ + allowPublishToZeroTopicPeers: true + }) + } + }; + + // Add peer discovery + const peerDiscovery = []; + + // Add bootstrap peers if provided + if (this.config.bootstrap && this.config.bootstrap.length > 0) { + peerDiscovery.push(bootstrap({ + list: this.config.bootstrap + })); + } + + // Add mDNS for local discovery + peerDiscovery.push(mdns({ + interval: 1000 + })); + + if (peerDiscovery.length > 0) { + libp2pConfig.peerDiscovery = peerDiscovery; + } + + this.libp2p = await createLibp2p(libp2pConfig); + + // Create Helia instance + this.helia = await createHelia({ + libp2p: this.libp2p + }); + + console.log(`IPFS Service initialized with peer ID: ${this.libp2p.peerId}`); + } + + async stop(): Promise { + if (this.helia) { + await this.helia.stop(); + } + } + + getHelia(): any { + return this.helia; + } + + getLibp2pInstance(): any { + return this.libp2p; + } + + async getConnectedPeers(): Promise> { + if (!this.libp2p) { + return new Map(); + } + + const peers = this.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 { + if (this.helia && this.helia.pins) { + await this.helia.pins.add(cid); + console.log(`Pinned ${cid} on node ${nodeId}`); + } + } + + get pubsub() { + if (!this.libp2p || !this.libp2p.services.pubsub) { + return undefined; + } + + return { + publish: async (topic: string, data: string) => { + const encoder = new TextEncoder(); + await this.libp2p.services.pubsub.publish(topic, encoder.encode(data)); + }, + subscribe: async (topic: string, handler: (message: any) => void) => { + this.libp2p.services.pubsub.subscribe(topic); + this.libp2p.services.pubsub.addEventListener('message', (event: any) => { + if (event.detail.topic === topic) { + handler(event.detail); + } + }); + }, + unsubscribe: async (topic: string) => { + this.libp2p.services.pubsub.unsubscribe(topic); + } + }; + } +} \ No newline at end of file diff --git a/src/framework/services/OrbitDBService.ts b/src/framework/services/OrbitDBService.ts new file mode 100644 index 0000000..4f539a5 --- /dev/null +++ b/src/framework/services/OrbitDBService.ts @@ -0,0 +1,122 @@ +import { StoreType } from '../types/framework'; + +export interface OrbitDBInstance { + openDB(name: string, type: string): Promise; + getOrbitDB(): any; + init(): Promise; + stop?(): Promise; +} + +export interface IPFSInstance { + init(): Promise; + getHelia(): any; + getLibp2pInstance(): any; + stop?(): Promise; + pubsub?: { + publish(topic: string, data: string): Promise; + subscribe(topic: string, handler: (message: any) => void): Promise; + unsubscribe(topic: string): Promise; + }; +} + +export class FrameworkOrbitDBService { + private orbitDBService: OrbitDBInstance; + private initialized: boolean = false; + + constructor(orbitDBService: OrbitDBInstance) { + this.orbitDBService = orbitDBService; + // Check if the service is already initialized by trying to get OrbitDB + try { + if (orbitDBService.getOrbitDB && orbitDBService.getOrbitDB()) { + this.initialized = true; + } + } catch (error) { + // Service not initialized yet + } + } + + async openDatabase(name: string, type: StoreType): Promise { + return await this.orbitDBService.openDB(name, type); + } + + async init(): Promise { + if (!this.initialized) { + await this.orbitDBService.init(); + this.initialized = true; + } + } + + async stop(): Promise { + if (this.orbitDBService.stop) { + await this.orbitDBService.stop(); + } + } + + getOrbitDB(): any { + return this.orbitDBService.getOrbitDB(); + } +} + +export class FrameworkIPFSService { + private ipfsService: IPFSInstance; + private initialized: boolean = false; + + constructor(ipfsService: IPFSInstance) { + this.ipfsService = ipfsService; + // Check if the service is already initialized by trying to get Helia + try { + if (ipfsService.getHelia && ipfsService.getHelia()) { + this.initialized = true; + } + } catch (error) { + // Service not initialized yet + } + } + + async init(): Promise { + if (!this.initialized) { + await this.ipfsService.init(); + this.initialized = true; + } + } + + async stop(): Promise { + if (this.ipfsService.stop) { + await this.ipfsService.stop(); + } + } + + getHelia(): any { + return this.ipfsService.getHelia(); + } + + getLibp2p(): any { + return this.ipfsService.getLibp2pInstance(); + } + + async getConnectedPeers(): Promise> { + 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 { + // 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; + } +} diff --git a/src/framework/services/RealOrbitDBService.ts b/src/framework/services/RealOrbitDBService.ts new file mode 100644 index 0000000..b605b84 --- /dev/null +++ b/src/framework/services/RealOrbitDBService.ts @@ -0,0 +1,57 @@ +import { createOrbitDB } from '@orbitdb/core'; + +export class OrbitDBService { + private orbitdb: any; + private ipfsService: any; + + constructor(ipfsService: any) { + this.ipfsService = ipfsService; + } + + async init(): Promise { + if (!this.ipfsService) { + throw new Error('IPFS service is required for OrbitDB'); + } + + this.orbitdb = await createOrbitDB({ + ipfs: this.ipfsService.getHelia(), + directory: './orbitdb' + }); + + console.log('OrbitDB Service initialized'); + } + + async stop(): Promise { + if (this.orbitdb) { + await this.orbitdb.stop(); + } + } + + async openDB(name: string, type: string): Promise { + if (!this.orbitdb) { + throw new Error('OrbitDB not initialized'); + } + + // Map framework types to OrbitDB v2 types + const orbitDBType = this.mapFrameworkTypeToOrbitDB(type); + + return await this.orbitdb.open(name, { + type: orbitDBType, + AccessController: this.orbitdb.AccessController + }); + } + + private mapFrameworkTypeToOrbitDB(frameworkType: string): string { + const typeMapping: { [key: string]: string } = { + 'docstore': 'documents', + 'keyvalue': 'keyvalue', + 'eventlog': 'eventlog' + }; + + return typeMapping[frameworkType] || frameworkType; + } + + getOrbitDB(): any { + return this.orbitdb; + } +} \ No newline at end of file diff --git a/src/framework/sharding/ShardManager.ts b/src/framework/sharding/ShardManager.ts new file mode 100644 index 0000000..6d3e1cc --- /dev/null +++ b/src/framework/sharding/ShardManager.ts @@ -0,0 +1,301 @@ +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 = new Map(); + private shardConfigs: Map = new Map(); + + setOrbitDBService(service: FrameworkOrbitDBService): void { + this.orbitDBService = service; + } + + async createShards( + modelName: string, + config: ShardingConfig, + dbType: StoreType = 'docstore', + ): Promise { + 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 + + const shardIndex = Math.floor(range * shardCount); + // Ensure the index is within bounds (handle edge case where range = 1.0) + return Math.min(shardIndex, shardCount - 1); + } + + 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 { + 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 { + 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 { + 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 { + 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 { + 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, + ): Promise { + 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 { + console.log('🛑 Stopping ShardManager...'); + + this.shards.clear(); + this.shardConfigs.clear(); + + console.log('✅ ShardManager stopped'); + } +} diff --git a/src/framework/types/framework.ts b/src/framework/types/framework.ts new file mode 100644 index 0000000..52b5707 --- /dev/null +++ b/src/framework/types/framework.ts @@ -0,0 +1,54 @@ +export type StoreType = 'eventlog' | 'keyvalue' | 'docstore' | 'counter' | 'feed'; + +export interface FrameworkConfig { + cache?: CacheConfig; + defaultPinning?: PinningConfig; + autoMigration?: boolean; +} + +export interface CacheConfig { + enabled?: boolean; + maxSize?: number; + ttl?: number; +} + +export type PinningStrategy = 'fixed' | 'popularity' | 'size' | 'age' | 'custom'; + +export interface PinningConfig { + strategy?: PinningStrategy; + factor?: number; + maxPins?: number; + minAccessCount?: number; + maxAge?: number; +} + +export interface PinningStats { + totalPinned: number; + totalSize: number; + averageSize: number; + strategies: Record; + oldestPin: number; + recentActivity: Array<{ action: string; hash: string; timestamp: number }>; +} + +export interface PubSubConfig { + enabled?: boolean; + events?: string[]; + channels?: string[]; +} + +export interface ShardingConfig { + strategy: 'hash' | 'range' | 'user'; + count: number; + key: string; +} + +export interface ValidationResult { + valid: boolean; + errors: string[]; +} + +export interface ValidationError { + field: string; + message: string; +} diff --git a/src/framework/types/models.ts b/src/framework/types/models.ts new file mode 100644 index 0000000..5c5154e --- /dev/null +++ b/src/framework/types/models.ts @@ -0,0 +1,50 @@ +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; + modelFactory?: () => typeof BaseModel; + targetModel?: () => typeof BaseModel; // Alias for test compatibility + foreignKey: string; + localKey?: string; + otherKey?: string; + through?: typeof BaseModel | string; + lazy?: boolean; + propertyKey?: string; + options?: any; +} + +export interface UserMappings { + userId: string; + databases: Record; +} + +export class ValidationError extends Error { + public errors: string[]; + + constructor(errors: string[]) { + super(`Validation failed: ${errors.join(', ')}`); + this.errors = errors; + this.name = 'ValidationError'; + } +} diff --git a/src/framework/types/queries.ts b/src/framework/types/queries.ts new file mode 100644 index 0000000..8bae62d --- /dev/null +++ b/src/framework/types/queries.ts @@ -0,0 +1,19 @@ +export interface QueryCondition { + field: string; + operator: string; + value: any; + logical?: 'and' | 'or'; + type?: 'condition' | 'group'; + conditions?: QueryCondition[]; +} + +export interface SortConfig { + field: string; + direction: 'asc' | 'desc'; +} + +export interface QueryOptions { + limit?: number; + offset?: number; + relations?: string[]; +} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index f61e395..0000000 --- a/src/index.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Config exports -import { config, defaultConfig, type DebrosConfig } from './config'; -import { validateConfig, type ValidationResult } from './ipfs/config/configValidator'; - -// Database service exports (new abstracted layer) -import { - init as initDB, - create, - get, - update, - remove, - list, - query, - createIndex, - createTransaction, - commitTransaction, - subscribe, - uploadFile, - getFile, - deleteFile, - defineSchema, - closeConnection, - stop as stopDB, -} from './db/dbService'; - -import { ErrorCode, StoreType } from './db/types'; - -// Import types -import type { - Transaction, - CreateResult, - UpdateResult, - PaginatedResult, - ListOptions, - QueryOptions, - FileUploadResult, - FileResult, - CollectionSchema, - SchemaDefinition, - Metrics, -} from './db/types'; - -import { DBError } from './db/core/error'; - -// Legacy exports (internal use only, not exposed in default export) -import { getConnectedPeers, logPeersStatus } from './ipfs/ipfsService'; - -// Load balancer exports -import loadBalancerController from './ipfs/loadBalancerController'; - -// Logger exports -import logger, { - createServiceLogger, - createDebrosLogger, - type LoggerOptions, -} from './utils/logger'; - -// Export public API -export { - // Configuration - config, - defaultConfig, - validateConfig, - type DebrosConfig, - type ValidationResult, - - // Database Service (Main public API) - initDB, - create, - get, - update, - remove, - list, - query, - createIndex, - createTransaction, - commitTransaction, - subscribe, - uploadFile, - getFile, - deleteFile, - defineSchema, - closeConnection, - stopDB, - ErrorCode, - StoreType, - - // Load Balancer - loadBalancerController, - getConnectedPeers, - logPeersStatus, - - // Types - type Transaction, - type DBError, - type CollectionSchema, - type SchemaDefinition, - type CreateResult, - type UpdateResult, - type PaginatedResult, - type ListOptions, - type QueryOptions, - type FileUploadResult, - type FileResult, - type Metrics, - - // Logger - logger, - createServiceLogger, - createDebrosLogger, - type LoggerOptions, -}; - -// Default export for convenience -export default { - config, - validateConfig, - // Database Service as main interface - db: { - init: initDB, - create, - get, - update, - remove, - list, - query, - createIndex, - createTransaction, - commitTransaction, - subscribe, - uploadFile, - getFile, - deleteFile, - defineSchema, - closeConnection, - stop: stopDB, - ErrorCode, - StoreType, - }, - loadBalancerController, - logPeersStatus, - getConnectedPeers, - logger, - createServiceLogger, -}; diff --git a/src/ipfs/config/configValidator.ts b/src/ipfs/config/configValidator.ts deleted file mode 100644 index 0546608..0000000 --- a/src/ipfs/config/configValidator.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { config } from '../../config'; - -export interface ValidationResult { - valid: boolean; - errors: string[]; -} - -/** - * Validates the IPFS configuration - */ -export const validateConfig = (): ValidationResult => { - const errors: string[] = []; - - // Check fingerprint - if (!config.env.fingerprint || config.env.fingerprint === 'default-fingerprint') { - errors.push('Fingerprint not set or using default value. Please set a unique fingerprint.'); - } - - // Check port - const port = Number(config.env.port); - if (isNaN(port) || port < 1 || port > 65535) { - errors.push('Invalid port configuration. Port must be a number between 1 and 65535.'); - } - - // Check service discovery topic - if (!config.ipfs.serviceDiscovery.topic) { - errors.push('Service discovery topic not configured.'); - } - - // Check blockstore path - if (!config.ipfs.blockstorePath) { - errors.push('Blockstore path not configured.'); - } - - // Check orbitdb directory - if (!config.orbitdb.directory) { - errors.push('OrbitDB directory not configured.'); - } - - return { - valid: errors.length === 0, - errors, - }; -}; diff --git a/src/ipfs/config/ipfsConfig.ts b/src/ipfs/config/ipfsConfig.ts deleted file mode 100644 index 7e91a7e..0000000 --- a/src/ipfs/config/ipfsConfig.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { config } from '../../config'; - -// Determine the IPFS port to use -export const getIpfsPort = (): number => { - if (process.env.IPFS_PORT) { - return parseInt(process.env.IPFS_PORT); - } - const httpPort = parseInt(process.env.PORT || '7777'); - // Add some randomness to avoid port conflicts during retries - const basePort = httpPort + 1; - const randomOffset = Math.floor(Math.random() * 10); - return basePort + randomOffset; // Add random offset to avoid conflicts -}; - -// Get a node-specific blockstore path -export const getBlockstorePath = (): string => { - const basePath = config.ipfs.blockstorePath; - const fingerprint = config.env.fingerprint; - return `${basePath}-${fingerprint}`; -}; - -// IPFS configuration -export const ipfsConfig = { - blockstorePath: getBlockstorePath(), - port: getIpfsPort(), - serviceDiscovery: { - topic: config.ipfs.serviceDiscovery.topic, - heartbeatInterval: config.ipfs.serviceDiscovery.heartbeatInterval || 2000, - staleTimeout: config.ipfs.serviceDiscovery.staleTimeout || 30000, - logInterval: config.ipfs.serviceDiscovery.logInterval || 60000, - publicAddress: config.ipfs.serviceDiscovery.publicAddress, - }, - bootstrapNodes: process.env.BOOTSTRAP_NODES, -}; diff --git a/src/ipfs/ipfsService.ts b/src/ipfs/ipfsService.ts deleted file mode 100644 index 1a675de..0000000 --- a/src/ipfs/ipfsService.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { Libp2p } from 'libp2p'; - -import { - initIpfsNode, - stopIpfsNode, - getHeliaInstance, - getLibp2pInstance, - getProxyAgentInstance, -} from './services/ipfsCoreService'; - -import { - getConnectedPeers, - getOptimalPeer, - updateNodeLoad, - logPeersStatus, -} from './services/discoveryService'; -import { createServiceLogger } from '../utils/logger'; - -// Create logger for IPFS service -const logger = createServiceLogger('IPFS'); - -// Interface definition for the IPFS module -export interface IPFSModule { - init: (externalProxyAgent?: any) => Promise; - stop: () => Promise; - getHelia: () => any; - getProxyAgent: () => any; - getInstance: (externalProxyAgent?: any) => Promise<{ - getHelia: () => any; - getProxyAgent: () => any; - }>; - getLibp2p: () => Libp2p; - getConnectedPeers: () => Map; - getOptimalPeer: () => string | null; - updateNodeLoad: (load: number) => void; - logPeersStatus: () => void; -} - -const init = async (externalProxyAgent: any = null) => { - try { - await initIpfsNode(externalProxyAgent); - logger.info('IPFS service initialized successfully'); - return getHeliaInstance(); - } catch (error) { - logger.error('Failed to initialize IPFS service:', error); - throw error; - } -}; - -const stop = async () => { - await stopIpfsNode(); - logger.info('IPFS service stopped'); -}; - -const getHelia = () => { - return getHeliaInstance(); -}; - -const getProxyAgent = () => { - return getProxyAgentInstance(); -}; - -const getLibp2p = () => { - return getLibp2pInstance(); -}; - -const getInstance = async (externalProxyAgent: any = null) => { - if (!getHeliaInstance()) { - await init(externalProxyAgent); - } - - return { - getHelia, - getProxyAgent, - }; -}; - -// Export individual functions -export { - init, - stop, - getHelia, - getProxyAgent, - getInstance, - getLibp2p, - getConnectedPeers, - getOptimalPeer, - updateNodeLoad, - logPeersStatus, -}; - -// Export as default module -export default { - init, - stop, - getHelia, - getProxyAgent, - getInstance, - getLibp2p, - getConnectedPeers, - getOptimalPeer, - updateNodeLoad, - logPeersStatus, -} as IPFSModule; diff --git a/src/ipfs/loadBalancerController.ts b/src/ipfs/loadBalancerController.ts deleted file mode 100644 index 96e9472..0000000 --- a/src/ipfs/loadBalancerController.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Load balancer controller - Handles API routes for service discovery and load balancing -import { Request, Response, NextFunction } from 'express'; -import loadBalancerService from './loadBalancerService'; -import { config } from '../config'; - -export interface LoadBalancerControllerModule { - getNodeInfo: (_req: Request, _res: Response, _next: NextFunction) => void; - getOptimalPeer: (_req: Request, _res: Response, _next: NextFunction) => void; - getAllPeers: (_req: Request, _res: Response, _next: NextFunction) => void; -} - -/** - * Get information about the node and its load - */ -const getNodeInfo = (req: Request, res: Response, next: NextFunction) => { - try { - const status = loadBalancerService.getNodeStatus(); - res.json({ - fingerprint: config.env.fingerprint, - peerCount: status.peerCount, - isLoadBalancer: config.features.enableLoadBalancing, - loadBalancerStrategy: config.loadBalancer.strategy, - maxConnections: config.loadBalancer.maxConnections, - }); - } catch (error) { - next(error); - } -}; - -/** - * Get the optimal peer for client connection - */ -const getOptimalPeer = (req: Request, res: Response, next: NextFunction) => { - try { - // Check if load balancing is enabled - if (!config.features.enableLoadBalancing) { - res.status(200).json({ - useThisNode: true, - message: 'Load balancing is disabled, use this node', - fingerprint: config.env.fingerprint, - publicAddress: config.ipfs.serviceDiscovery.publicAddress, - }); - return; - } - - // Get the optimal peer - const optimalPeer = loadBalancerService.getOptimalPeer(); - - // If there are no peer nodes, use this node - if (!optimalPeer) { - res.status(200).json({ - useThisNode: true, - message: 'No other peers available, use this node', - fingerprint: config.env.fingerprint, - publicAddress: config.ipfs.serviceDiscovery.publicAddress, - }); - return; - } - - // Check if this node is the optimal peer - const isThisNodeOptimal = optimalPeer.peerId === config.env.fingerprint; - - if (isThisNodeOptimal) { - res.status(200).json({ - useThisNode: true, - message: 'This node is optimal', - fingerprint: config.env.fingerprint, - publicAddress: config.ipfs.serviceDiscovery.publicAddress, - }); - return; - } - - // Return the optimal peer information - res.status(200).json({ - useThisNode: false, - optimalPeer: { - peerId: optimalPeer.peerId, - load: optimalPeer.load, - publicAddress: optimalPeer.publicAddress, - }, - message: 'Found optimal peer', - }); - } catch (error) { - next(error); - } -}; - -/** - * Get all available peers - */ -const getAllPeers = (req: Request, res: Response, next: NextFunction) => { - try { - const peers = loadBalancerService.getAllPeers(); - res.status(200).json({ - peerCount: peers.length, - peers, - }); - } catch (error) { - next(error); - } -}; - -export default { - getNodeInfo, - getOptimalPeer, - getAllPeers, -} as LoadBalancerControllerModule; diff --git a/src/ipfs/loadBalancerService.ts b/src/ipfs/loadBalancerService.ts deleted file mode 100644 index a7d763c..0000000 --- a/src/ipfs/loadBalancerService.ts +++ /dev/null @@ -1,112 +0,0 @@ -import * as ipfsService from './ipfsService'; -import { config } from '../config'; -import { createServiceLogger } from '../utils/logger'; - -const logger = createServiceLogger('LOAD_BALANCER'); - -// Track last peer chosen for round-robin strategy -let lastPeerIndex = -1; - -// Type definitions -export interface PeerInfo { - peerId: string; - load: number; - publicAddress: string; -} - -export interface PeerStatus extends PeerInfo { - lastSeen: number; -} - -export interface NodeStatus { - fingerprint: string; - peerCount: number; - isHealthy: boolean; -} - -type LoadBalancerStrategy = 'leastLoaded' | 'roundRobin' | 'random'; - -/** - * Strategies for peer selection - */ -const strategies = { - leastLoaded: (peers: PeerStatus[]): PeerStatus => { - return peers.reduce((min, current) => (current.load < min.load ? current : min), peers[0]); - }, - - roundRobin: (peers: PeerStatus[]): PeerStatus => { - lastPeerIndex = (lastPeerIndex + 1) % peers.length; - return peers[lastPeerIndex]; - }, - - random: (peers: PeerStatus[]): PeerStatus => { - const randomIndex = Math.floor(Math.random() * peers.length); - return peers[randomIndex]; - }, -}; - -/** - * Get the optimal peer based on the configured load balancing strategy - */ -export const getOptimalPeer = (): PeerInfo | null => { - const connectedPeers = ipfsService.getConnectedPeers(); - - if (connectedPeers.size === 0) { - logger.info('No peers available for load balancing'); - return null; - } - - // Convert Map to Array for easier manipulation - const peersArray = Array.from(connectedPeers.entries()).map(([peerId, data]) => ({ - peerId, - load: data.load, - lastSeen: data.lastSeen, - publicAddress: data.publicAddress, - })); - - // Apply the selected load balancing strategy - const strategy = config.loadBalancer.strategy as LoadBalancerStrategy; - let selectedPeer; - - // Select strategy function or default to least loaded - const strategyFn = strategies[strategy] || strategies.leastLoaded; - selectedPeer = strategyFn(peersArray); - - logger.info( - `Selected peer (${strategy}): ${selectedPeer.peerId.substring(0, 15)}... with load ${selectedPeer.load}%`, - ); - - return { - peerId: selectedPeer.peerId, - load: selectedPeer.load, - publicAddress: selectedPeer.publicAddress, - }; -}; - -/** - * Get all available peers with their load information - */ -export const getAllPeers = (): PeerStatus[] => { - const connectedPeers = ipfsService.getConnectedPeers(); - - return Array.from(connectedPeers.entries()).map(([peerId, data]) => ({ - peerId, - load: data.load, - lastSeen: data.lastSeen, - publicAddress: data.publicAddress, - })); -}; - -/** - * Get information about the current node's load - */ -export const getNodeStatus = (): NodeStatus => { - const connectedPeers = ipfsService.getConnectedPeers(); - return { - fingerprint: config.env.fingerprint, - peerCount: connectedPeers.size, - isHealthy: true, - }; -}; - -export default { getOptimalPeer, getAllPeers, getNodeStatus }; diff --git a/src/ipfs/services/discoveryService.ts b/src/ipfs/services/discoveryService.ts deleted file mode 100644 index a32b0b6..0000000 --- a/src/ipfs/services/discoveryService.ts +++ /dev/null @@ -1,162 +0,0 @@ -import type { PubSub } from '@libp2p/interface'; -import { config } from '../../config'; -import { ipfsConfig } from '../config/ipfsConfig'; -import { createServiceLogger } from '../../utils/logger'; - -// Create loggers for service discovery and heartbeat -const discoveryLogger = createServiceLogger('SERVICE-DISCOVERY'); -const heartbeatLogger = createServiceLogger('HEARTBEAT'); - -// Node metadata -const fingerprint = config.env.fingerprint; - -const connectedPeers: Map< - string, - { lastSeen: number; load: number; publicAddress: string; fingerprint: string } -> = new Map(); -const SERVICE_DISCOVERY_TOPIC = ipfsConfig.serviceDiscovery.topic; -const HEARTBEAT_INTERVAL = ipfsConfig.serviceDiscovery.heartbeatInterval; -let heartbeatInterval: NodeJS.Timeout; -let nodeLoad = 0; - -export const setupServiceDiscovery = async (pubsub: PubSub) => { - await pubsub.subscribe(SERVICE_DISCOVERY_TOPIC); - discoveryLogger.info(`Subscribed to topic: ${SERVICE_DISCOVERY_TOPIC}`); - - // Listen for other peers heartbeats - pubsub.addEventListener('message', (event: any) => { - try { - const message = JSON.parse(event.detail.data.toString()); - if (message.type === 'heartbeat' && message.fingerprint !== fingerprint) { - const peerId = event.detail.from.toString(); - const existingPeer = connectedPeers.has(peerId); - - connectedPeers.set(peerId, { - lastSeen: Date.now(), - load: message.load, - publicAddress: message.publicAddress, - fingerprint: message.fingerprint, - }); - - if (!existingPeer) { - discoveryLogger.info( - `New peer discovered: ${peerId} (fingerprint=${message.fingerprint})`, - ); - } - heartbeatLogger.info( - `Received from ${peerId}: load=${message.load}, addr=${message.publicAddress}`, - ); - } - } catch (err) { - discoveryLogger.error(`Error processing message:`, err); - } - }); - - // Send periodic heartbeats with our load information - heartbeatInterval = setInterval(async () => { - try { - nodeLoad = calculateNodeLoad(); - const heartbeatMsg = { - type: 'heartbeat', - fingerprint, - load: nodeLoad, - timestamp: Date.now(), - publicAddress: ipfsConfig.serviceDiscovery.publicAddress, - }; - - await pubsub.publish( - SERVICE_DISCOVERY_TOPIC, - new TextEncoder().encode(JSON.stringify(heartbeatMsg)), - ); - heartbeatLogger.info( - `Sent: fingerprint=${fingerprint}, load=${nodeLoad}, addr=${heartbeatMsg.publicAddress}`, - ); - - const now = Date.now(); - const staleTime = ipfsConfig.serviceDiscovery.staleTimeout; - - for (const [peerId, peerData] of connectedPeers.entries()) { - if (now - peerData.lastSeen > staleTime) { - discoveryLogger.info( - `Peer ${peerId.substring(0, 15)}... is stale, removing from load balancer`, - ); - connectedPeers.delete(peerId); - } - } - - if (Date.now() % 60000 < HEARTBEAT_INTERVAL) { - logPeersStatus(); - } - } catch (err) { - discoveryLogger.error(`Error sending heartbeat:`, err); - } - }, HEARTBEAT_INTERVAL); - - discoveryLogger.info(`Service initialized with fingerprint: ${fingerprint}`); -}; - -/** - * Calculates the current node load - */ -export const calculateNodeLoad = (): number => { - // This is a simple implementation and could be enhanced with - // actual metrics like CPU usage, memory, active connections, etc. - return Math.floor(Math.random() * 100); // Placeholder implementation -}; - -/** - * Logs the status of connected peers - */ -export const logPeersStatus = () => { - const peerCount = connectedPeers.size; - discoveryLogger.info(`Connected peers: ${peerCount}`); - discoveryLogger.info(`Current node load: ${nodeLoad}`); - - if (peerCount > 0) { - discoveryLogger.info('Peer status:'); - connectedPeers.forEach((data, peerId) => { - discoveryLogger.debug( - ` - ${peerId} Load: ${data.load}% Last seen: ${new Date(data.lastSeen).toISOString()}`, - ); - }); - } -}; - -export const getOptimalPeer = (): string | null => { - if (connectedPeers.size === 0) return null; - - let lowestLoad = Number.MAX_SAFE_INTEGER; - let optimalPeer: string | null = null; - - connectedPeers.forEach((data, peerId) => { - if (data.load < lowestLoad) { - lowestLoad = data.load; - optimalPeer = peerId; - } - }); - - return optimalPeer; -}; - -export const updateNodeLoad = (load: number) => { - nodeLoad = load; -}; - -export const getConnectedPeers = () => { - return connectedPeers; -}; - -export const stopDiscoveryService = async (pubsub: PubSub | null) => { - if (heartbeatInterval) { - clearInterval(heartbeatInterval); - } - - if (pubsub) { - try { - await pubsub.unsubscribe(SERVICE_DISCOVERY_TOPIC); - discoveryLogger.info(`Unsubscribed from topic: ${SERVICE_DISCOVERY_TOPIC}`); - } catch (err) { - discoveryLogger.error(`Error unsubscribing from topic:`, err); - } - } -}; diff --git a/src/ipfs/services/ipfsCoreService.ts b/src/ipfs/services/ipfsCoreService.ts deleted file mode 100644 index e046222..0000000 --- a/src/ipfs/services/ipfsCoreService.ts +++ /dev/null @@ -1,259 +0,0 @@ -import fs from 'fs'; -import { createHelia } from 'helia'; -import { FsBlockstore } from 'blockstore-fs'; -import { createLibp2p } from 'libp2p'; -import { gossipsub } from '@chainsafe/libp2p-gossipsub'; -import { tcp } from '@libp2p/tcp'; -import { noise } from '@chainsafe/libp2p-noise'; -import { yamux } from '@chainsafe/libp2p-yamux'; -import { identify } from '@libp2p/identify'; -import { bootstrap } from '@libp2p/bootstrap'; -import type { Libp2p } from 'libp2p'; -import { FaultTolerance, PubSub } from '@libp2p/interface'; - -import { ipfsConfig } from '../config/ipfsConfig'; -import { getPrivateKey } from '../utils/crypto'; -import { setupServiceDiscovery, stopDiscoveryService } from './discoveryService'; -import { createServiceLogger } from '../../utils/logger'; - -const logger = createServiceLogger('IPFS'); -const p2pLogger = createServiceLogger('P2P'); - -let helia: any; -let proxyAgent: any; -let libp2pNode: Libp2p; -let reconnectInterval: NodeJS.Timeout; - -export const initIpfsNode = async (externalProxyAgent: any = null) => { - try { - // If already initialized, return existing instance - if (helia && libp2pNode) { - logger.info('IPFS node already initialized, returning existing instance'); - return helia; - } - - // Clean up any existing instances first - if (helia || libp2pNode) { - logger.info('Cleaning up existing IPFS instances before reinitializing'); - await stopIpfsNode(); - } - - proxyAgent = externalProxyAgent; - - const blockstorePath = ipfsConfig.blockstorePath; - try { - if (!fs.existsSync(blockstorePath)) { - fs.mkdirSync(blockstorePath, { recursive: true, mode: 0o755 }); - logger.info(`Created blockstore directory: ${blockstorePath}`); - } - - // Check write permissions - fs.accessSync(blockstorePath, fs.constants.W_OK); - logger.info(`Verified write permissions for blockstore directory: ${blockstorePath}`); - } catch (permError: any) { - logger.error(`Permission error with blockstore directory: ${blockstorePath}`, permError); - throw new Error(`Cannot access or write to blockstore directory: ${permError.message}`); - } - - const blockstore = new FsBlockstore(blockstorePath); - - const currentNodeIp = process.env.HOSTNAME || ''; - logger.info(`Current node public IP: ${currentNodeIp}`); - - const bootstrapList = getBootstrapList(); - logger.info(`Bootstrap peers: ${JSON.stringify(bootstrapList)}`); - - const bootStrap = bootstrap({ - list: bootstrapList, - }) as unknown as any; - - logger.info(`Configuring bootstrap with peers: ${JSON.stringify(bootstrapList)}`); - - const ipfsPort = ipfsConfig.port; - logger.info(`Using port ${ipfsPort} for IPFS/libp2p`); - - libp2pNode = await createLibp2p({ - transports: [tcp()], - streamMuxers: [yamux()], - connectionEncrypters: [noise()], - services: { - identify: identify(), - pubsub: gossipsub({ - allowPublishToZeroTopicPeers: true, - emitSelf: false, - }), - }, - peerDiscovery: [bootStrap], - addresses: { - listen: [`/ip4/0.0.0.0/tcp/${ipfsPort}`], - }, - transportManager: { - faultTolerance: FaultTolerance.NO_FATAL, - }, - privateKey: await getPrivateKey(), - }); - - p2pLogger.info(`PEER ID: ${libp2pNode.peerId.toString()}`); - logger.info( - `Listening on: ${libp2pNode - .getMultiaddrs() - .map((addr: any) => addr.toString()) - .join(', ')}`, - ); - - helia = await createHelia({ - blockstore, - libp2p: libp2pNode, - }); - - const pubsub = libp2pNode.services.pubsub as PubSub; - await setupServiceDiscovery(pubsub); - - setupPeerEventListeners(libp2pNode); - - connectToSpecificPeers(libp2pNode); - - return helia; - } catch (error) { - logger.error('Failed to initialize node:', error); - throw error; - } -}; - -function getBootstrapList(): string[] { - let bootstrapList: string[] = []; - bootstrapList = process.env.BOOTSTRAP_NODES?.split(',').map((node) => node.trim()) || []; - - return bootstrapList; -} - -function setupPeerEventListeners(node: Libp2p) { - node.addEventListener('peer:discovery', (event) => { - const peerId = event.detail.id.toString(); - logger.info(`Discovered peer: ${peerId}`); - }); - - node.addEventListener('peer:connect', (event) => { - const peerId = event.detail.toString(); - logger.info(`Peer connection succeeded: ${peerId}`); - node.peerStore - .get(event.detail) - .then((peerInfo) => { - const multiaddrs = peerInfo?.addresses.map((addr) => addr.multiaddr.toString()) || [ - 'unknown', - ]; - logger.info(`Peer multiaddrs: ${multiaddrs.join(', ')}`); - }) - .catch((error) => { - logger.error(`Error fetching peer info for ${peerId}: ${error.message}`); - }); - }); - - node.addEventListener('peer:disconnect', (event) => { - const peerId = event.detail.toString(); - logger.info(`Disconnected from peer: ${peerId}`); - }); - - node.addEventListener('peer:reconnect-failure', (event) => { - const peerId = event.detail.toString(); - logger.error(`Peer reconnection failed: ${peerId}`); - node.peerStore - .get(event.detail) - .then((peerInfo) => { - const multiaddrs = peerInfo?.addresses.map((addr) => addr.multiaddr.toString()) || [ - 'unknown', - ]; - logger.error(`Peer multiaddrs: ${multiaddrs.join(', ')}`); - }) - .catch((error) => { - logger.error(`Error fetching peer info for ${peerId}: ${error.message}`); - }); - }); - - node.addEventListener('connection:close', (event) => { - const connection = event.detail; - const peerId = connection.remotePeer.toString(); - const remoteAddr = connection.remoteAddr.toString(); - logger.info(`Connection closed for peer: ${peerId}`); - logger.info(`Remote address: ${remoteAddr}`); - }); -} - -export const stopIpfsNode = async () => { - logger.info('Stopping IPFS node...'); - - if (reconnectInterval) { - clearInterval(reconnectInterval); - reconnectInterval = undefined as any; - } - - if (libp2pNode) { - try { - const pubsub = libp2pNode.services.pubsub as PubSub; - await stopDiscoveryService(pubsub); - - // Stop libp2p - await libp2pNode.stop(); - } catch (error) { - logger.error('Error stopping libp2p node:', error); - } - libp2pNode = undefined as any; - } else { - await stopDiscoveryService(null); - } - - if (helia) { - try { - await helia.stop(); - } catch (error) { - logger.error('Error stopping Helia:', error); - } - helia = null; - } - - logger.info('IPFS node stopped successfully'); -}; - -export const getHeliaInstance = () => { - return helia; -}; - -export const getLibp2pInstance = () => { - return libp2pNode; -}; - -export const getProxyAgentInstance = () => { - return proxyAgent; -}; - -function connectToSpecificPeers(node: Libp2p) { - setTimeout(async () => { - await attemptPeerConnections(node); - - reconnectInterval = setInterval(async () => { - await attemptPeerConnections(node); - }, 120000); - }, 5000); -} - -async function attemptPeerConnections(node: Libp2p) { - logger.info('Current peer connections:'); - const peers = node.getPeers(); - if (peers.length === 0) { - logger.info(' - No connected peers'); - } else { - for (const peerId of peers) { - try { - // Get peer info including addresses - const peerInfo = await node.peerStore.get(peerId); - const addresses = - peerInfo?.addresses.map((addr) => addr.multiaddr.toString()).join(', ') || 'unknown'; - logger.info(` - Connected to peer: ${peerId.toString()}`); - logger.info(` Addresses: ${addresses}`); - } catch (_error) { - // Fallback to just showing the peer ID if we can't get address info - logger.info(` - Connected to peer: ${peerId.toString()}`); - } - } - } -} diff --git a/src/ipfs/utils/crypto.ts b/src/ipfs/utils/crypto.ts deleted file mode 100644 index d7b7007..0000000 --- a/src/ipfs/utils/crypto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { generateKeyPairFromSeed } from '@libp2p/crypto/keys'; -import forge from 'node-forge'; -import { config } from '../../config'; -import { createServiceLogger } from '../../utils/logger'; - -const logger = createServiceLogger('CRYPTO'); - -/** - * Generates a deterministic private key based on the node's fingerprint - */ -export const getPrivateKey = async () => { - try { - const userInput = config.env.fingerprint; - - // Use SHA-256 to create a deterministic seed - const md = forge.md.sha256.create(); - md.update(userInput); - const seedString = md.digest().getBytes(); // Get raw bytes as a string - - // Convert the seed string to Uint8Array - const seed = Uint8Array.from(forge.util.binary.raw.decode(seedString)); - - // Generate an Ed25519 private key (libp2p-compatible) - const privateKey = await generateKeyPairFromSeed('Ed25519', seed); - return privateKey; - } catch (error) { - logger.error('Error generating private key:', error); - throw error; - } -}; diff --git a/src/orbit/orbitDBService.ts b/src/orbit/orbitDBService.ts deleted file mode 100644 index b2bce98..0000000 --- a/src/orbit/orbitDBService.ts +++ /dev/null @@ -1,159 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { createOrbitDB, IPFSAccessController } from '@orbitdb/core'; -import { registerFeed } from '@orbitdb/feed-db'; -import { config } from '../config'; -import { createServiceLogger } from '../utils/logger'; -import { getHelia } from '../ipfs/ipfsService'; - -const logger = createServiceLogger('ORBITDB'); - -let orbitdb: any; - -// Create a node-specific directory based on fingerprint to avoid lock conflicts -export const getOrbitDBDir = (): string => { - const baseDir = config.orbitdb.directory; - const fingerprint = config.env.fingerprint; - // Use path.join for proper cross-platform path handling - return path.join(baseDir, `debros-${fingerprint}`); -}; - -const ORBITDB_DIR = getOrbitDBDir(); -const ADDRESS_DIR = path.join(ORBITDB_DIR, 'addresses'); - -export const getDBAddress = (name: string): string | null => { - try { - const addressFile = path.join(ADDRESS_DIR, `${name}.address`); - if (fs.existsSync(addressFile)) { - return fs.readFileSync(addressFile, 'utf-8').trim(); - } - } catch (error) { - logger.error(`Error reading DB address for ${name}:`, error); - } - return null; -}; - -export const saveDBAddress = (name: string, address: string): boolean => { - try { - // Ensure the address directory exists - if (!fs.existsSync(ADDRESS_DIR)) { - fs.mkdirSync(ADDRESS_DIR, { recursive: true, mode: 0o755 }); - } - - const addressFile = path.join(ADDRESS_DIR, `${name}.address`); - fs.writeFileSync(addressFile, address, { mode: 0o644 }); - logger.info(`Saved DB address for ${name} at ${addressFile}`); - return true; - } catch (error) { - logger.error(`Failed to save DB address for ${name}:`, error); - return false; - } -}; - -export const init = async () => { - try { - // Create directory with proper permissions if it doesn't exist - try { - if (!fs.existsSync(ORBITDB_DIR)) { - fs.mkdirSync(ORBITDB_DIR, { recursive: true, mode: 0o755 }); - logger.info(`Created OrbitDB directory: ${ORBITDB_DIR}`); - } - - // Check write permissions - fs.accessSync(ORBITDB_DIR, fs.constants.W_OK); - } catch (permError: any) { - logger.error(`Permission error with OrbitDB directory: ${ORBITDB_DIR}`, permError); - throw new Error(`Cannot access or write to OrbitDB directory: ${permError.message}`); - } - - // Create the addresses directory - try { - if (!fs.existsSync(ADDRESS_DIR)) { - fs.mkdirSync(ADDRESS_DIR, { recursive: true, mode: 0o755 }); - logger.info(`Created OrbitDB addresses directory: ${ADDRESS_DIR}`); - } - } catch (dirError) { - logger.error(`Error creating addresses directory: ${ADDRESS_DIR}`, dirError); - // Continue anyway, we'll handle failures when saving addresses - } - - registerFeed(); - - const ipfs = getHelia(); - if (!ipfs) { - throw new Error('IPFS instance is not initialized.'); - } - - logger.info(`Initializing OrbitDB with directory: ${ORBITDB_DIR}`); - - orbitdb = await createOrbitDB({ - ipfs, - directory: ORBITDB_DIR, - }); - - logger.info('OrbitDB initialized successfully.'); - return orbitdb; - } catch (e: any) { - logger.error('Failed to initialize OrbitDB:', e); - throw new Error(`OrbitDB initialization failed: ${e.message}`); - } -}; - -export const openDB = async (name: string, type: string) => { - if (!orbitdb) { - throw new Error('OrbitDB not initialized. Call init() first.'); - } - - const existingAddress = getDBAddress(name); - let db; - - try { - const dbOptions = { - type, - overwrite: false, - AccessController: IPFSAccessController({ - write: ['*'], - }), - }; - - if (existingAddress) { - logger.info(`Loading existing database with address: ${existingAddress}`); - db = await orbitdb.open(existingAddress, dbOptions); - } else { - logger.info(`Creating new database: ${name}`); - db = await orbitdb.open(name, dbOptions); - saveDBAddress(name, db.address.toString()); - } - - // Log the access controller type to verify - logger.info('Access Controller Type:', db.access.type); - return db; - } catch (error) { - logger.error(`Error opening database '${name}':`, error); - throw error; - } -}; - -export const getOrbitDB = () => { - return orbitdb; -}; - -export const db = async (dbName: string, type: string) => { - try { - if (!orbitdb) { - throw new Error('OrbitDB not initialized. Call init() first.'); - } - - return await openDB(dbName, type); - } catch (error: any) { - logger.error(`Error accessing database '${dbName}':`, error); - throw new Error(`Database error: ${error.message}`); - } -}; - -export default { - init, - openDB, - getOrbitDB, - db, -}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts deleted file mode 100644 index 3a0cb90..0000000 --- a/src/utils/logger.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { createLogger, format, transports } from 'winston'; -import fs from 'fs'; -import path from 'path'; - -// Define logger options interface -export interface LoggerOptions { - logsDir?: string; - level?: string; - disableConsole?: boolean; - disableFile?: boolean; -} - -// Define colors for different service types -const colors: Record = { - error: '\x1b[31m', // red - warn: '\x1b[33m', // yellow - info: '\x1b[32m', // green - debug: '\x1b[36m', // cyan - reset: '\x1b[0m', // reset - - // Service specific colors - IPFS: '\x1b[36m', // cyan - HEARTBEAT: '\x1b[33m', // yellow - SOCKET: '\x1b[34m', // blue - 'LOAD-BALANCER': '\x1b[35m', // magenta - DEFAULT: '\x1b[37m', // white -}; - -// Create a customizable logger factory -export function createDebrosLogger(options: LoggerOptions = {}) { - // Set default options - const logsDir = options.logsDir || path.join(process.cwd(), 'logs'); - const logLevel = options.level || process.env.LOG_LEVEL || 'info'; - - // Create logs directory if it doesn't exist - if (!fs.existsSync(logsDir) && !options.disableFile) { - fs.mkdirSync(logsDir, { recursive: true }); - } - - // Custom format for console output with colors - const customConsoleFormat = format.printf(({ level, message, timestamp, service }: any) => { - // Truncate error messages - if (level === 'error' && typeof message === 'string' && message.length > 300) { - message = message.substring(0, 300) + '... [truncated]'; - } - - // Handle objects and errors - if (typeof message === 'object' && message !== null) { - if (message instanceof Error) { - message = message.message; - // Truncate error messages - if (message.length > 300) { - message = message.substring(0, 300) + '... [truncated]'; - } - } else { - try { - message = JSON.stringify(message, null, 2); - } catch (e: any) { - console.error(e); - message = '[Object]'; - } - } - } - - const serviceColor = service && colors[service] ? colors[service] : colors.DEFAULT; - const levelColor = colors[level] || colors.DEFAULT; - const serviceTag = service ? `[${service}]` : ''; - - return `${timestamp} ${levelColor}${level}${colors.reset}: ${serviceColor}${serviceTag}${colors.reset} ${message}`; - }); - - // Custom format for file output (no colors) - const customFileFormat = format.printf(({ level, message, timestamp, service }) => { - // Handle objects and errors - if (typeof message === 'object' && message !== null) { - if (message instanceof Error) { - message = message.message; - } else { - try { - message = JSON.stringify(message); - } catch (e: any) { - console.error(e); - message = '[Object]'; - } - } - } - - const serviceTag = service ? `[${service}]` : ''; - return `${timestamp} ${level}: ${serviceTag} ${message}`; - }); - - // Configure transports - const loggerTransports = []; - - // Add console transport if not disabled - if (!options.disableConsole) { - loggerTransports.push( - new transports.Console({ - format: format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - customConsoleFormat, - ), - }), - ); - } - - // Add file transports if not disabled - if (!options.disableFile) { - loggerTransports.push( - // Combined log file - new transports.File({ - filename: path.join(logsDir, 'app.log'), - format: format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - customFileFormat, - ), - }), - // Error log file - new transports.File({ - filename: path.join(logsDir, 'error.log'), - level: 'error', - format: format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - customFileFormat, - ), - }), - ); - } - - // Create the logger - const logger = createLogger({ - level: logLevel, - format: format.combine(format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), format.splat()), - defaultMeta: { service: 'DEFAULT' }, - transports: loggerTransports, - }); - - // Helper function to create a logger for a specific service - const createServiceLogger = (serviceName: string) => { - return { - error: (message: any, ...meta: any[]) => - logger.error(message, { service: serviceName, ...meta }), - warn: (message: any, ...meta: any[]) => - logger.warn(message, { service: serviceName, ...meta }), - info: (message: any, ...meta: any[]) => - logger.info(message, { service: serviceName, ...meta }), - debug: (message: any, ...meta: any[]) => - logger.debug(message, { service: serviceName, ...meta }), - }; - }; - - return { - logger, - createServiceLogger, - }; -} - -// Create a default logger instance -const { logger, createServiceLogger } = createDebrosLogger(); - -export { logger, createServiceLogger }; -export default logger; diff --git a/system.txt b/system.txt new file mode 100644 index 0000000..c179db6 --- /dev/null +++ b/system.txt @@ -0,0 +1,1646 @@ +DebrosFramework Development Specification +Overview +DebrosFramework is a comprehensive Node.js framework built on top of OrbitDB and IPFS that provides a model-based abstraction layer for building decentralized applications. The framework automatically handles database partitioning, global indexing, relationships, schema migrations, pinning strategies, and pub/sub communication while providing a clean, familiar API similar to traditional ORMs. +Architecture Principles +Core Design Goals + +Scalability: Handle millions of users through automatic database partitioning +Developer Experience: Provide familiar ORM-like API with decorators and relationships +Automatic Management: Handle pinning, indexing, pub/sub, and migrations automatically +Type Safety: Full TypeScript support with comprehensive type definitions +Performance: Intelligent caching and query optimization +Flexibility: Support different database types and scoping strategies + +Framework Layers +┌─────────────────────────────────────┐ +│ Developer API │ ← Models, decorators, query builder +├─────────────────────────────────────┤ +│ Framework Core │ ← Model management, relationships +├─────────────────────────────────────┤ +│ Database Management │ ← Sharding, indexing, caching +├─────────────────────────────────────┤ +│ Existing @debros/network │ ← IPFS/OrbitDB abstraction +└─────────────────────────────────────┘ +Project Structure +@debros/network/ +├── src/ +│ ├── framework/ # New framework code +│ │ ├── core/ +│ │ │ ├── DebrosFramework.ts # Main framework class +│ │ │ ├── ModelRegistry.ts # Model registration and management +│ │ │ ├── DatabaseManager.ts # Database creation and management +│ │ │ └── ConfigManager.ts # Framework configuration +│ │ ├── models/ +│ │ │ ├── BaseModel.ts # Base model class +│ │ │ ├── decorators/ # Model decorators +│ │ │ │ ├── Model.ts # @Model decorator +│ │ │ │ ├── Field.ts # @Field decorator +│ │ │ │ ├── relationships.ts # @HasMany, @BelongsTo, etc. +│ │ │ │ └── hooks.ts # @BeforeCreate, @AfterUpdate, etc. +│ │ │ └── ModelFactory.ts # Model instantiation +│ │ ├── query/ +│ │ │ ├── QueryBuilder.ts # Main query builder +│ │ │ ├── QueryExecutor.ts # Query execution strategies +│ │ │ ├── QueryOptimizer.ts # Query optimization +│ │ │ └── QueryCache.ts # Query result caching +│ │ ├── relationships/ +│ │ │ ├── RelationshipManager.ts +│ │ │ ├── LazyLoader.ts +│ │ │ └── RelationshipCache.ts +│ │ ├── sharding/ +│ │ │ ├── ShardManager.ts # Database sharding logic +│ │ │ ├── HashSharding.ts # Hash-based sharding +│ │ │ ├── UserSharding.ts # User-scoped databases +│ │ │ └── GlobalIndexManager.ts # Global index management +│ │ ├── migrations/ +│ │ │ ├── MigrationManager.ts # Schema migration handling +│ │ │ ├── VersionManager.ts # Document version management +│ │ │ └── MigrationStrategies.ts +│ │ ├── pinning/ +│ │ │ ├── PinningManager.ts # Automatic pinning strategies +│ │ │ ├── TieredPinning.ts # Tiered pinning implementation +│ │ │ └── PinningStrategies.ts # Various pinning strategies +│ │ ├── pubsub/ +│ │ │ ├── PubSubManager.ts # Pub/sub abstraction +│ │ │ ├── EventEmitter.ts # Model event emission +│ │ │ └── SubscriptionManager.ts +│ │ ├── cache/ +│ │ │ ├── CacheManager.ts # Multi-level caching +│ │ │ ├── MemoryCache.ts # In-memory cache +│ │ │ ├── QueryCache.ts # Query result cache +│ │ │ └── RelationshipCache.ts # Relationship cache +│ │ ├── validation/ +│ │ │ ├── SchemaValidator.ts # Schema validation +│ │ │ ├── TypeValidator.ts # Field type validation +│ │ │ └── CustomValidators.ts # Custom validation rules +│ │ └── types/ +│ │ ├── framework.ts # Framework type definitions +│ │ ├── models.ts # Model type definitions +│ │ ├── decorators.ts # Decorator type definitions +│ │ └── queries.ts # Query type definitions +│ ├── db/ # Existing database service +│ ├── ipfs/ # Existing IPFS service +│ ├── orbit/ # Existing OrbitDB service +│ └── utils/ # Existing utilities +├── examples/ +│ ├── basic-usage/ +│ ├── relationships/ +│ ├── sharding/ +│ └── migrations/ +├── docs/ +│ ├── getting-started.md +│ ├── models.md +│ ├── relationships.md +│ ├── queries.md +│ ├── migrations.md +│ └── advanced.md +└── tests/ + ├── unit/ + ├── integration/ + └── performance/ +Implementation Roadmap +Phase 1: Core Model System (Weeks 1-2) +1.1 Base Model Implementation +File: src/framework/models/BaseModel.ts +typescript// Core functionality every model inherits +export abstract class BaseModel { + // Properties + public id: string; + public createdAt: number; + public updatedAt: number; + protected _loadedRelations: Map = new Map(); + protected _isDirty: boolean = false; + protected _isNew: boolean = true; + + // Static properties + static modelName: string; + static dbType: StoreType; + static scope: 'user' | 'global'; + static sharding?: ShardingConfig; + static pinning?: PinningConfig; + static fields: Map = new Map(); + static relationships: Map = new Map(); + static hooks: Map = new Map(); + + // Constructor + constructor(data: any = {}) { + this.fromJSON(data); + } + + // Core CRUD operations + async save(): Promise; + static async create(data: any): Promise; + static async get(id: string): Promise; + static async find(id: string): Promise; + async update(data: Partial): Promise; + async delete(): Promise; + + // Query operations + static where(field: string, operator: string, value: any): QueryBuilder; + static whereIn(field: string, values: any[]): QueryBuilder; + static orderBy(field: string, direction: 'asc' | 'desc'): QueryBuilder; + static limit(count: number): QueryBuilder; + static all(): Promise; + + // Relationship operations + async load(relationships: string[]): Promise; + async loadRelation(relationName: string): Promise; + + // Serialization + toJSON(): any; + fromJSON(data: any): this; + + // Validation + async validate(): Promise; + + // Hooks + async beforeCreate(): Promise; + async afterCreate(): Promise; + async beforeUpdate(): Promise; + async afterUpdate(): Promise; + async beforeDelete(): Promise; + async afterDelete(): Promise; +} +Implementation Tasks: + + Create BaseModel class with all core methods + Implement toJSON/fromJSON serialization + Add validation framework integration + Implement hook system (beforeCreate, afterUpdate, etc.) + Add dirty tracking for efficient updates + Create static method stubs (will be implemented in later phases) + +1.2 Model Decorators +File: src/framework/models/decorators/Model.ts +typescriptexport interface ModelConfig { + type?: StoreType; + scope?: 'user' | 'global'; + sharding?: ShardingConfig; + pinning?: PinningConfig; + pubsub?: PubSubConfig; + cache?: CacheConfig; + tableName?: string; +} + +export function Model(config: ModelConfig = {}) { + return function (target: T) { + // Set model configuration + target.modelName = config.tableName || target.name; + target.dbType = config.type || autoDetectType(target); + target.scope = config.scope || 'global'; + + // Register with framework + ModelRegistry.register(target.name, target, config); + + // Set up automatic database creation + DatabaseManager.scheduleCreation(target); + + return target; + }; +} + +function autoDetectType(modelClass: any): StoreType { + // Analyze model fields to suggest optimal database type + // Implementation details... +} +File: src/framework/models/decorators/Field.ts +typescriptexport 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 function Field(config: FieldConfig) { + return function (target: any, propertyKey: string) { + if (!target.constructor.fields) { + target.constructor.fields = new Map(); + } + + target.constructor.fields.set(propertyKey, config); + + // Create getter/setter with validation + const privateKey = `_${propertyKey}`; + + Object.defineProperty(target, propertyKey, { + get() { + return this[privateKey]; + }, + set(value) { + const validationResult = validateFieldValue(value, config); + if (!validationResult.valid) { + throw new ValidationError(validationResult.errors); + } + + this[privateKey] = config.transform ? config.transform(value) : value; + this._isDirty = true; + }, + enumerable: true, + configurable: true + }); + }; +} +File: src/framework/models/decorators/relationships.ts +typescriptexport interface RelationshipConfig { + type: 'belongsTo' | 'hasMany' | 'hasOne' | 'manyToMany'; + model: typeof BaseModel; + foreignKey: string; + localKey?: string; + through?: typeof BaseModel; + lazy?: boolean; +} + +export function BelongsTo(model: typeof BaseModel, foreignKey: string) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'belongsTo', + model, + foreignKey, + lazy: true + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +export function HasMany(model: typeof BaseModel, foreignKey: string) { + return function (target: any, propertyKey: string) { + const config: RelationshipConfig = { + type: 'hasMany', + model, + foreignKey, + lazy: true + }; + + registerRelationship(target, propertyKey, config); + createRelationshipProperty(target, propertyKey, config); + }; +} + +function createRelationshipProperty(target: any, propertyKey: string, config: RelationshipConfig) { + Object.defineProperty(target, propertyKey, { + get() { + if (!this._loadedRelations.has(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.`); + } + } + return this._loadedRelations.get(propertyKey); + }, + enumerable: true, + configurable: true + }); +} +Implementation Tasks: + + Implement @Model decorator with configuration + Create @Field decorator with validation and transformation + Implement relationship decorators (@BelongsTo, @HasMany, @HasOne, @ManyToMany) + Add hook decorators (@BeforeCreate, @AfterUpdate, etc.) + Create auto-detection logic for optimal database types + Add decorator validation and error handling + +1.3 Model Registry +File: src/framework/core/ModelRegistry.ts +typescriptexport class ModelRegistry { + private static models: Map = new Map(); + private static configs: Map = new Map(); + + static register(name: string, modelClass: typeof BaseModel, config: ModelConfig) { + this.models.set(name, modelClass); + this.configs.set(name, config); + + // Validate model configuration + this.validateModel(modelClass, config); + } + + 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 { + return this.models; + } + + static getUserScopedModels(): Array { + return Array.from(this.models.values()).filter( + model => model.scope === 'user' + ); + } + + static getGlobalModels(): Array { + return Array.from(this.models.values()).filter( + model => model.scope === 'global' + ); + } + + private static validateModel(modelClass: typeof BaseModel, config: ModelConfig) { + // Validate model configuration + // Check for conflicts, missing requirements, etc. + } +} +Phase 2: Database Management & Sharding (Weeks 3-4) +2.1 Database Manager +File: src/framework/core/DatabaseManager.ts +typescriptexport class DatabaseManager { + private framework: DebrosFramework; + private databases: Map = new Map(); + private userMappings: Map = new Map(); + + constructor(framework: DebrosFramework) { + this.framework = framework; + } + + async initializeAllDatabases() { + // Initialize global databases + await this.initializeGlobalDatabases(); + + // Initialize system databases (user directory, etc.) + await this.initializeSystemDatabases(); + } + + async createUserDatabases(userId: string): Promise { + const userScopedModels = ModelRegistry.getUserScopedModels(); + const databases: any = {}; + + // Create mappings database first + const mappingsDB = await this.createDatabase( + `${userId}-mappings`, + 'keyvalue', + 'user' + ); + + // Create database for each user-scoped model + for (const model of userScopedModels) { + const dbName = `${userId}-${model.modelName.toLowerCase()}`; + const db = await this.createDatabase(dbName, model.dbType, 'user'); + databases[`${model.modelName.toLowerCase()}DB`] = db.address.toString(); + } + + // Store mappings + await mappingsDB.set('mappings', databases); + + // Register in global directory + await this.registerUserInDirectory(userId, mappingsDB.address.toString()); + + return new UserMappings(userId, databases); + } + + async getUserDatabase(userId: string, modelName: string): Promise { + const mappings = await this.getUserMappings(userId); + const dbAddress = mappings[`${modelName.toLowerCase()}DB`]; + + if (!dbAddress) { + throw new Error(`Database not found for user ${userId} and model ${modelName}`); + } + + return await this.openDatabase(dbAddress); + } + + async getUserMappings(userId: string): Promise { + // Check cache first + if (this.userMappings.has(userId)) { + return this.userMappings.get(userId); + } + + // Get from global directory + const directoryShards = await this.getGlobalDirectoryShards(); + const shardIndex = this.getShardIndex(userId, directoryShards.length); + const shard = directoryShards[shardIndex]; + + 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'); + + // Cache for future use + this.userMappings.set(userId, mappings); + + return mappings; + } + + private async createDatabase(name: string, type: StoreType, scope: string): Promise { + // Use existing OrbitDB service + return await this.framework.orbitDBService.openDB(name, type); + } + + 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; + } +} +2.2 Shard Manager +File: src/framework/sharding/ShardManager.ts +typescriptexport interface ShardingConfig { + strategy: 'hash' | 'range' | 'user'; + count: number; + key: string; +} + +export class ShardManager { + private shards: Map = new Map(); + + async createShards(modelName: string, config: ShardingConfig): Promise { + const shards: any[] = []; + + for (let i = 0; i < config.count; i++) { + const shardName = `${modelName.toLowerCase()}-shard-${i}`; + const shard = await this.createShard(shardName, config); + shards.push(shard); + } + + this.shards.set(modelName, shards); + } + + getShardForKey(modelName: string, key: string): any { + const shards = this.shards.get(modelName); + if (!shards) { + throw new Error(`No shards found for model ${modelName}`); + } + + const shardIndex = this.calculateShardIndex(key, shards.length); + return shards[shardIndex]; + } + + getAllShards(modelName: string): any[] { + return this.shards.get(modelName) || []; + } + + private calculateShardIndex(key: string, shardCount: number): number { + // 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 async createShard(shardName: string, config: ShardingConfig): Promise { + // Create OrbitDB database for this shard + // Implementation depends on existing OrbitDB service + } +} +Phase 3: Query System (Weeks 5-6) +3.1 Query Builder +File: src/framework/query/QueryBuilder.ts +typescriptexport class QueryBuilder { + private model: typeof BaseModel; + private conditions: QueryCondition[] = []; + private relations: string[] = []; + private sorting: SortConfig[] = []; + private limitation?: number; + private offsetValue?: number; + + constructor(model: typeof BaseModel) { + this.model = model; + } + + 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); + } + + whereUserIn(userIds: string[]): this { + // Special method for user-scoped queries + this.conditions.push({ + field: 'userId', + operator: 'userIn', + value: userIds + }); + return this; + } + + orderBy(field: string, direction: 'asc' | 'desc' = 'asc'): this { + this.sorting.push({ field, direction }); + return this; + } + + limit(count: number): this { + this.limitation = count; + return this; + } + + offset(count: number): this { + this.offsetValue = count; + return this; + } + + load(relationships: string[]): this { + this.relations = relationships; + return this; + } + + async exec(): Promise { + const executor = new QueryExecutor(this.model, this); + return await executor.execute(); + } + + async first(): Promise { + const results = await this.limit(1).exec(); + return results[0] || null; + } + + async count(): Promise { + const executor = new QueryExecutor(this.model, this); + return await executor.count(); + } + + // Getters for query configuration + 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; } +} +3.2 Query Executor +File: src/framework/query/QueryExecutor.ts +typescriptexport class QueryExecutor { + private model: typeof BaseModel; + private query: QueryBuilder; + private framework: DebrosFramework; + + constructor(model: typeof BaseModel, query: QueryBuilder) { + this.model = model; + this.query = query; + this.framework = DebrosFramework.getInstance(); + } + + async execute(): Promise { + if (this.model.scope === 'user') { + return await this.executeUserScopedQuery(); + } else { + return await this.executeGlobalQuery(); + } + } + + private async executeUserScopedQuery(): Promise { + 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 { + const userIds = userFilter.operator === 'userIn' + ? userFilter.value + : [userFilter.value]; + + 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); + } 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 { + // Query global index for user-scoped models + const globalIndexName = `${this.model.modelName}Index`; + const indexShards = this.framework.shardManager.getAllShards(globalIndexName); + + const results: any[] = []; + + // Query all shards in parallel + const promises = indexShards.map(shard => this.queryDatabase(shard)); + const shardResults = await Promise.all(promises); + + for (const shardResult of shardResults) { + results.push(...shardResult); + } + + // Now fetch actual documents from user databases + return await this.fetchActualDocuments(results); + } + + private async executeGlobalQuery(): Promise { + // 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); + } + } + + private async queryDatabase(database: any): Promise { + // Get all documents from OrbitDB + let documents: any[]; + + if (this.model.dbType === 'eventlog') { + const iterator = database.iterator(); + documents = iterator.collect(); + } else if (this.model.dbType === 'keyvalue') { + documents = Object.values(database.all()); + } else if (this.model.dbType === 'docstore') { + documents = database.query(() => true); + } + + // 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 + return documents.map(doc => new this.model(doc) as T); + } + + 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; + const docValue = this.getNestedValue(doc, field); + + switch (operator) { + case '=': + case '==': + return docValue === value; + case '!=': + return docValue !== value; + case '>': + return docValue > value; + case '>=': + return docValue >= value; + case '<': + return docValue < value; + case '<=': + return docValue <= value; + case '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()); + default: + throw new Error(`Unsupported operator: ${operator}`); + } + } + + 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; + } +} +Phase 4: Relationships & Loading (Weeks 7-8) +4.1 Relationship Manager +File: src/framework/relationships/RelationshipManager.ts +typescriptexport class RelationshipManager { + private framework: DebrosFramework; + private cache: RelationshipCache; + + constructor(framework: DebrosFramework) { + this.framework = framework; + this.cache = new RelationshipCache(); + } + + async loadRelationship( + instance: BaseModel, + relationshipName: string + ): Promise { + const relationConfig = instance.constructor.relationships.get(relationshipName); + + if (!relationConfig) { + throw new Error(`Relationship '${relationshipName}' not found`); + } + + // Check cache first + const cacheKey = this.getCacheKey(instance, relationshipName); + const cached = this.cache.get(cacheKey); + if (cached) { + return cached; + } + + let result: any; + + switch (relationConfig.type) { + case 'belongsTo': + result = await this.loadBelongsTo(instance, relationConfig); + break; + case 'hasMany': + result = await this.loadHasMany(instance, relationConfig); + break; + case 'hasOne': + result = await this.loadHasOne(instance, relationConfig); + break; + case 'manyToMany': + result = await this.loadManyToMany(instance, relationConfig); + break; + default: + throw new Error(`Unsupported relationship type: ${relationConfig.type}`); + } + + // Cache the result + this.cache.set(cacheKey, result); + + // Store in instance + instance._loadedRelations.set(relationshipName, result); + + return result; + } + + private async loadBelongsTo( + instance: BaseModel, + config: RelationshipConfig + ): Promise { + const foreignKeyValue = instance[config.foreignKey]; + + if (!foreignKeyValue) { + return null; + } + + return await config.model.get(foreignKeyValue); + } + + private async loadHasMany( + instance: BaseModel, + config: RelationshipConfig + ): Promise { + if (config.through) { + return await this.loadManyToMany(instance, config); + } + + // Direct has-many relationship + const query = config.model.where(config.foreignKey, '=', instance.id); + return await query.exec(); + } + + private async loadHasOne( + instance: BaseModel, + config: RelationshipConfig + ): Promise { + const results = await this.loadHasMany(instance, config); + return results[0] || null; + } + + private async loadManyToMany( + instance: BaseModel, + config: RelationshipConfig + ): Promise { + if (!config.through) { + throw new Error('Many-to-many relationships require a through model'); + } + + // Get junction table records + const junctionRecords = await config.through + .where(config.localKey || 'id', '=', instance.id) + .exec(); + + // Extract foreign keys + const foreignKeys = junctionRecords.map(record => record[config.foreignKey]); + + // Get related models + return await config.model.whereIn('id', foreignKeys).exec(); + } + + async eagerLoadRelationships( + instances: BaseModel[], + relationships: string[] + ): Promise { + // Load relationships for multiple instances efficiently + for (const relationshipName of relationships) { + await this.eagerLoadSingleRelationship(instances, relationshipName); + } + } + + private async eagerLoadSingleRelationship( + instances: BaseModel[], + relationshipName: string + ): Promise { + if (instances.length === 0) return; + + const firstInstance = instances[0]; + const relationConfig = firstInstance.constructor.relationships.get(relationshipName); + + if (!relationConfig) { + throw new Error(`Relationship '${relationshipName}' not found`); + } + + switch (relationConfig.type) { + case 'belongsTo': + await this.eagerLoadBelongsTo(instances, relationConfig); + break; + case 'hasMany': + await this.eagerLoadHasMany(instances, relationConfig); + break; + // Add other relationship types... + } + } + + private async eagerLoadBelongsTo( + instances: BaseModel[], + config: RelationshipConfig + ): Promise { + // Get all foreign key values + const foreignKeys = instances + .map(instance => instance[config.foreignKey]) + .filter(key => key != null); + + // Remove duplicates + const uniqueForeignKeys = [...new Set(foreignKeys)]; + + // Load all related models at once + const relatedModels = await config.model.whereIn('id', uniqueForeignKeys).exec(); + + // Create lookup map + const relatedMap = new Map(); + relatedModels.forEach(model => relatedMap.set(model.id, model)); + + // Assign to instances + instances.forEach(instance => { + const foreignKeyValue = instance[config.foreignKey]; + const related = relatedMap.get(foreignKeyValue) || null; + instance._loadedRelations.set(relationshipName, related); + }); + } +} +Phase 5: Automatic Features (Weeks 9-10) +5.1 Pinning Manager +File: src/framework/pinning/PinningManager.ts +typescriptexport class PinningManager { + private framework: DebrosFramework; + private strategies: Map = new Map(); + + constructor(framework: DebrosFramework) { + this.framework = framework; + this.initializeStrategies(); + } + + async pinDocument(model: BaseModel, document: any): Promise { + const modelClass = model.constructor as typeof BaseModel; + const pinningConfig = ModelRegistry.getConfig(modelClass.name)?.pinning; + + if (!pinningConfig) { + // Use default pinning + await this.pinToNodes(document.cid, 2); + return; + } + + const strategy = this.strategies.get(pinningConfig.strategy || 'fixed'); + if (!strategy) { + throw new Error(`Unknown pinning strategy: ${pinningConfig.strategy}`); + } + + const pinningFactor = await strategy.calculatePinningFactor(document, pinningConfig); + await this.pinToNodes(document.cid, pinningFactor); + } + + private async pinToNodes(cid: string, factor: number): Promise { + // Get available nodes from IPFS service + const availableNodes = await this.framework.ipfsService.getConnectedPeers(); + const nodeArray = Array.from(availableNodes.keys()); + + // Select nodes for pinning + const selectedNodes = this.selectPinningNodes(nodeArray, factor); + + // Pin to selected nodes + const pinPromises = selectedNodes.map(nodeId => + this.pinToSpecificNode(nodeId, cid) + ); + + await Promise.allSettled(pinPromises); + } + + private selectPinningNodes(nodes: string[], factor: number): string[] { + // Simple round-robin selection for now + // Could be enhanced with load balancing, geographic distribution, etc. + const shuffled = [...nodes].sort(() => Math.random() - 0.5); + return shuffled.slice(0, Math.min(factor, nodes.length)); + } + + private async pinToSpecificNode(nodeId: string, cid: string): Promise { + try { + // Implementation depends on your IPFS cluster setup + // This could be HTTP API calls, libp2p messages, etc. + await this.framework.ipfsService.pinOnNode(nodeId, cid); + } catch (error) { + console.warn(`Failed to pin ${cid} to node ${nodeId}:`, error); + } + } +} + +export interface PinningStrategy { + calculatePinningFactor(document: any, config: PinningConfig): Promise; +} + +export class PopularityPinningStrategy implements PinningStrategy { + async calculatePinningFactor(document: any, config: PinningConfig): Promise { + const baseFactor = config.factor || 2; + + // Increase pinning based on engagement + const likes = document.likes || 0; + const comments = document.comments || 0; + const engagement = likes + (comments * 2); + + if (engagement > 1000) return baseactor * 5; + if (engagement > 100) return baseactor * 3; + if (engagement > 10) return baseactor * 2; + + return baseFactor; + } +} +5.2 PubSub Manager +File: src/framework/pubsub/PubSubManager.ts +typescriptexport class PubSubManager { + private framework: DebrosFramework; + private subscriptions: Map> = new Map(); + + constructor(framework: DebrosFramework) { + this.framework = framework; + } + + async publishModelEvent( + model: BaseModel, + event: string, + data: any + ): Promise { + const modelClass = model.constructor as typeof BaseModel; + const pubsubConfig = ModelRegistry.getConfig(modelClass.name)?.pubsub; + + if (!pubsubConfig || !pubsubConfig.events.includes(event)) { + return; // No pub/sub configured for this event + } + + // Publish to configured channels + for (const channelTemplate of pubsubConfig.channels) { + const channel = this.resolveChannelTemplate(channelTemplate, model, data); + await this.publishToChannel(channel, { + model: modelClass.name, + event, + data, + timestamp: Date.now() + }); + } + } + + async subscribe( + channel: string, + callback: (data: any) => void + ): Promise<() => void> { + if (!this.subscriptions.has(channel)) { + this.subscriptions.set(channel, new Set()); + + // Subscribe to IPFS pubsub + await this.framework.ipfsService.pubsub.subscribe( + channel, + (message) => this.handleChannelMessage(channel, message) + ); + } + + this.subscriptions.get(channel)!.add(callback); + + // Return unsubscribe function + return () => { + const channelSubs = this.subscriptions.get(channel); + if (channelSubs) { + channelSubs.delete(callback); + if (channelSubs.size === 0) { + this.subscriptions.delete(channel); + this.framework.ipfsService.pubsub.unsubscribe(channel); + } + } + }; + } + + private resolveChannelTemplate( + template: string, + model: BaseModel, + data: any + ): string { + return template + .replace('{userId}', model.userId || 'unknown') + .replace('{modelName}', model.constructor.name) + .replace('{id}', model.id); + } + + private async publishToChannel(channel: string, data: any): Promise { + const message = JSON.stringify(data); + await this.framework.ipfsService.pubsub.publish(channel, message); + } + + private handleChannelMessage(channel: string, message: any): void { + const subscribers = this.subscriptions.get(channel); + if (!subscribers) return; + + try { + const data = JSON.parse(message.data.toString()); + subscribers.forEach(callback => { + try { + callback(data); + } catch (error) { + console.error('Error in pubsub callback:', error); + } + }); + } catch (error) { + console.error('Error parsing pubsub message:', error); + } + } +} +Phase 6: Migration System (Weeks 11-12) +6.1 Migration Manager +File: src/framework/migrations/MigrationManager.ts +typescriptexport abstract class Migration { + abstract version: number; + abstract modelName: string; + + abstract up(document: any): any; + abstract down(document: any): any; + + async validate(document: any): Promise { + // Optional validation after migration + return true; + } +} + +export class MigrationManager { + private migrations: Map = new Map(); + private framework: DebrosFramework; + + constructor(framework: DebrosFramework) { + this.framework = framework; + } + + registerMigration(migration: Migration): void { + const modelName = migration.modelName; + + if (!this.migrations.has(modelName)) { + this.migrations.set(modelName, []); + } + + const modelMigrations = this.migrations.get(modelName)!; + modelMigrations.push(migration); + + // Sort by version + modelMigrations.sort((a, b) => a.version - b.version); + } + + async migrateDocument( + document: any, + modelName: string, + targetVersion?: number + ): Promise { + const currentVersion = document._schemaVersion || 1; + const modelClass = ModelRegistry.get(modelName); + + if (!modelClass) { + throw new Error(`Model ${modelName} not found`); + } + + const finalVersion = targetVersion || modelClass.currentVersion || 1; + + if (currentVersion === finalVersion) { + return document; // No migration needed + } + + if (currentVersion > finalVersion) { + return await this.downgradeDocument(document, modelName, currentVersion, finalVersion); + } else { + return await this.upgradeDocument(document, modelName, currentVersion, finalVersion); + } + } + + private async upgradeDocument( + document: any, + modelName: string, + fromVersion: number, + toVersion: number + ): Promise { + const migrations = this.getMigrationsForModel(modelName); + let current = { ...document }; + + for (const migration of migrations) { + if (migration.version > fromVersion && migration.version <= toVersion) { + try { + current = migration.up(current); + current._schemaVersion = migration.version; + + // Validate migration result + const isValid = await migration.validate(current); + if (!isValid) { + throw new Error(`Migration validation failed for version ${migration.version}`); + } + } catch (error) { + console.error(`Migration failed at version ${migration.version}:`, error); + throw error; + } + } + } + + return current; + } + + private async downgradeDocument( + document: any, + modelName: string, + fromVersion: number, + toVersion: number + ): Promise { + const migrations = this.getMigrationsForModel(modelName).reverse(); + let current = { ...document }; + + for (const migration of migrations) { + if (migration.version <= fromVersion && migration.version > toVersion) { + try { + current = migration.down(current); + current._schemaVersion = migration.version - 1; + } catch (error) { + console.error(`Downgrade failed at version ${migration.version}:`, error); + throw error; + } + } + } + + return current; + } + + private getMigrationsForModel(modelName: string): Migration[] { + return this.migrations.get(modelName) || []; + } + + async migrateAllDocuments(modelName: string): Promise { + // Background migration of all documents for a model + const modelClass = ModelRegistry.get(modelName); + if (!modelClass) { + throw new Error(`Model ${modelName} not found`); + } + + if (modelClass.scope === 'user') { + await this.migrateUserScopedModel(modelName); + } else { + await this.migrateGlobalModel(modelName); + } + } + + private async migrateUserScopedModel(modelName: string): Promise { + // This is complex - would need to iterate through all users + // and migrate their individual databases + console.log(`Background migration for user-scoped model ${modelName} not implemented`); + } + + private async migrateGlobalModel(modelName: string): Promise { + // Migrate documents in global database + const db = await this.framework.databaseManager.getGlobalDatabase(modelName); + // Implementation depends on database type and migration strategy + } +} + +// Example migration +export class PostAddMediaMigration extends Migration { + version = 2; + modelName = 'Post'; + + up(document: any): any { + return { + ...document, + mediaCIDs: [], // Add new field + _schemaVersion: 2 + }; + } + + down(document: any): any { + const { mediaCIDs, ...rest } = document; + return { + ...rest, + _schemaVersion: 1 + }; + } +} +Phase 7: Framework Integration (Weeks 13-14) +7.1 Main Framework Class +File: src/framework/core/DebrosFramework.ts +typescriptexport class DebrosFramework { + private static instance: DebrosFramework; + + public databaseManager: DatabaseManager; + public shardManager: ShardManager; + public queryExecutor: QueryExecutor; + public relationshipManager: RelationshipManager; + public pinningManager: PinningManager; + public pubsubManager: PubSubManager; + public migrationManager: MigrationManager; + public cacheManager: CacheManager; + + public ipfsService: any; + public orbitDBService: any; + + private initialized: boolean = false; + + constructor(config: FrameworkConfig) { + this.databaseManager = new DatabaseManager(this); + this.shardManager = new ShardManager(); + this.relationshipManager = new RelationshipManager(this); + this.pinningManager = new PinningManager(this); + this.pubsubManager = new PubSubManager(this); + this.migrationManager = new MigrationManager(this); + this.cacheManager = new CacheManager(config.cache); + + // Use existing services + this.ipfsService = ipfsService; + this.orbitDBService = orbitDBService; + } + + static getInstance(config?: FrameworkConfig): DebrosFramework { + if (!DebrosFramework.instance) { + if (!config) { + throw new Error('Framework not initialized. Provide config on first call.'); + } + DebrosFramework.instance = new DebrosFramework(config); + } + return DebrosFramework.instance; + } + + async initialize(models: Array = []): Promise { + if (this.initialized) { + return; + } + + // Initialize underlying services + await this.ipfsService.init(); + await this.orbitDBService.init(); + + // Register models + models.forEach(model => { + if (!ModelRegistry.get(model.name)) { + // Auto-register models that weren't registered via decorators + ModelRegistry.register(model.name, model, {}); + } + }); + + // Initialize databases + await this.databaseManager.initializeAllDatabases(); + + // Create shards for global models + const globalModels = ModelRegistry.getGlobalModels(); + for (const model of globalModels) { + if (model.sharding) { + await this.shardManager.createShards(model.name, model.sharding); + } + } + + // Set up model stores + await this.setupModelStores(); + + // Set up automatic event handling + await this.setupEventHandling(); + + this.initialized = true; + } + + async createUser(userData: any): Promise { + return await this.databaseManager.createUserDatabases(userData.id); + } + + async getUser(userId: string): Promise { + return await this.databaseManager.getUserMappings(userId); + } + + private async setupModelStores(): Promise { + const allModels = ModelRegistry.getAllModels(); + + for (const [modelName, modelClass] of allModels) { + // Set the store for each model + if (modelClass.scope === 'global') { + if (modelClass.sharding) { + // Sharded global model + const shards = this.shardManager.getAllShards(modelName); + modelClass.setShards(shards); + } else { + // Single global database + const db = await this.databaseManager.getGlobalDatabase(modelName); + modelClass.setStore(db); + } + } + // User-scoped models get their stores dynamically per query + } + } + + private async setupEventHandling(): Promise { + // Set up automatic pub/sub and pinning for model events + const allModels = ModelRegistry.getAllModels(); + + for (const [modelName, modelClass] of allModels) { + // Hook into model lifecycle events + this.setupModelEventHooks(modelClass); + } + } + + private setupModelEventHooks(modelClass: typeof BaseModel): void { + const originalCreate = modelClass.create; + const originalUpdate = modelClass.prototype.update; + const originalDelete = modelClass.prototype.delete; + + // Override create method + modelClass.create = async function(data: any) { + const instance = await originalCreate.call(this, data); + + // Automatic pinning + await DebrosFramework.getInstance().pinningManager.pinDocument(instance, data); + + // Automatic pub/sub + await DebrosFramework.getInstance().pubsubManager.publishModelEvent( + instance, 'created', data + ); + + return instance; + }; + + // Override update method + modelClass.prototype.update = async function(data: any) { + const result = await originalUpdate.call(this, data); + + // Automatic pub/sub + await DebrosFramework.getInstance().pubsubManager.publishModelEvent( + this, 'updated', data + ); + + return result; + }; + + // Override delete method + modelClass.prototype.delete = async function() { + const result = await originalDelete.call(this); + + // Automatic pub/sub + await DebrosFramework.getInstance().pubsubManager.publishModelEvent( + this, 'deleted', {} + ); + + return result; + }; + } + + async stop(): Promise { + await this.orbitDBService.stop(); + await this.ipfsService.stop(); + this.initialized = false; + } +} + +export interface FrameworkConfig { + cache?: CacheConfig; + defaultPinning?: PinningConfig; + autoMigration?: boolean; +} +Testing Strategy +Unit Tests + +Model Tests: Test model creation, validation, serialization +Decorator Tests: Test all decorators work correctly +Query Tests: Test query builder and execution +Relationship Tests: Test all relationship types +Migration Tests: Test schema migrations +Sharding Tests: Test shard distribution and querying + +Integration Tests + +End-to-End Scenarios: Complete user workflows +Cross-Model Tests: Complex queries across multiple models +Performance Tests: Large dataset handling +Failure Recovery: Network failures, node failures + +Performance Tests + +Scalability Tests: Test with millions of documents +Query Performance: Benchmark query execution times +Memory Usage: Monitor memory consumption +Concurrent Access: Test multiple simultaneous operations + +Documentation Requirements +Developer Documentation + +Getting Started Guide: Basic setup and first model +Model Guide: Comprehensive model documentation +Relationships Guide: All relationship types with examples +Query Guide: Complete query API documentation +Migration Guide: Schema evolution patterns +Advanced Features: Sharding, pinning, pub/sub customization + +API Reference + +Complete TypeScript API documentation +All decorators with examples +Query builder methods +Framework configuration options + +Examples + +Basic blog application +Social media features +Real-time chat +Complex relationship scenarios +Migration examples + +Implementation Priorities +Critical Path (Must implement first) + +BaseModel class with basic CRUD +@Model and @Field decorators +Basic query builder (where, limit, etc.) +User database creation and management +Simple relationships (belongsTo, hasMany) + +High Priority (Implement second) + +Global indexing for cross-user queries +Automatic pinning based on model config +Query optimization and caching +Complete relationship types +Migration system + +Medium Priority (Can be added later) + +Advanced sharding strategies +Complex pinning strategies +Pub/sub system +Performance monitoring +Advanced caching + +Nice to Have (Future enhancements) + +GraphQL integration +Real-time subscriptions +Backup and recovery +Analytics and metrics +Admin dashboard + +Development Workflow +Phase Implementation + +Create feature branch for each phase +Implement core functionality with basic tests +Add comprehensive tests for the feature +Update documentation with examples +Review and integrate with main branch + +Code Quality Standards + +TypeScript strict mode enabled +100% type coverage for public APIs +Unit test coverage minimum 80% +ESLint and Prettier for code formatting +Conventional commits for changelog generation + +Testing Approach + +TDD approach where possible +Mock OrbitDB/IPFS for unit tests +Real OrbitDB for integration tests +Performance benchmarks for critical paths + +This specification provides a comprehensive roadmap for building the DebrosFramework. Each phase builds upon the previous one, allowing for iterative development and testing. The framework will provide a powerful, developer-friendly abstraction over OrbitDB while maintaining the scalability benefits of your original architecture.Crafted comprehensive dev document for AI framework implementation.7sThe user wants a comprehensive development document that can be provided to AI assistants to help build the framework. I've created a very detailed specification that covers: + +Architecture and Design Principles - Clear overview of what we're building and why +Detailed Project Structure - Complete file organization +Phase-by-Phase Implementation Plan - 7 phases over 14 weeks with specific deliverables +Complete Code Examples - Detailed implementation examples for each major component +Testing Strategy - Unit, integration, and performance testing approaches +Documentation Requirements - What docs need to be created +Implementation Priorities - What to build first vs. later +Development Workflow - How to organize the work + +The document is structured so that an AI assistant could: + +Understand the overall architecture +Implement specific components with the provided examples +Follow the testing and documentation requirements +Prioritize work appropriately + +Each phase has specific deliverables and code examples that show exactly what needs to be built. The document balances being comprehensive with being actionable - it provides enough detail that someone (or an AI) could start implementing immediately. +Key strengths of this specification: + +Concrete code examples for all major components +Clear separation of concerns between different modules +Incremental development approach that allows testing at each phase +Realistic timeline with achievable milestones +Comprehensive testing strategy to ensure quality +Scalability considerations built into the design from the start + +The document should give the user everything they need to either implement this themselves or provide to AI assistants for help with implementation.I've created a comprehensive development specification document that covers everything needed to build the DebrosFramework. This document is designed to be provided to AI assistants like Claude for implementation help. +Key Features of This Specification: +Complete Architecture Overview + +Clear design principles and goals +Detailed project structure with file organization +Integration with your existing @debros/network codebase + +Phase-by-Phase Implementation Plan (14 weeks) + +Phase 1-2: Core model system with decorators +Phase 3-4: Database management and sharding +Phase 5-6: Query system and relationships +Phase 7-8: Relationship loading and caching +Phase 9-10: Automatic pinning and pub/sub +Phase 11-12: Migration system +Phase 13-14: Framework integration and optimization + +Detailed Code Examples + +Complete BaseModel implementation +All decorator implementations (@Model, @Field, @BelongsTo, etc.) +Query builder with smart execution strategies +Relationship management across database types +Automatic pinning and pub/sub systems +Schema migration handling + +Comprehensive Implementation Details + +How user-scoped vs global databases work +Sharding strategies for millions of users +Query optimization for different scenarios +Relationship loading across database types +Automatic feature management (pinning, pub/sub, etc.) + +Developer Experience Focus +The framework provides a clean, familiar API while handling all OrbitDB complexity automatically: +typescript// Simple API that developers see +@Model({ scope: 'user', pinning: { factor: 3 } }) +class Post extends BaseModel { + @Field({ type: 'string' }) + content: string; + + @BelongsTo(User, 'userId') + author: User; +} + +// Complex operations work seamlessly +const posts = await Post + .whereUserIn(followedUsers) + .where('isPublic', '=', true) + .load(['author']) + .orderBy('createdAt', 'desc') + .limit(50) + .exec(); +Testing & Quality Assurance + +Unit, integration, and performance testing strategies +Code quality standards and workflows +Documentation requirements + +How to Use This Specification: + +For AI Assistance: Provide this entire document to Claude or other AI assistants when asking for implementation help +For Development Teams: Use as a technical specification and roadmap +For Phase Planning: Each phase has clear deliverables and can be implemented independently \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..54f7a97 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,44 @@ +# Tests + +This directory contains the test suite for the Debros Network framework. + +## Structure + +``` +tests/ +├── unit/ # Unit tests for individual components +│ ├── core/ # Core framework components +│ ├── models/ # Model-related functionality +│ ├── relationships/ # Relationship management +│ ├── sharding/ # Data sharding functionality +│ ├── decorators/ # Decorator functionality +│ └── migrations/ # Database migrations +├── real-integration/ # Real integration tests with Docker +│ └── blog-scenario/ # Complete blog application scenario +├── mocks/ # Mock implementations for testing +└── setup.ts # Test setup and configuration + +``` + +## Running Tests + +### Unit Tests +Run all unit tests (fast, uses mocks): +```bash +pnpm run test:unit +``` + +### Real Integration Tests +Run full integration tests with Docker (slower, uses real services): +```bash +pnpm run test:real +``` + +## Test Categories + +- **Unit Tests**: Fast, isolated tests that use mocks for external dependencies +- **Real Integration Tests**: End-to-end tests that spin up actual IPFS nodes and OrbitDB instances using Docker + +## Coverage + +Unit tests provide code coverage reports in the `coverage/` directory after running. diff --git a/tests/mocks/ipfs.ts b/tests/mocks/ipfs.ts new file mode 100644 index 0000000..f9b7863 --- /dev/null +++ b/tests/mocks/ipfs.ts @@ -0,0 +1,248 @@ +// Mock IPFS for testing +export class MockLibp2p { + private peers = new Set(); + + async start() { + // Mock start + } + + async stop() { + // Mock stop + } + + getPeers() { + return Array.from(this.peers); + } + + async dial(peerId: string) { + this.peers.add(peerId); + return { remotePeer: peerId }; + } + + async hangUp(peerId: string) { + this.peers.delete(peerId); + } + + get peerId() { + return { toString: () => 'mock-peer-id' }; + } + + // PubSub mock + pubsub = { + publish: jest.fn(async (topic: string, data: Uint8Array) => { + // Mock publish + }), + subscribe: jest.fn(async (topic: string) => { + // Mock subscribe + }), + unsubscribe: jest.fn(async (topic: string) => { + // Mock unsubscribe + }), + getTopics: jest.fn(() => []), + getPeers: jest.fn(() => []) + }; + + // Services mock + services = { + pubsub: this.pubsub + }; +} + +export class MockHelia { + public libp2p: MockLibp2p; + private content = new Map(); + private pins = new Set(); + + constructor() { + this.libp2p = new MockLibp2p(); + } + + async start() { + await this.libp2p.start(); + } + + async stop() { + await this.libp2p.stop(); + } + + get blockstore() { + return { + put: jest.fn(async (cid: any, block: Uint8Array) => { + const key = cid.toString(); + this.content.set(key, block); + return cid; + }), + get: jest.fn(async (cid: any) => { + const key = cid.toString(); + const block = this.content.get(key); + if (!block) { + throw new Error(`Block not found: ${key}`); + } + return block; + }), + has: jest.fn(async (cid: any) => { + return this.content.has(cid.toString()); + }), + delete: jest.fn(async (cid: any) => { + return this.content.delete(cid.toString()); + }) + }; + } + + get datastore() { + return { + put: jest.fn(async (key: any, value: Uint8Array) => { + this.content.set(key.toString(), value); + }), + get: jest.fn(async (key: any) => { + const value = this.content.get(key.toString()); + if (!value) { + throw new Error(`Key not found: ${key}`); + } + return value; + }), + has: jest.fn(async (key: any) => { + return this.content.has(key.toString()); + }), + delete: jest.fn(async (key: any) => { + return this.content.delete(key.toString()); + }) + }; + } + + get pins() { + return { + add: jest.fn(async (cid: any) => { + this.pins.add(cid.toString()); + }), + rm: jest.fn(async (cid: any) => { + this.pins.delete(cid.toString()); + }), + ls: jest.fn(async function* () { + for (const pin of Array.from(this.pins)) { + yield { cid: pin }; + } + }.bind(this)) + }; + } + + // Add UnixFS mock + get fs() { + return { + addBytes: jest.fn(async (data: Uint8Array) => { + const cid = `mock-cid-${Date.now()}`; + this.content.set(cid, data); + return { toString: () => cid }; + }), + cat: jest.fn(async function* (cid: any) { + const data = this.content.get(cid.toString()); + if (data) { + yield data; + } + }.bind(this)), + addFile: jest.fn(async (file: any) => { + const cid = `mock-file-cid-${Date.now()}`; + return { toString: () => cid }; + }) + }; + } +} + +export const createHelia = jest.fn(async (options: any = {}) => { + const helia = new MockHelia(); + await helia.start(); + return helia; +}); + +export const createLibp2p = jest.fn(async (options: any = {}) => { + return new MockLibp2p(); +}); + +// Mock IPFS service for framework +export class MockIPFSService { + private helia: MockHelia; + + constructor() { + this.helia = new MockHelia(); + } + + async init() { + await this.helia.start(); + } + + async stop() { + await this.helia.stop(); + } + + getHelia() { + return this.helia; + } + + getLibp2pInstance() { + return this.helia.libp2p; + } + + async getConnectedPeers() { + const peers = this.helia.libp2p.getPeers(); + const peerMap = new Map(); + peers.forEach(peer => peerMap.set(peer, peer)); + return peerMap; + } + + async pinOnNode(nodeId: string, cid: string) { + await this.helia.pins.add(cid); + } + + get pubsub() { + return { + publish: jest.fn(async (topic: string, data: string) => { + await this.helia.libp2p.pubsub.publish(topic, new TextEncoder().encode(data)); + }), + subscribe: jest.fn(async (topic: string, handler: Function) => { + // Mock subscribe + }), + unsubscribe: jest.fn(async (topic: string) => { + // Mock unsubscribe + }) + }; + } +} + +// Mock OrbitDB service for framework +export class MockOrbitDBService { + private orbitdb: any; + + constructor() { + this.orbitdb = new (require('./orbitdb').MockOrbitDB)(); + } + + async init() { + await this.orbitdb.start(); + } + + async stop() { + await this.orbitdb.stop(); + } + + async openDB(name: string, type: string) { + return await this.orbitdb.open(name, { type }); + } + + async openDatabase(name: string, type: string) { + return await this.openDB(name, type); + } + + getOrbitDB() { + return this.orbitdb; + } +} + +// Default export +export default { + createHelia, + createLibp2p, + MockHelia, + MockLibp2p, + MockIPFSService, + MockOrbitDBService +}; \ No newline at end of file diff --git a/tests/mocks/orbitdb.ts b/tests/mocks/orbitdb.ts new file mode 100644 index 0000000..f016712 --- /dev/null +++ b/tests/mocks/orbitdb.ts @@ -0,0 +1,154 @@ +// Mock OrbitDB for testing +export class MockOrbitDB { + private databases = new Map(); + private isOpen = false; + + async open(name: string, options: any = {}) { + if (!this.databases.has(name)) { + this.databases.set(name, new MockDatabase(name, options)); + } + return this.databases.get(name); + } + + async stop() { + this.isOpen = false; + for (const db of this.databases.values()) { + await db.close(); + } + } + + async start() { + this.isOpen = true; + } + + get address() { + return 'mock-orbitdb-address'; + } +} + +export class MockDatabase { + private data = new Map(); + private _events: Array<{ type: string; payload: any }> = []; + public name: string; + public type: string; + + constructor(name: string, options: any = {}) { + this.name = name; + this.type = options.type || 'docstore'; + } + + // DocStore methods + async put(doc: any, options?: any) { + const id = doc._id || doc.id || this.generateId(); + const record = { ...doc, _id: id }; + this.data.set(id, record); + this._events.push({ type: 'write', payload: record }); + return id; + } + + async get(id: string) { + return this.data.get(id) || null; + } + + async del(id: string) { + const deleted = this.data.delete(id); + if (deleted) { + this._events.push({ type: 'delete', payload: { _id: id } }); + } + return deleted; + } + + async query(filter?: (doc: any) => boolean) { + const docs = Array.from(this.data.values()); + return filter ? docs.filter(filter) : docs; + } + + async all() { + return Array.from(this.data.values()); + } + + // EventLog methods + async add(data: any) { + const entry = { + payload: data, + hash: this.generateId(), + clock: { time: Date.now() } + }; + this._events.push(entry); + return entry.hash; + } + + async iterator(options?: any) { + const events = this._events.slice(); + return { + collect: () => events, + [Symbol.iterator]: function* () { + for (const event of events) { + yield event; + } + } + }; + } + + // KeyValue methods + async set(key: string, value: any) { + this.data.set(key, value); + this._events.push({ type: 'put', payload: { key, value } }); + return key; + } + + // Counter methods + async inc(amount: number = 1) { + const current = this.data.get('counter') || 0; + const newValue = current + amount; + this.data.set('counter', newValue); + this._events.push({ type: 'increment', payload: { amount, value: newValue } }); + return newValue; + } + + get value() { + return this.data.get('counter') || 0; + } + + // General methods + async close() { + // Mock close + } + + async drop() { + this.data.clear(); + this._events = []; + } + + get address() { + return `mock-db-${this.name}`; + } + + get events() { + return this._events; + } + + // Event emitter mock + on(event: string, callback: Function) { + // Mock event listener + } + + off(event: string, callback: Function) { + // Mock event listener removal + } + + private generateId(): string { + return `mock-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } +} + +export const createOrbitDB = jest.fn(async (options: any) => { + return new MockOrbitDB(); +}); + +// Default export for ES modules +export default { + createOrbitDB, + MockOrbitDB, + MockDatabase +}; \ No newline at end of file diff --git a/tests/mocks/services.ts b/tests/mocks/services.ts new file mode 100644 index 0000000..a7fc64c --- /dev/null +++ b/tests/mocks/services.ts @@ -0,0 +1,35 @@ +// Mock services factory for testing +import { MockIPFSService, MockOrbitDBService } from './ipfs'; + +export function createMockServices() { + const ipfsService = new MockIPFSService(); + const orbitDBService = new MockOrbitDBService(); + + return { + ipfsService, + orbitDBService, + async initialize() { + await ipfsService.init(); + await orbitDBService.init(); + }, + async cleanup() { + await ipfsService.stop(); + await orbitDBService.stop(); + } + }; +} + +// Test utilities +export function createMockDatabase() { + const { MockDatabase } = require('./orbitdb'); + return new MockDatabase('test-db', { type: 'docstore' }); +} + +export function createMockRecord(overrides: any = {}) { + return { + id: `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + createdAt: Date.now(), + updatedAt: Date.now(), + ...overrides + }; +} \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/README.md b/tests/real-integration/blog-scenario/README.md new file mode 100644 index 0000000..a83d08e --- /dev/null +++ b/tests/real-integration/blog-scenario/README.md @@ -0,0 +1,367 @@ +# Blog Scenario - Real Integration Tests + +This directory contains comprehensive Docker-based integration tests for the DebrosFramework blog scenario. These tests validate real-world functionality including IPFS private swarm networking, cross-node data synchronization, and complete blog workflow operations. + +## Overview + +The blog scenario tests a complete blogging platform built on DebrosFramework, including: + +- **User Management**: Registration, authentication, profile management +- **Content Creation**: Categories, posts, drafts, publishing +- **Comment System**: Comments, replies, moderation, engagement +- **Cross-Node Sync**: Data consistency across multiple nodes +- **Network Resilience**: Peer connections, private swarm functionality + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Blog Node 1 │ │ Blog Node 2 │ │ Blog Node 3 │ +│ Port: 3001 │◄──►│ Port: 3002 │◄──►│ Port: 3003 │ +│ IPFS: 4011 │ │ IPFS: 4012 │ │ IPFS: 4013 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────┼───────────────────────┘ + │ + ┌─────────────────┐ + │ Bootstrap Node │ + │ IPFS: 4001 │ + │ Private Swarm │ + └─────────────────┘ +``` + +## Test Structure + +``` +blog-scenario/ +├── docker/ +│ ├── docker-compose.blog.yml # Docker orchestration +│ ├── Dockerfile.blog-api # Blog API server image +│ ├── Dockerfile.bootstrap # IPFS bootstrap node +│ ├── Dockerfile.test-runner # Test execution environment +│ ├── blog-api-server.ts # Blog API implementation +│ ├── bootstrap-config.sh # Bootstrap node configuration +│ └── swarm.key # Private IPFS swarm key +├── models/ +│ ├── BlogModels.ts # User, Post, Comment, Category models +│ └── BlogValidation.ts # Input validation and sanitization +├── scenarios/ +│ └── BlogTestRunner.ts # Test execution utilities +├── tests/ +│ └── blog-workflow.test.ts # Main test suite +├── run-tests.ts # Test orchestration script +└── README.md # This file +``` + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose installed +- Node.js 18+ for development +- At least 8GB RAM (recommended for multiple nodes) +- Available ports: 3001-3003, 4001, 4011-4013 + +### Running Tests + +#### Option 1: Full Docker-based Test (Recommended) + +```bash +# Run complete integration tests +pnpm run test:blog-real + +# Or use the test runner for better control +pnpm run test:blog-runner +``` + +#### Option 2: Build and Run Manually + +```bash +# Build Docker images +pnpm run test:blog-build + +# Run tests +pnpm run test:blog-real + +# Clean up afterwards +pnpm run test:blog-clean +``` + +#### Option 3: Development Mode + +```bash +# Start services only (for debugging) +cd tests/real-integration/blog-scenario +docker-compose -f docker/docker-compose.blog.yml up blog-node-1 blog-node-2 blog-node-3 + +# Run tests against running services +pnpm run test:blog-integration +``` + +## Test Scenarios + +### 1. User Management Workflow + +- ✅ Cross-node user creation and synchronization +- ✅ User profile updates across nodes +- ✅ User authentication state management + +### 2. Category Management + +- ✅ Category creation and sync +- ✅ Slug generation and uniqueness +- ✅ Category hierarchy support + +### 3. Content Publishing Workflow + +- ✅ Draft post creation +- ✅ Post publishing/unpublishing +- ✅ Cross-node content synchronization +- ✅ Post engagement (views, likes) +- ✅ Content relationships (author, category) + +### 4. Comment System + +- ✅ Distributed comment creation +- ✅ Nested comments (replies) +- ✅ Comment moderation +- ✅ Comment engagement + +### 5. Performance & Scalability + +- ✅ Concurrent operations across nodes +- ✅ Data consistency under load +- ✅ Network resilience testing + +### 6. Network Tests + +- ✅ Private IPFS swarm functionality +- ✅ Peer discovery and connections +- ✅ Data replication verification + +## API Endpoints + +Each blog node exposes a REST API: + +### Users + +- `POST /api/users` - Create user +- `GET /api/users/:id` - Get user by ID +- `GET /api/users` - List users (with pagination) +- `PUT /api/users/:id` - Update user +- `POST /api/users/:id/login` - Record login + +### Categories + +- `POST /api/categories` - Create category +- `GET /api/categories` - List categories +- `GET /api/categories/:id` - Get category by ID + +### Posts + +- `POST /api/posts` - Create post +- `GET /api/posts/:id` - Get post with relationships +- `GET /api/posts` - List posts (with filters) +- `PUT /api/posts/:id` - Update post +- `POST /api/posts/:id/publish` - Publish post +- `POST /api/posts/:id/unpublish` - Unpublish post +- `POST /api/posts/:id/like` - Like post +- `POST /api/posts/:id/view` - Increment views + +### Comments + +- `POST /api/comments` - Create comment +- `GET /api/posts/:postId/comments` - Get post comments +- `POST /api/comments/:id/approve` - Approve comment +- `POST /api/comments/:id/like` - Like comment + +### Metrics + +- `GET /health` - Node health status +- `GET /api/metrics/network` - Network metrics +- `GET /api/metrics/data` - Data count metrics +- `GET /api/metrics/framework` - Framework metrics + +## Configuration + +### Environment Variables + +Each node supports these environment variables: + +```bash +NODE_ID=blog-node-1 # Unique node identifier +NODE_PORT=3000 # HTTP API port +IPFS_PORT=4001 # IPFS swarm port +BOOTSTRAP_PEER=blog-bootstrap # Bootstrap node hostname +SWARM_KEY_FILE=/data/swarm.key # Private swarm key path +NODE_ENV=test # Environment mode +``` + +### Private IPFS Swarm + +The tests use a private IPFS swarm with a shared key to ensure: + +- ✅ Network isolation from public IPFS +- ✅ Controlled peer discovery +- ✅ Predictable network topology +- ✅ Enhanced security for testing + +## Monitoring and Debugging + +### View Logs + +```bash +# Follow all container logs +docker-compose -f docker/docker-compose.blog.yml logs -f + +# Follow specific service logs +docker-compose -f docker/docker-compose.blog.yml logs -f blog-node-1 +``` + +### Check Node Status + +```bash +# Health check +curl http://localhost:3001/health +curl http://localhost:3002/health +curl http://localhost:3003/health + +# Network metrics +curl http://localhost:3001/api/metrics/network + +# Data metrics +curl http://localhost:3001/api/metrics/data +``` + +### Connect to Running Containers + +```bash +# Access blog node shell +docker-compose -f docker/docker-compose.blog.yml exec blog-node-1 sh + +# Check IPFS status +docker-compose -f docker/docker-compose.blog.yml exec blog-bootstrap ipfs swarm peers +``` + +## Test Data + +The tests automatically generate realistic test data: + +- **Users**: Various user roles (author, editor, user) +- **Categories**: Technology, Design, Business, etc. +- **Posts**: Different statuses (draft, published, archived) +- **Comments**: Including nested replies and engagement + +## Performance Expectations + +Based on the test configuration: + +- **Node Startup**: < 60 seconds for all nodes +- **Peer Discovery**: < 30 seconds for full mesh +- **Data Sync**: < 15 seconds for typical operations +- **Concurrent Operations**: 20+ simultaneous requests +- **Test Execution**: 5-10 minutes for full suite + +## Troubleshooting + +### Common Issues + +#### Ports Already in Use + +```bash +# Check port usage +lsof -i :3001-3003 +lsof -i :4001 +lsof -i :4011-4013 + +# Clean up existing containers +pnpm run test:blog-clean +``` + +#### Docker Build Failures + +```bash +# Clean Docker cache +docker system prune -f + +# Rebuild without cache +docker-compose -f docker/docker-compose.blog.yml build --no-cache +``` + +#### Node Connection Issues + +```bash +# Check network connectivity +docker network ls +docker network inspect blog-scenario_blog-network + +# Verify swarm key consistency +docker-compose -f docker/docker-compose.blog.yml exec blog-node-1 cat /data/swarm.key +``` + +#### Test Timeouts + +```bash +# Increase test timeout in jest.config.js or test files +# Monitor resource usage +docker stats + +# Check available memory and CPU +free -h +``` + +### Debug Mode + +To run tests with additional debugging: + +```bash +# Set debug environment +DEBUG=* pnpm run test:blog-real + +# Run with increased verbosity +LOG_LEVEL=debug pnpm run test:blog-real +``` + +## Development + +### Adding New Tests + +1. Add test cases to `tests/blog-workflow.test.ts` +2. Extend `BlogTestRunner` with new utilities +3. Update models if needed in `models/` +4. Test locally before CI integration + +### Modifying API + +1. Update `blog-api-server.ts` +2. Add corresponding validation in `BlogValidation.ts` +3. Update test scenarios +4. Rebuild Docker images + +### Performance Tuning + +1. Adjust timeouts in test configuration +2. Modify Docker resource limits +3. Optimize IPFS/OrbitDB configuration +4. Scale node count as needed + +## Next Steps + +This blog scenario provides a foundation for: + +1. **Social Scenario**: User relationships, feeds, messaging +2. **E-commerce Scenario**: Products, orders, payments +3. **Collaborative Scenario**: Real-time editing, conflict resolution +4. **Performance Testing**: Load testing, stress testing +5. **Security Testing**: Attack scenarios, validation testing + +The modular design allows easy extension to new scenarios while reusing the infrastructure components. + +## Support + +For issues or questions: + +1. Check the troubleshooting section above +2. Review Docker and test logs +3. Verify your environment meets prerequisites +4. Open an issue with detailed logs and configuration diff --git a/tests/real-integration/blog-scenario/docker/Dockerfile.blog-api b/tests/real-integration/blog-scenario/docker/Dockerfile.blog-api new file mode 100644 index 0000000..58a3369 --- /dev/null +++ b/tests/real-integration/blog-scenario/docker/Dockerfile.blog-api @@ -0,0 +1,49 @@ +# Blog API Node +FROM node:18-alpine + +# Install system dependencies including build tools for native modules +RUN apk add --no-cache \ + curl \ + python3 \ + make \ + g++ \ + git \ + cmake \ + pkgconfig \ + libc6-compat \ + linux-headers + +# Create app directory +WORKDIR /app + +# Copy package files +COPY package*.json pnpm-lock.yaml ./ + +# Install pnpm +RUN npm install -g pnpm + +# Install full dependencies and reflect-metadata +RUN pnpm install --frozen-lockfile \ + && pnpm add reflect-metadata @babel/runtime + +# Install tsx globally for running TypeScript files (better ESM support) +RUN npm install -g tsx + +# Copy source code +COPY . . + +# Build the application +RUN pnpm run build + +# Create data directory +RUN mkdir -p /data + +# Expose API port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:3000/health || exit 1 + +# Start the blog API server using tsx with explicit tsconfig +CMD ["tsx", "--tsconfig", "tests/real-integration/blog-scenario/docker/tsconfig.docker.json", "tests/real-integration/blog-scenario/docker/blog-api-server.ts"] diff --git a/tests/real-integration/blog-scenario/docker/Dockerfile.bootstrap b/tests/real-integration/blog-scenario/docker/Dockerfile.bootstrap new file mode 100644 index 0000000..7913d70 --- /dev/null +++ b/tests/real-integration/blog-scenario/docker/Dockerfile.bootstrap @@ -0,0 +1,30 @@ +# Bootstrap node for IPFS peer discovery +FROM node:18-alpine + +# Install dependencies +RUN apk add --no-cache curl jq + +# Create app directory +WORKDIR /app + +# Install IPFS +RUN wget https://dist.ipfs.tech/kubo/v0.24.0/kubo_v0.24.0_linux-amd64.tar.gz \ + && tar -xzf kubo_v0.24.0_linux-amd64.tar.gz \ + && mv kubo/ipfs /usr/local/bin/ \ + && rm -rf kubo kubo_v0.24.0_linux-amd64.tar.gz + +# Copy swarm key +COPY tests/real-integration/blog-scenario/docker/swarm.key /data/swarm.key + +# Initialize IPFS +RUN ipfs init --profile=test + +# Copy configuration script +COPY tests/real-integration/blog-scenario/docker/bootstrap-config.sh /app/bootstrap-config.sh +RUN chmod +x /app/bootstrap-config.sh + +# Expose IPFS ports +EXPOSE 4001 5001 8080 + +# Start IPFS daemon +CMD ["/app/bootstrap-config.sh"] \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/docker/Dockerfile.test-runner b/tests/real-integration/blog-scenario/docker/Dockerfile.test-runner new file mode 100644 index 0000000..6980abc --- /dev/null +++ b/tests/real-integration/blog-scenario/docker/Dockerfile.test-runner @@ -0,0 +1,33 @@ +# Test Runner for Blog Integration Tests +FROM node:18-alpine + +# Install dependencies +RUN apk add --no-cache curl jq + +# Create app directory +WORKDIR /app + +# Copy package files +COPY package*.json pnpm-lock.yaml ./ + +# Install pnpm and tsx +RUN npm install -g pnpm tsx + +# Install all dependencies (including dev dependencies for testing, skip prepare script) +RUN pnpm install --frozen-lockfile --ignore-scripts + +# Copy source code +COPY . . + +# Build the application +RUN pnpm run build + +# Create results directory +RUN mkdir -p /app/results + +# Set environment variables +ENV NODE_ENV=test +ENV TEST_SCENARIO=blog + +# Default command (can be overridden) +CMD ["pnpm", "run", "test:blog-integration"] \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/docker/blog-api-server.ts b/tests/real-integration/blog-scenario/docker/blog-api-server.ts new file mode 100644 index 0000000..fab5e90 --- /dev/null +++ b/tests/real-integration/blog-scenario/docker/blog-api-server.ts @@ -0,0 +1,690 @@ +#!/usr/bin/env node + +// Import reflect-metadata first for decorator support +import 'reflect-metadata'; + +// Polyfill CustomEvent for Node.js environment +if (typeof globalThis.CustomEvent === 'undefined') { + globalThis.CustomEvent = class CustomEvent extends Event { + detail: T; + + constructor(type: string, eventInitDict?: CustomEventInit) { + super(type, eventInitDict); + this.detail = eventInitDict?.detail; + } + } as any; +} + +import express from 'express'; +import { DebrosFramework } from '../../../../src/framework/DebrosFramework'; +import { User, UserProfile, Category, Post, Comment } from '../models/BlogModels'; +import { BlogValidation, ValidationError } from '../models/BlogValidation'; + +class BlogAPIServer { + private app: express.Application; + private framework: DebrosFramework; + private nodeId: string; + + constructor() { + this.app = express(); + this.nodeId = process.env.NODE_ID || 'blog-node'; + this.setupMiddleware(); + this.setupRoutes(); + } + + private setupMiddleware() { + this.app.use(express.json({ limit: '10mb' })); + this.app.use(express.urlencoded({ extended: true })); + + // CORS + this.app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + if (req.method === 'OPTIONS') { + res.sendStatus(200); + } else { + next(); + } + }); + + // Logging + this.app.use((req, res, next) => { + console.log(`[${this.nodeId}] ${new Date().toISOString()} ${req.method} ${req.path}`); + next(); + }); + + // Error handling + this.app.use( + (error: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error(`[${this.nodeId}] Error:`, error); + + if (error instanceof ValidationError) { + return res.status(400).json({ + error: error.message, + field: error.field, + nodeId: this.nodeId, + }); + } + + res.status(500).json({ + error: 'Internal server error', + nodeId: this.nodeId, + }); + }, + ); + } + + private setupRoutes() { + // Health check + this.app.get('/health', async (req, res) => { + try { + const peers = await this.getConnectedPeerCount(); + res.json({ + status: 'healthy', + nodeId: this.nodeId, + peers, + timestamp: Date.now(), + }); + } catch (error) { + res.status(500).json({ + status: 'unhealthy', + nodeId: this.nodeId, + error: error.message, + }); + } + }); + + // API routes + this.setupUserRoutes(); + this.setupCategoryRoutes(); + this.setupPostRoutes(); + this.setupCommentRoutes(); + this.setupMetricsRoutes(); + } + + private setupUserRoutes() { + // Create user + this.app.post('/api/users', async (req, res, next) => { + try { + const sanitizedData = BlogValidation.sanitizeUserInput(req.body); + BlogValidation.validateUser(sanitizedData); + + const user = await User.create(sanitizedData); + + console.log(`[${this.nodeId}] Created user: ${user.username} (${user.id})`); + res.status(201).json(user.toJSON()); + } catch (error) { + next(error); + } + }); + + // Get user by ID + this.app.get('/api/users/:id', async (req, res, next) => { + try { + const user = await User.findById(req.params.id); + if (!user) { + return res.status(404).json({ + error: 'User not found', + nodeId: this.nodeId, + }); + } + res.json(user.toJSON()); + } catch (error) { + next(error); + } + }); + + // Get all users + this.app.get('/api/users', async (req, res, next) => { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); + const search = req.query.search as string; + + let query = User.query(); + + if (search) { + query = query + .where('username', 'like', `%${search}%`) + .orWhere('displayName', 'like', `%${search}%`); + } + + const users = await query + .orderBy('createdAt', 'desc') + .limit(limit) + .offset((page - 1) * limit) + .find(); + + res.json({ + users: users.map((u) => u.toJSON()), + page, + limit, + nodeId: this.nodeId, + }); + } catch (error) { + next(error); + } + }); + + // Update user + this.app.put('/api/users/:id', async (req, res, next) => { + try { + const user = await User.findById(req.params.id); + if (!user) { + return res.status(404).json({ + error: 'User not found', + nodeId: this.nodeId, + }); + } + + // Only allow updating certain fields + const allowedFields = ['displayName', 'avatar', 'roles']; + const updateData: any = {}; + + allowedFields.forEach((field) => { + if (req.body[field] !== undefined) { + updateData[field] = req.body[field]; + } + }); + + Object.assign(user, updateData); + await user.save(); + + console.log(`[${this.nodeId}] Updated user: ${user.username}`); + res.json(user.toJSON()); + } catch (error) { + next(error); + } + }); + + // User login (update last login) + this.app.post('/api/users/:id/login', async (req, res, next) => { + try { + const user = await User.findById(req.params.id); + if (!user) { + return res.status(404).json({ + error: 'User not found', + nodeId: this.nodeId, + }); + } + + await user.updateLastLogin(); + res.json({ message: 'Login recorded', lastLoginAt: user.lastLoginAt }); + } catch (error) { + next(error); + } + }); + } + + private setupCategoryRoutes() { + // Create category + this.app.post('/api/categories', async (req, res, next) => { + try { + const sanitizedData = BlogValidation.sanitizeCategoryInput(req.body); + BlogValidation.validateCategory(sanitizedData); + + const category = await Category.create(sanitizedData); + + console.log(`[${this.nodeId}] Created category: ${category.name} (${category.id})`); + res.status(201).json(category); + } catch (error) { + next(error); + } + }); + + // Get all categories + this.app.get('/api/categories', async (req, res, next) => { + try { + const categories = await Category.query() + .where('isActive', true) + .orderBy('name', 'asc') + .find(); + + res.json({ + categories, + nodeId: this.nodeId, + }); + } catch (error) { + next(error); + } + }); + + // Get category by ID + this.app.get('/api/categories/:id', async (req, res, next) => { + try { + const category = await Category.findById(req.params.id); + if (!category) { + return res.status(404).json({ + error: 'Category not found', + nodeId: this.nodeId, + }); + } + res.json(category); + } catch (error) { + next(error); + } + }); + } + + private setupPostRoutes() { + // Create post + this.app.post('/api/posts', async (req, res, next) => { + try { + const sanitizedData = BlogValidation.sanitizePostInput(req.body); + BlogValidation.validatePost(sanitizedData); + + const post = await Post.create(sanitizedData); + + console.log(`[${this.nodeId}] Created post: ${post.title} (${post.id})`); + res.status(201).json(post); + } catch (error) { + next(error); + } + }); + + // Get post by ID with relationships + this.app.get('/api/posts/:id', async (req, res, next) => { + try { + const post = await Post.query() + .where('id', req.params.id) + .with(['author', 'category', 'comments']) + .first(); + + if (!post) { + return res.status(404).json({ + error: 'Post not found', + nodeId: this.nodeId, + }); + } + + res.json(post); + } catch (error) { + next(error); + } + }); + + // Get all posts with pagination and filters + this.app.get('/api/posts', async (req, res, next) => { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = Math.min(parseInt(req.query.limit as string) || 10, 50); + const status = req.query.status as string; + const authorId = req.query.authorId as string; + const categoryId = req.query.categoryId as string; + const tag = req.query.tag as string; + + let query = Post.query().with(['author', 'category']); + + if (status) { + query = query.where('status', status); + } + + if (authorId) { + query = query.where('authorId', authorId); + } + + if (categoryId) { + query = query.where('categoryId', categoryId); + } + + if (tag) { + query = query.where('tags', 'includes', tag); + } + + const posts = await query + .orderBy('createdAt', 'desc') + .limit(limit) + .offset((page - 1) * limit) + .find(); + + res.json({ + posts, + page, + limit, + nodeId: this.nodeId, + }); + } catch (error) { + next(error); + } + }); + + // Update post + this.app.put('/api/posts/:id', async (req, res, next) => { + try { + const post = await Post.findById(req.params.id); + if (!post) { + return res.status(404).json({ + error: 'Post not found', + nodeId: this.nodeId, + }); + } + + BlogValidation.validatePostUpdate(req.body); + + Object.assign(post, req.body); + post.updatedAt = Date.now(); + await post.save(); + + console.log(`[${this.nodeId}] Updated post: ${post.title}`); + res.json(post); + } catch (error) { + next(error); + } + }); + + // Publish post + this.app.post('/api/posts/:id/publish', async (req, res, next) => { + try { + const post = await Post.findById(req.params.id); + if (!post) { + return res.status(404).json({ + error: 'Post not found', + nodeId: this.nodeId, + }); + } + + await post.publish(); + console.log(`[${this.nodeId}] Published post: ${post.title}`); + res.json(post); + } catch (error) { + next(error); + } + }); + + // Unpublish post + this.app.post('/api/posts/:id/unpublish', async (req, res, next) => { + try { + const post = await Post.findById(req.params.id); + if (!post) { + return res.status(404).json({ + error: 'Post not found', + nodeId: this.nodeId, + }); + } + + await post.unpublish(); + console.log(`[${this.nodeId}] Unpublished post: ${post.title}`); + res.json(post); + } catch (error) { + next(error); + } + }); + + // Like post + this.app.post('/api/posts/:id/like', async (req, res, next) => { + try { + const post = await Post.findById(req.params.id); + if (!post) { + return res.status(404).json({ + error: 'Post not found', + nodeId: this.nodeId, + }); + } + + await post.like(); + res.json({ likeCount: post.likeCount }); + } catch (error) { + next(error); + } + }); + + // View post (increment view count) + this.app.post('/api/posts/:id/view', async (req, res, next) => { + try { + const post = await Post.findById(req.params.id); + if (!post) { + return res.status(404).json({ + error: 'Post not found', + nodeId: this.nodeId, + }); + } + + await post.incrementViews(); + res.json({ viewCount: post.viewCount }); + } catch (error) { + next(error); + } + }); + } + + private setupCommentRoutes() { + // Create comment + this.app.post('/api/comments', async (req, res, next) => { + try { + const sanitizedData = BlogValidation.sanitizeCommentInput(req.body); + BlogValidation.validateComment(sanitizedData); + + const comment = await Comment.create(sanitizedData); + + console.log( + `[${this.nodeId}] Created comment on post ${comment.postId} by ${comment.authorId}`, + ); + res.status(201).json(comment); + } catch (error) { + next(error); + } + }); + + // Get comments for a post + this.app.get('/api/posts/:postId/comments', async (req, res, next) => { + try { + const comments = await Comment.query() + .where('postId', req.params.postId) + .where('isApproved', true) + .with(['author']) + .orderBy('createdAt', 'asc') + .find(); + + res.json({ + comments, + nodeId: this.nodeId, + }); + } catch (error) { + next(error); + } + }); + + // Approve comment + this.app.post('/api/comments/:id/approve', async (req, res, next) => { + try { + const comment = await Comment.findById(req.params.id); + if (!comment) { + return res.status(404).json({ + error: 'Comment not found', + nodeId: this.nodeId, + }); + } + + await comment.approve(); + console.log(`[${this.nodeId}] Approved comment ${comment.id}`); + res.json(comment); + } catch (error) { + next(error); + } + }); + + // Like comment + this.app.post('/api/comments/:id/like', async (req, res, next) => { + try { + const comment = await Comment.findById(req.params.id); + if (!comment) { + return res.status(404).json({ + error: 'Comment not found', + nodeId: this.nodeId, + }); + } + + await comment.like(); + res.json({ likeCount: comment.likeCount }); + } catch (error) { + next(error); + } + }); + } + + private setupMetricsRoutes() { + // Network metrics + this.app.get('/api/metrics/network', async (req, res, next) => { + try { + const peers = await this.getConnectedPeerCount(); + res.json({ + nodeId: this.nodeId, + peers, + timestamp: Date.now(), + }); + } catch (error) { + next(error); + } + }); + + // Data metrics + this.app.get('/api/metrics/data', async (req, res, next) => { + try { + const [userCount, postCount, commentCount, categoryCount] = await Promise.all([ + User.count(), + Post.count(), + Comment.count(), + Category.count(), + ]); + + res.json({ + nodeId: this.nodeId, + counts: { + users: userCount, + posts: postCount, + comments: commentCount, + categories: categoryCount, + }, + timestamp: Date.now(), + }); + } catch (error) { + next(error); + } + }); + + // Framework metrics + this.app.get('/api/metrics/framework', async (req, res, next) => { + try { + const metrics = this.framework.getMetrics(); + res.json({ + nodeId: this.nodeId, + ...metrics, + timestamp: Date.now(), + }); + } catch (error) { + next(error); + } + }); + } + + private async getConnectedPeerCount(): Promise { + try { + if (this.framework) { + const ipfsService = this.framework.getIPFSService(); + if (ipfsService && ipfsService.getConnectedPeers) { + const peers = await ipfsService.getConnectedPeers(); + return peers.size; + } + } + return 0; + } catch (error) { + console.warn(`[${this.nodeId}] Failed to get peer count:`, error.message); + return 0; + } + } + + async start() { + try { + console.log(`[${this.nodeId}] Starting Blog API Server...`); + + // Wait for dependencies + await this.waitForDependencies(); + + // Initialize framework + await this.initializeFramework(); + + // Start HTTP server + const port = process.env.NODE_PORT || 3000; + this.app.listen(port, () => { + console.log(`[${this.nodeId}] Blog API server listening on port ${port}`); + console.log(`[${this.nodeId}] Health check: http://localhost:${port}/health`); + }); + } catch (error) { + console.error(`[${this.nodeId}] Failed to start:`, error); + process.exit(1); + } + } + + private async waitForDependencies(): Promise { + // In a real deployment, you might wait for database connections, etc. + console.log(`[${this.nodeId}] Dependencies ready`); + } + + private async initializeFramework(): Promise { + // Import services + const { IPFSService } = await import('../../../../src/framework/services/IPFSService'); + const { OrbitDBService } = await import( + '../../../../src/framework/services/RealOrbitDBService' + ); + const { FrameworkIPFSService, FrameworkOrbitDBService } = await import( + '../../../../src/framework/services/OrbitDBService' + ); + + // Initialize IPFS service + const ipfsService = new IPFSService({ + swarmKeyFile: process.env.SWARM_KEY_FILE, + bootstrap: process.env.BOOTSTRAP_PEER ? [`/ip4/${process.env.BOOTSTRAP_PEER}/tcp/4001`] : [], + ports: { + swarm: parseInt(process.env.IPFS_PORT) || 4001, + }, + }); + + await ipfsService.init(); + console.log(`[${this.nodeId}] IPFS service initialized`); + + // Initialize OrbitDB service + const orbitDBService = new OrbitDBService(ipfsService); + await orbitDBService.init(); + console.log(`[${this.nodeId}] OrbitDB service initialized`); + + // Wrap services for framework + const frameworkIPFS = new FrameworkIPFSService(ipfsService); + const frameworkOrbitDB = new FrameworkOrbitDBService(orbitDBService); + + // Initialize framework + this.framework = new DebrosFramework({ + environment: 'test', + features: { + autoMigration: true, + automaticPinning: true, + pubsub: true, + queryCache: true, + relationshipCache: true, + }, + performance: { + queryTimeout: 10000, + maxConcurrentOperations: 20, + batchSize: 50, + }, + }); + + await this.framework.initialize(orbitDBService, ipfsService); + console.log(`[${this.nodeId}] DebrosFramework initialized successfully`); + } +} + +// Handle graceful shutdown +process.on('SIGTERM', () => { + console.log('Received SIGTERM, shutting down gracefully...'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('Received SIGINT, shutting down gracefully...'); + process.exit(0); +}); + +// Start the server +const server = new BlogAPIServer(); +server.start(); diff --git a/tests/real-integration/blog-scenario/docker/bootstrap-config.sh b/tests/real-integration/blog-scenario/docker/bootstrap-config.sh new file mode 100644 index 0000000..41a424e --- /dev/null +++ b/tests/real-integration/blog-scenario/docker/bootstrap-config.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +echo "Configuring bootstrap IPFS node..." + +# Set IPFS path +export IPFS_PATH=/data/ipfs + +# Copy swarm key for private network +if [ -f "/data/ipfs/swarm.key" ]; then + echo "Using existing swarm key" +else + echo "Swarm key not found" + exit 1 +fi + +# Configure IPFS for private network +ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' +ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]' +ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]' + +# Remove default bootstrap nodes (for private network) +ipfs bootstrap rm --all + +# Configure addresses +ipfs config Addresses.API "/ip4/0.0.0.0/tcp/5001" +ipfs config Addresses.Gateway "/ip4/0.0.0.0/tcp/8080" +ipfs config --json Addresses.Swarm '["/ip4/0.0.0.0/tcp/4001"]' + +# Enable PubSub +ipfs config --json Pubsub.Enabled true + +echo "Starting IPFS daemon..." +exec ipfs daemon --enable-gc \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/docker/docker-compose.blog.yml b/tests/real-integration/blog-scenario/docker/docker-compose.blog.yml new file mode 100644 index 0000000..75ae57f --- /dev/null +++ b/tests/real-integration/blog-scenario/docker/docker-compose.blog.yml @@ -0,0 +1,155 @@ + +services: + # Bootstrap node for peer discovery + blog-bootstrap: + build: + context: ../../../../ + dockerfile: tests/real-integration/shared/infrastructure/docker/Dockerfile.bootstrap + environment: + - NODE_TYPE=bootstrap + - NODE_ID=blog-bootstrap + - SWARM_KEY_FILE=/data/swarm.key + volumes: + - ./swarm.key:/data/swarm.key:ro + - bootstrap-data:/data/ipfs + networks: + - blog-network + ports: + - "4001:4001" + healthcheck: + test: ["CMD", "sh", "-c", "ipfs id >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 5 + + # Blog API Node 1 + blog-node-1: + build: + context: ../../../../ + dockerfile: tests/real-integration/blog-scenario/docker/Dockerfile.blog-api + depends_on: + blog-bootstrap: + condition: service_healthy + environment: + - NODE_ID=blog-node-1 + - NODE_PORT=3000 + - IPFS_PORT=4011 + - BOOTSTRAP_PEER=blog-bootstrap + - SWARM_KEY_FILE=/data/swarm.key + - NODE_ENV=test + ports: + - "3001:3000" + - "4011:4011" + volumes: + - ./swarm.key:/data/swarm.key:ro + - blog-node-1-data:/data + networks: + - blog-network + healthcheck: + test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1"] + interval: 15s + timeout: 10s + retries: 10 + start_period: 30s + + # Blog API Node 2 + blog-node-2: + build: + context: ../../../../ + dockerfile: tests/real-integration/blog-scenario/docker/Dockerfile.blog-api + depends_on: + blog-bootstrap: + condition: service_healthy + environment: + - NODE_ID=blog-node-2 + - NODE_PORT=3000 + - IPFS_PORT=4012 + - BOOTSTRAP_PEER=blog-bootstrap + - SWARM_KEY_FILE=/data/swarm.key + - NODE_ENV=test + ports: + - "3002:3000" + - "4012:4012" + volumes: + - ./swarm.key:/data/swarm.key:ro + - blog-node-2-data:/data + networks: + - blog-network + healthcheck: + test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1"] + interval: 15s + timeout: 10s + retries: 10 + start_period: 30s + + # Blog API Node 3 + blog-node-3: + build: + context: ../../../../ + dockerfile: tests/real-integration/blog-scenario/docker/Dockerfile.blog-api + depends_on: + blog-bootstrap: + condition: service_healthy + environment: + - NODE_ID=blog-node-3 + - NODE_PORT=3000 + - IPFS_PORT=4013 + - BOOTSTRAP_PEER=blog-bootstrap + - SWARM_KEY_FILE=/data/swarm.key + - NODE_ENV=test + ports: + - "3003:3000" + - "4013:4013" + volumes: + - ./swarm.key:/data/swarm.key:ro + - blog-node-3-data:/data + networks: + - blog-network + healthcheck: + test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1"] + interval: 15s + timeout: 10s + retries: 10 + start_period: 30s + + # Test Runner + blog-test-runner: + build: + context: ../../../../ + dockerfile: tests/real-integration/blog-scenario/docker/Dockerfile.test-runner + depends_on: + blog-node-1: + condition: service_healthy + blog-node-2: + condition: service_healthy + blog-node-3: + condition: service_healthy + environment: + - TEST_SCENARIO=blog + - NODE_ENDPOINTS=http://blog-node-1:3000,http://blog-node-2:3000,http://blog-node-3:3000 + - TEST_TIMEOUT=300000 + - NODE_ENV=test + volumes: + - test-results:/app/results + networks: + - blog-network + command: ["pnpm", "run", "test:blog-integration"] + +volumes: + bootstrap-data: + driver: local + blog-node-1-data: + driver: local + blog-node-2-data: + driver: local + blog-node-3-data: + driver: local + test-results: + driver: local + +networks: + blog-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/docker/swarm.key b/tests/real-integration/blog-scenario/docker/swarm.key new file mode 100644 index 0000000..cca8446 --- /dev/null +++ b/tests/real-integration/blog-scenario/docker/swarm.key @@ -0,0 +1,3 @@ +/key/swarm/psk/1.0.0/ +/base16/ +9c4b2a1b3e5c8d7f2e1a9c4b6d8f3e5c7a9b2d4f6e8c1a3b5d7f9e2c4a6b8d0f \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/docker/tsconfig.docker.json b/tests/real-integration/blog-scenario/docker/tsconfig.docker.json new file mode 100644 index 0000000..2ff5eac --- /dev/null +++ b/tests/real-integration/blog-scenario/docker/tsconfig.docker.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "useDefineForClassFields": false, + "strictPropertyInitialization": false, + "skipLibCheck": true, + "outDir": "dist", + "isolatedModules": true, + "removeComments": true, + "inlineSources": true, + "sourceMap": true, + "allowJs": true, + "strict": true, + "baseUrl": "../../../../" + }, + "include": ["blog-api-server.ts", "../../../../src/**/*"] +} diff --git a/tests/real-integration/blog-scenario/models/BlogModels.ts b/tests/real-integration/blog-scenario/models/BlogModels.ts new file mode 100644 index 0000000..4ef1e07 --- /dev/null +++ b/tests/real-integration/blog-scenario/models/BlogModels.ts @@ -0,0 +1,371 @@ +import 'reflect-metadata'; +import { BaseModel } from '../../../../src/framework/models/BaseModel'; +import { Model, Field, HasMany, BelongsTo, HasOne, BeforeCreate, AfterCreate } from '../../../../src/framework/models/decorators'; + +// User Profile Model +@Model({ + scope: 'global', + type: 'docstore' +}) +export class UserProfile extends BaseModel { + @Field({ type: 'string', required: true }) + userId: string; + + @Field({ type: 'string', required: false }) + bio?: string; + + @Field({ type: 'string', required: false }) + location?: string; + + @Field({ type: 'string', required: false }) + website?: string; + + @Field({ type: 'object', required: false }) + socialLinks?: { + twitter?: string; + github?: string; + linkedin?: string; + }; + + @Field({ type: 'array', required: false, default: [] }) + interests: string[]; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + createdAt: number; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + updatedAt: number; + + @BelongsTo(() => User, 'userId') + user: User; +} + +// User Model +@Model({ + scope: 'global', + type: 'docstore' +}) +export class User extends BaseModel { + @Field({ type: 'string', required: true, unique: true }) + username: string; + + @Field({ type: 'string', required: true, unique: true }) + email: string; + + @Field({ type: 'string', required: false }) + displayName?: string; + + @Field({ type: 'string', required: false }) + avatar?: string; + + @Field({ type: 'boolean', required: false, default: true }) + isActive: boolean; + + @Field({ type: 'array', required: false, default: [] }) + roles: string[]; + + @Field({ type: 'number', required: false }) + createdAt: number; + + @Field({ type: 'number', required: false }) + lastLoginAt?: number; + + @HasMany(() => Post, 'authorId') + posts: Post[]; + + @HasMany(() => Comment, 'authorId') + comments: Comment[]; + + @HasOne(() => UserProfile, 'userId') + profile: UserProfile; + + @BeforeCreate() + setTimestamps() { + this.createdAt = Date.now(); + } + + // Helper methods + async updateLastLogin(): Promise { + this.lastLoginAt = Date.now(); + await this.save(); + } + + toJSON() { + const json = super.toJSON(); + // Don't expose sensitive data in API responses + delete json.password; + return json; + } +} + +// Category Model +@Model({ + scope: 'global', + type: 'docstore' +}) +export class Category extends BaseModel { + @Field({ type: 'string', required: true, unique: true }) + name: string; + + @Field({ type: 'string', required: true, unique: true }) + slug: string; + + @Field({ type: 'string', required: false }) + description?: string; + + @Field({ type: 'string', required: false }) + color?: string; + + @Field({ type: 'boolean', required: false, default: true }) + isActive: boolean; + + @Field({ type: 'number', required: false, default: () => Date.now() }) + createdAt: number; + + @HasMany(() => Post, 'categoryId') + posts: Post[]; + + @BeforeCreate() + generateSlug() { + if (!this.slug && this.name) { + this.slug = this.name + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, ''); + } + } +} + +// Post Model +@Model({ + scope: 'user', + type: 'docstore' +}) +export class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true, unique: true }) + slug: string; + + @Field({ type: 'string', required: true }) + content: string; + + @Field({ type: 'string', required: false }) + excerpt?: string; + + @Field({ type: 'string', required: true }) + authorId: string; + + @Field({ type: 'string', required: false }) + categoryId?: string; + + @Field({ type: 'array', required: false, default: [] }) + tags: string[]; + + @Field({ type: 'string', required: false, default: 'draft' }) + status: 'draft' | 'published' | 'archived'; + + @Field({ type: 'string', required: false }) + featuredImage?: string; + + @Field({ type: 'boolean', required: false, default: false }) + isFeatured: boolean; + + @Field({ type: 'number', required: false, default: 0 }) + viewCount: number; + + @Field({ type: 'number', required: false, default: 0 }) + likeCount: number; + + @Field({ type: 'number', required: false }) + createdAt: number; + + @Field({ type: 'number', required: false }) + updatedAt: number; + + @Field({ type: 'number', required: false }) + publishedAt?: number; + + @BelongsTo(() => User, 'authorId') + author: User; + + @BelongsTo(() => Category, 'categoryId') + category: Category; + + @HasMany(() => Comment, 'postId') + comments: Comment[]; + + @BeforeCreate() + setTimestamps() { + const now = Date.now(); + this.createdAt = now; + this.updatedAt = now; + + // Generate slug before validation if missing + if (!this.slug && this.title) { + this.slug = this.title + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, ''); + } + } + + @AfterCreate() + finalizeSlug() { + // Add unique identifier to slug after creation to ensure uniqueness + if (this.slug && this.id) { + this.slug = this.slug + '-' + this.id.slice(-8); + } + } + + // Helper methods + async publish(): Promise { + this.status = 'published'; + this.publishedAt = Date.now(); + this.updatedAt = Date.now(); + await this.save(); + } + + async unpublish(): Promise { + this.status = 'draft'; + this.publishedAt = undefined; + this.updatedAt = Date.now(); + await this.save(); + } + + async incrementViews(): Promise { + this.viewCount += 1; + await this.save(); + } + + async like(): Promise { + this.likeCount += 1; + await this.save(); + } + + async unlike(): Promise { + if (this.likeCount > 0) { + this.likeCount -= 1; + await this.save(); + } + } + + async archive(): Promise { + this.status = 'archived'; + this.updatedAt = Date.now(); + await this.save(); + } +} + +// Comment Model +@Model({ + scope: 'user', + type: 'docstore' +}) +export class Comment extends BaseModel { + @Field({ type: 'string', required: true }) + content: string; + + @Field({ type: 'string', required: true }) + postId: string; + + @Field({ type: 'string', required: true }) + authorId: string; + + @Field({ type: 'string', required: false }) + parentId?: string; // For nested comments + + @Field({ type: 'boolean', required: false, default: true }) + isApproved: boolean; + + @Field({ type: 'number', required: false, default: 0 }) + likeCount: number; + + @Field({ type: 'number', required: false }) + createdAt: number; + + @Field({ type: 'number', required: false }) + updatedAt: number; + + @BelongsTo(() => Post, 'postId') + post: Post; + + @BelongsTo(() => User, 'authorId') + author: User; + + @BelongsTo(() => Comment, 'parentId') + parent?: Comment; + + @HasMany(() => Comment, 'parentId') + replies: Comment[]; + + @BeforeCreate() + setTimestamps() { + const now = Date.now(); + this.createdAt = now; + this.updatedAt = now; + } + + // Helper methods + async approve(): Promise { + this.isApproved = true; + this.updatedAt = Date.now(); + await this.save(); + } + + async like(): Promise { + this.likeCount += 1; + await this.save(); + } + + async unlike(): Promise { + if (this.likeCount > 0) { + this.likeCount -= 1; + await this.save(); + } + } +} + +// Type definitions for API requests +export interface CreateUserRequest { + username: string; + email: string; + displayName?: string; + avatar?: string; + roles?: string[]; +} + +export interface CreateCategoryRequest { + name: string; + description?: string; + color?: string; +} + +export interface CreatePostRequest { + title: string; + content: string; + excerpt?: string; + authorId: string; + categoryId?: string; + tags?: string[]; + featuredImage?: string; + status?: 'draft' | 'published'; +} + +export interface CreateCommentRequest { + content: string; + postId: string; + authorId: string; + parentId?: string; +} + +export interface UpdatePostRequest { + title?: string; + content?: string; + excerpt?: string; + categoryId?: string; + tags?: string[]; + featuredImage?: string; + isFeatured?: boolean; +} \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/models/BlogValidation.ts b/tests/real-integration/blog-scenario/models/BlogValidation.ts new file mode 100644 index 0000000..99e4760 --- /dev/null +++ b/tests/real-integration/blog-scenario/models/BlogValidation.ts @@ -0,0 +1,216 @@ +import { CreateUserRequest, CreateCategoryRequest, CreatePostRequest, CreateCommentRequest, UpdatePostRequest } from './BlogModels'; + +export class ValidationError extends Error { + constructor(message: string, public field?: string) { + super(message); + this.name = 'ValidationError'; + } +} + +export class BlogValidation { + static validateUser(data: CreateUserRequest): void { + if (!data.username || data.username.length < 3 || data.username.length > 30) { + throw new ValidationError('Username must be between 3 and 30 characters', 'username'); + } + + if (!/^[a-zA-Z0-9_]+$/.test(data.username)) { + throw new ValidationError('Username can only contain letters, numbers, and underscores', 'username'); + } + + if (!data.email || !this.isValidEmail(data.email)) { + throw new ValidationError('Valid email is required', 'email'); + } + + if (data.displayName && data.displayName.length > 100) { + throw new ValidationError('Display name cannot exceed 100 characters', 'displayName'); + } + + if (data.avatar && !this.isValidUrl(data.avatar)) { + throw new ValidationError('Avatar must be a valid URL', 'avatar'); + } + + if (data.roles && !Array.isArray(data.roles)) { + throw new ValidationError('Roles must be an array', 'roles'); + } + } + + static validateCategory(data: CreateCategoryRequest): void { + if (!data.name || data.name.length < 2 || data.name.length > 50) { + throw new ValidationError('Category name must be between 2 and 50 characters', 'name'); + } + + if (data.description && data.description.length > 500) { + throw new ValidationError('Description cannot exceed 500 characters', 'description'); + } + + if (data.color && !/^#[0-9A-Fa-f]{6}$/.test(data.color)) { + throw new ValidationError('Color must be a valid hex color code', 'color'); + } + } + + static validatePost(data: CreatePostRequest): void { + if (!data.title || data.title.length < 3 || data.title.length > 200) { + throw new ValidationError('Title must be between 3 and 200 characters', 'title'); + } + + if (!data.content || data.content.length < 10) { + throw new ValidationError('Content must be at least 10 characters long', 'content'); + } + + if (data.content.length > 50000) { + throw new ValidationError('Content cannot exceed 50,000 characters', 'content'); + } + + if (!data.authorId) { + throw new ValidationError('Author ID is required', 'authorId'); + } + + if (data.excerpt && data.excerpt.length > 300) { + throw new ValidationError('Excerpt cannot exceed 300 characters', 'excerpt'); + } + + if (data.tags && !Array.isArray(data.tags)) { + throw new ValidationError('Tags must be an array', 'tags'); + } + + if (data.tags && data.tags.length > 10) { + throw new ValidationError('Cannot have more than 10 tags', 'tags'); + } + + if (data.tags) { + for (const tag of data.tags) { + if (typeof tag !== 'string' || tag.length > 30) { + throw new ValidationError('Each tag must be a string with max 30 characters', 'tags'); + } + } + } + + if (data.featuredImage && !this.isValidUrl(data.featuredImage)) { + throw new ValidationError('Featured image must be a valid URL', 'featuredImage'); + } + + if (data.status && !['draft', 'published'].includes(data.status)) { + throw new ValidationError('Status must be either "draft" or "published"', 'status'); + } + } + + static validatePostUpdate(data: UpdatePostRequest): void { + if (data.title && (data.title.length < 3 || data.title.length > 200)) { + throw new ValidationError('Title must be between 3 and 200 characters', 'title'); + } + + if (data.content && (data.content.length < 10 || data.content.length > 50000)) { + throw new ValidationError('Content must be between 10 and 50,000 characters', 'content'); + } + + if (data.excerpt && data.excerpt.length > 300) { + throw new ValidationError('Excerpt cannot exceed 300 characters', 'excerpt'); + } + + if (data.tags && !Array.isArray(data.tags)) { + throw new ValidationError('Tags must be an array', 'tags'); + } + + if (data.tags && data.tags.length > 10) { + throw new ValidationError('Cannot have more than 10 tags', 'tags'); + } + + if (data.tags) { + for (const tag of data.tags) { + if (typeof tag !== 'string' || tag.length > 30) { + throw new ValidationError('Each tag must be a string with max 30 characters', 'tags'); + } + } + } + + if (data.featuredImage && !this.isValidUrl(data.featuredImage)) { + throw new ValidationError('Featured image must be a valid URL', 'featuredImage'); + } + + if (data.isFeatured !== undefined && typeof data.isFeatured !== 'boolean') { + throw new ValidationError('isFeatured must be a boolean', 'isFeatured'); + } + } + + static validateComment(data: CreateCommentRequest): void { + if (!data.content || data.content.length < 1 || data.content.length > 2000) { + throw new ValidationError('Comment must be between 1 and 2000 characters', 'content'); + } + + if (!data.postId) { + throw new ValidationError('Post ID is required', 'postId'); + } + + if (!data.authorId) { + throw new ValidationError('Author ID is required', 'authorId'); + } + + // parentId is optional, but if provided should be a string + if (data.parentId !== undefined && typeof data.parentId !== 'string') { + throw new ValidationError('Parent ID must be a string', 'parentId'); + } + } + + private static isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + private static isValidUrl(url: string): boolean { + try { + new URL(url); + return true; + } catch { + return false; + } + } + + // Sanitization helpers + static sanitizeString(input: string): string { + return input.trim().replace(/[<>]/g, ''); + } + + static sanitizeArray(input: string[]): string[] { + return input.map(item => this.sanitizeString(item)).filter(item => item.length > 0); + } + + static sanitizeUserInput(data: CreateUserRequest): CreateUserRequest { + return { + username: this.sanitizeString(data.username), + email: this.sanitizeString(data.email.toLowerCase()), + displayName: data.displayName ? this.sanitizeString(data.displayName) : undefined, + avatar: data.avatar ? this.sanitizeString(data.avatar) : undefined, + roles: data.roles ? this.sanitizeArray(data.roles) : undefined + }; + } + + static sanitizeCategoryInput(data: CreateCategoryRequest): CreateCategoryRequest { + return { + name: this.sanitizeString(data.name), + description: data.description ? this.sanitizeString(data.description) : undefined, + color: data.color ? this.sanitizeString(data.color) : undefined + }; + } + + static sanitizePostInput(data: CreatePostRequest): CreatePostRequest { + return { + title: this.sanitizeString(data.title), + content: data.content.trim(), // Don't sanitize content too aggressively + excerpt: data.excerpt ? this.sanitizeString(data.excerpt) : undefined, + authorId: this.sanitizeString(data.authorId), + categoryId: data.categoryId ? this.sanitizeString(data.categoryId) : undefined, + tags: data.tags ? this.sanitizeArray(data.tags) : undefined, + featuredImage: data.featuredImage ? this.sanitizeString(data.featuredImage) : undefined, + status: data.status + }; + } + + static sanitizeCommentInput(data: CreateCommentRequest): CreateCommentRequest { + return { + content: data.content.trim(), + postId: this.sanitizeString(data.postId), + authorId: this.sanitizeString(data.authorId), + parentId: data.parentId ? this.sanitizeString(data.parentId) : undefined + }; + } +} \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/run-tests.ts b/tests/real-integration/blog-scenario/run-tests.ts new file mode 100644 index 0000000..6caa340 --- /dev/null +++ b/tests/real-integration/blog-scenario/run-tests.ts @@ -0,0 +1,243 @@ +#!/usr/bin/env node + +import { spawn, ChildProcess } from 'child_process'; +import path from 'path'; +import fs from 'fs'; + +interface TestConfig { + scenario: string; + composeFile: string; + testCommand: string; + timeout: number; +} + +class BlogIntegrationTestRunner { + private dockerProcess: ChildProcess | null = null; + private isShuttingDown = false; + + constructor(private config: TestConfig) { + // Handle graceful shutdown + process.on('SIGINT', () => this.shutdown()); + process.on('SIGTERM', () => this.shutdown()); + process.on('exit', () => this.shutdown()); + } + + async run(): Promise { + console.log(`🚀 Starting ${this.config.scenario} integration tests...`); + console.log(`Using compose file: ${this.config.composeFile}`); + + try { + // Verify compose file exists + if (!fs.existsSync(this.config.composeFile)) { + throw new Error(`Docker compose file not found: ${this.config.composeFile}`); + } + + // Clean up any existing containers + await this.cleanup(); + + // Start Docker services + const success = await this.startServices(); + if (!success) { + throw new Error('Failed to start Docker services'); + } + + // Wait for services to be healthy + const healthy = await this.waitForHealthy(); + if (!healthy) { + throw new Error('Services failed to become healthy'); + } + + // Run tests + const testResult = await this.runTests(); + + // Cleanup + await this.cleanup(); + + return testResult; + + } catch (error) { + console.error('❌ Test execution failed:', error.message); + await this.cleanup(); + return false; + } + } + + private async startServices(): Promise { + console.log('🔧 Starting Docker services...'); + + return new Promise((resolve) => { + this.dockerProcess = spawn('docker-compose', [ + '-f', this.config.composeFile, + 'up', + '--build', + '--abort-on-container-exit' + ], { + stdio: 'pipe', + cwd: path.dirname(this.config.composeFile) + }); + + let servicesStarted = false; + let testRunnerFinished = false; + + this.dockerProcess.stdout?.on('data', (data) => { + const output = data.toString(); + console.log('[DOCKER]', output.trim()); + + // Check if all services are up + if (output.includes('blog-node-3') && output.includes('healthy')) { + servicesStarted = true; + } + + // Check if test runner has finished + if (output.includes('blog-test-runner') && (output.includes('exited') || output.includes('done'))) { + testRunnerFinished = true; + } + }); + + this.dockerProcess.stderr?.on('data', (data) => { + console.error('[DOCKER ERROR]', data.toString().trim()); + }); + + this.dockerProcess.on('exit', (code) => { + console.log(`Docker process exited with code: ${code}`); + resolve(code === 0 && testRunnerFinished); + }); + + // Timeout after specified time + setTimeout(() => { + if (!testRunnerFinished) { + console.log('❌ Test execution timed out'); + resolve(false); + } + }, this.config.timeout); + }); + } + + private async waitForHealthy(): Promise { + console.log('🔧 Waiting for services to be healthy...'); + + // Wait for health checks to pass + for (let attempt = 0; attempt < 30; attempt++) { + try { + const result = await this.checkHealth(); + if (result) { + console.log('✅ All services are healthy'); + return true; + } + } catch (error) { + // Continue waiting + } + + await new Promise(resolve => setTimeout(resolve, 5000)); + } + + console.log('❌ Services failed to become healthy within timeout'); + return false; + } + + private async checkHealth(): Promise { + return new Promise((resolve) => { + const healthCheck = spawn('docker-compose', [ + '-f', this.config.composeFile, + 'ps' + ], { + stdio: 'pipe', + cwd: path.dirname(this.config.composeFile) + }); + + let output = ''; + healthCheck.stdout?.on('data', (data) => { + output += data.toString(); + }); + + healthCheck.on('exit', () => { + // Check if all required services are healthy + const requiredServices = ['blog-node-1', 'blog-node-2', 'blog-node-3']; + const allHealthy = requiredServices.every(service => + output.includes(service) && output.includes('Up') && output.includes('healthy') + ); + + resolve(allHealthy); + }); + }); + } + + private async runTests(): Promise { + console.log('🧪 Running integration tests...'); + + // Tests are run as part of the Docker composition + // We just need to wait for the test runner container to complete + return true; + } + + private async cleanup(): Promise { + if (this.isShuttingDown) return; + this.isShuttingDown = true; + + console.log('🧹 Cleaning up Docker resources...'); + + try { + // Stop and remove containers + const cleanup = spawn('docker-compose', [ + '-f', this.config.composeFile, + 'down', + '-v', + '--remove-orphans' + ], { + stdio: 'inherit', + cwd: path.dirname(this.config.composeFile) + }); + + await new Promise((resolve) => { + cleanup.on('exit', resolve); + setTimeout(resolve, 10000); // Force cleanup after 10s + }); + + console.log('✅ Cleanup completed'); + } catch (error) { + console.warn('⚠️ Cleanup warning:', error.message); + } + } + + private async shutdown(): Promise { + console.log('\n🛑 Shutting down...'); + + if (this.dockerProcess && !this.dockerProcess.killed) { + this.dockerProcess.kill('SIGTERM'); + } + + await this.cleanup(); + process.exit(0); + } +} + +// Main execution +async function main() { + const config: TestConfig = { + scenario: 'blog', + composeFile: path.join(__dirname, 'docker', 'docker-compose.blog.yml'), + testCommand: 'npm run test:blog-integration', + timeout: 600000 // 10 minutes + }; + + const runner = new BlogIntegrationTestRunner(config); + const success = await runner.run(); + + if (success) { + console.log('🎉 Blog integration tests completed successfully!'); + process.exit(0); + } else { + console.log('❌ Blog integration tests failed!'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + main().catch((error) => { + console.error('💥 Unexpected error:', error); + process.exit(1); + }); +} + +export { BlogIntegrationTestRunner }; \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/scenarios/BlogTestRunner.ts b/tests/real-integration/blog-scenario/scenarios/BlogTestRunner.ts new file mode 100644 index 0000000..73d5aef --- /dev/null +++ b/tests/real-integration/blog-scenario/scenarios/BlogTestRunner.ts @@ -0,0 +1,446 @@ +import { ApiClient } from '../../shared/utils/ApiClient'; +import { SyncWaiter } from '../../shared/utils/SyncWaiter'; +import { CreateUserRequest, CreateCategoryRequest, CreatePostRequest, CreateCommentRequest } from '../models/BlogModels'; + +export interface BlogTestConfig { + nodeEndpoints: string[]; + syncTimeout: number; + operationTimeout: number; +} + +export class BlogTestRunner { + private apiClients: ApiClient[]; + private syncWaiter: SyncWaiter; + + constructor(private config: BlogTestConfig) { + this.apiClients = config.nodeEndpoints.map(endpoint => new ApiClient(endpoint)); + this.syncWaiter = new SyncWaiter(this.apiClients); + } + + // Initialization and setup + async waitForNodesReady(timeout: number = 60000): Promise { + console.log('🔧 Waiting for blog nodes to be ready...'); + return await this.syncWaiter.waitForNodesReady(timeout); + } + + async waitForPeerConnections(timeout: number = 30000): Promise { + console.log('🔧 Waiting for peer connections...'); + return await this.syncWaiter.waitForPeerConnections(2, timeout); + } + + async waitForSync(timeout: number = 10000): Promise { + await this.syncWaiter.waitForSync(timeout); + } + + // User operations + async createUser(nodeIndex: number, userData: CreateUserRequest): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post('/api/users', userData); + + if (response.error) { + throw new Error(`Failed to create user on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getUser(nodeIndex: number, userId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.get(`/api/users/${userId}`); + + if (response.error) { + throw new Error(`Failed to get user on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getUsers(nodeIndex: number, options: { page?: number; limit?: number; search?: string } = {}): Promise { + const client = this.getClient(nodeIndex); + const queryString = new URLSearchParams(options as any).toString(); + const response = await client.get(`/api/users?${queryString}`); + + if (response.error) { + throw new Error(`Failed to get users on node ${nodeIndex}: ${response.error}`); + } + + return response.data.users; + } + + async updateUser(nodeIndex: number, userId: string, updateData: any): Promise { + const client = this.getClient(nodeIndex); + const response = await client.put(`/api/users/${userId}`, updateData); + + if (response.error) { + throw new Error(`Failed to update user on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + // Category operations + async createCategory(nodeIndex: number, categoryData: CreateCategoryRequest): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post('/api/categories', categoryData); + + if (response.error) { + throw new Error(`Failed to create category on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getCategory(nodeIndex: number, categoryId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.get(`/api/categories/${categoryId}`); + + if (response.error) { + throw new Error(`Failed to get category on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getCategories(nodeIndex: number): Promise { + const client = this.getClient(nodeIndex); + const response = await client.get('/api/categories'); + + if (response.error) { + throw new Error(`Failed to get categories on node ${nodeIndex}: ${response.error}`); + } + + return response.data.categories; + } + + // Post operations + async createPost(nodeIndex: number, postData: CreatePostRequest): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post('/api/posts', postData); + + if (response.error) { + throw new Error(`Failed to create post on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getPost(nodeIndex: number, postId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.get(`/api/posts/${postId}`); + + if (response.error) { + throw new Error(`Failed to get post on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getPosts(nodeIndex: number, options: { + page?: number; + limit?: number; + status?: string; + authorId?: string; + categoryId?: string; + tag?: string; + } = {}): Promise { + const client = this.getClient(nodeIndex); + const queryString = new URLSearchParams(options as any).toString(); + const response = await client.get(`/api/posts?${queryString}`); + + if (response.error) { + throw new Error(`Failed to get posts on node ${nodeIndex}: ${response.error}`); + } + + return response.data.posts; + } + + async updatePost(nodeIndex: number, postId: string, updateData: any): Promise { + const client = this.getClient(nodeIndex); + const response = await client.put(`/api/posts/${postId}`, updateData); + + if (response.error) { + throw new Error(`Failed to update post on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async publishPost(nodeIndex: number, postId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post(`/api/posts/${postId}/publish`, {}); + + if (response.error) { + throw new Error(`Failed to publish post on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async unpublishPost(nodeIndex: number, postId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post(`/api/posts/${postId}/unpublish`, {}); + + if (response.error) { + throw new Error(`Failed to unpublish post on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async likePost(nodeIndex: number, postId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post(`/api/posts/${postId}/like`, {}); + + if (response.error) { + throw new Error(`Failed to like post on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async viewPost(nodeIndex: number, postId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post(`/api/posts/${postId}/view`, {}); + + if (response.error) { + throw new Error(`Failed to view post on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + // Comment operations + async createComment(nodeIndex: number, commentData: CreateCommentRequest): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post('/api/comments', commentData); + + if (response.error) { + throw new Error(`Failed to create comment on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getComments(nodeIndex: number, postId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.get(`/api/posts/${postId}/comments`); + + if (response.error) { + throw new Error(`Failed to get comments on node ${nodeIndex}: ${response.error}`); + } + + return response.data.comments; + } + + async approveComment(nodeIndex: number, commentId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post(`/api/comments/${commentId}/approve`, {}); + + if (response.error) { + throw new Error(`Failed to approve comment on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async likeComment(nodeIndex: number, commentId: string): Promise { + const client = this.getClient(nodeIndex); + const response = await client.post(`/api/comments/${commentId}/like`, {}); + + if (response.error) { + throw new Error(`Failed to like comment on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + // Metrics and monitoring + async getNetworkMetrics(nodeIndex: number): Promise { + const client = this.getClient(nodeIndex); + const response = await client.get('/api/metrics/network'); + + if (response.error) { + throw new Error(`Failed to get network metrics on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getDataMetrics(nodeIndex: number): Promise { + const client = this.getClient(nodeIndex); + const response = await client.get('/api/metrics/data'); + + if (response.error) { + throw new Error(`Failed to get data metrics on node ${nodeIndex}: ${response.error}`); + } + + return response.data; + } + + async getAllNetworkMetrics(): Promise { + const metrics = []; + for (let i = 0; i < this.apiClients.length; i++) { + try { + const nodeMetrics = await this.getNetworkMetrics(i); + metrics.push(nodeMetrics); + } catch (error) { + console.warn(`Failed to get metrics from node ${i}: ${error.message}`); + } + } + return metrics; + } + + async getAllDataMetrics(): Promise { + const metrics = []; + for (let i = 0; i < this.apiClients.length; i++) { + try { + const nodeMetrics = await this.getDataMetrics(i); + metrics.push(nodeMetrics); + } catch (error) { + console.warn(`Failed to get data metrics from node ${i}: ${error.message}`); + } + } + return metrics; + } + + // Data consistency checks + async verifyDataConsistency(dataType: 'users' | 'posts' | 'comments' | 'categories', expectedCount: number, tolerance: number = 0): Promise { + return await this.syncWaiter.waitForDataConsistency(dataType, expectedCount, this.config.syncTimeout, tolerance); + } + + async verifyUserSync(userId: string): Promise { + console.log(`🔍 Verifying user ${userId} sync across all nodes...`); + + try { + const userPromises = this.apiClients.map((_, index) => this.getUser(index, userId)); + const users = await Promise.all(userPromises); + + // Check if all users have the same data + const firstUser = users[0]; + const allSame = users.every(user => + user.id === firstUser.id && + user.username === firstUser.username && + user.email === firstUser.email + ); + + if (allSame) { + console.log(`✅ User ${userId} is consistent across all nodes`); + return true; + } else { + console.log(`❌ User ${userId} is not consistent across nodes`); + return false; + } + } catch (error) { + console.log(`❌ Failed to verify user sync: ${error.message}`); + return false; + } + } + + async verifyPostSync(postId: string): Promise { + console.log(`🔍 Verifying post ${postId} sync across all nodes...`); + + try { + const postPromises = this.apiClients.map((_, index) => this.getPost(index, postId)); + const posts = await Promise.all(postPromises); + + // Check if all posts have the same data + const firstPost = posts[0]; + const allSame = posts.every(post => + post.id === firstPost.id && + post.title === firstPost.title && + post.status === firstPost.status + ); + + if (allSame) { + console.log(`✅ Post ${postId} is consistent across all nodes`); + return true; + } else { + console.log(`❌ Post ${postId} is not consistent across nodes`); + return false; + } + } catch (error) { + console.log(`❌ Failed to verify post sync: ${error.message}`); + return false; + } + } + + // Utility methods + private getClient(nodeIndex: number): ApiClient { + if (nodeIndex >= this.apiClients.length) { + throw new Error(`Node index ${nodeIndex} is out of range. Available nodes: 0-${this.apiClients.length - 1}`); + } + return this.apiClients[nodeIndex]; + } + + async logStatus(): Promise { + console.log('\n📊 Blog Test Environment Status:'); + console.log(`Total Nodes: ${this.config.nodeEndpoints.length}`); + + const [networkMetrics, dataMetrics] = await Promise.all([ + this.getAllNetworkMetrics(), + this.getAllDataMetrics() + ]); + + networkMetrics.forEach((metrics, index) => { + const data = dataMetrics[index]; + console.log(`Node ${index} (${metrics.nodeId}):`); + console.log(` Peers: ${metrics.peers}`); + if (data) { + console.log(` Data: Users=${data.counts.users}, Posts=${data.counts.posts}, Comments=${data.counts.comments}, Categories=${data.counts.categories}`); + } + }); + console.log(''); + } + + async cleanup(): Promise { + console.log('🧹 Cleaning up blog test environment...'); + // Any cleanup logic if needed + } + + // Test data generators + generateUserData(index: number): CreateUserRequest { + return { + username: `testuser${index}`, + email: `testuser${index}@example.com`, + displayName: `Test User ${index}`, + roles: ['user'] + }; + } + + generateCategoryData(index: number): CreateCategoryRequest { + const categories = [ + { name: 'Technology', description: 'Posts about technology and programming' }, + { name: 'Design', description: 'UI/UX design and creative content' }, + { name: 'Business', description: 'Business strategies and entrepreneurship' }, + { name: 'Lifestyle', description: 'Lifestyle and personal development' }, + { name: 'Science', description: 'Scientific discoveries and research' } + ]; + + const category = categories[index % categories.length]; + return { + name: `${category.name} ${Math.floor(index / categories.length) || ''}`.trim(), + description: category.description + }; + } + + generatePostData(authorId: string, categoryId?: string, index: number = 0): CreatePostRequest { + return { + title: `Test Blog Post ${index + 1}`, + content: `This is the content of test blog post ${index + 1}. It contains detailed information about the topic and provides valuable insights to readers. The content is long enough to test the system's handling of substantial text data.`, + excerpt: `This is a test blog post excerpt ${index + 1}`, + authorId, + categoryId, + tags: [`tag${index}`, 'test', 'blog'], + status: 'draft' + }; + } + + generateCommentData(postId: string, authorId: string, index: number = 0, parentId?: string): CreateCommentRequest { + return { + content: `This is test comment ${index + 1}. It provides feedback on the blog post.`, + postId, + authorId, + parentId + }; + } +} \ No newline at end of file diff --git a/tests/real-integration/blog-scenario/tests/basic-operations.test.ts b/tests/real-integration/blog-scenario/tests/basic-operations.test.ts new file mode 100644 index 0000000..40d5f64 --- /dev/null +++ b/tests/real-integration/blog-scenario/tests/basic-operations.test.ts @@ -0,0 +1,236 @@ +import { describe, test, expect, beforeAll, afterAll } from '@jest/globals'; +import { blogTestHelper, TestUser, TestCategory, TestPost, TestComment } from './setup'; + +describe('Blog Basic Operations', () => { + let testUser: TestUser; + let testCategory: TestCategory; + let testPost: TestPost; + let testComment: TestComment; + + beforeAll(async () => { + console.log('🔄 Waiting for all nodes to be ready...'); + await blogTestHelper.waitForNodesReady(); + console.log('✅ All nodes are ready for testing'); + }, 60000); // 60 second timeout for setup + + describe('User Management', () => { + test('should create a user successfully', async () => { + const testData = blogTestHelper.generateTestData(); + + testUser = await blogTestHelper.createUser(testData.user); + + expect(testUser).toBeDefined(); + expect(testUser.id).toBeDefined(); + expect(testUser.username).toBe(testData.user.username); + expect(testUser.email).toBe(testData.user.email); + expect(testUser.displayName).toBe(testData.user.displayName); + + console.log(`✅ Created user: ${testUser.username} (${testUser.id})`); + }, 30000); + + test('should retrieve user by ID from all nodes', async () => { + expect(testUser?.id).toBeDefined(); + + // Test retrieval from each node + for (const node of blogTestHelper.getNodes()) { + const retrievedUser = await blogTestHelper.getUser(testUser.id!, node.id); + + expect(retrievedUser).toBeDefined(); + expect(retrievedUser!.id).toBe(testUser.id); + expect(retrievedUser!.username).toBe(testUser.username); + expect(retrievedUser!.email).toBe(testUser.email); + + console.log(`✅ Retrieved user from ${node.id}: ${retrievedUser!.username}`); + } + }, 30000); + + test('should list users with pagination', async () => { + const result = await blogTestHelper.listUsers(); + + expect(result).toBeDefined(); + expect(result.users).toBeInstanceOf(Array); + expect(result.users.length).toBeGreaterThan(0); + expect(result.page).toBeDefined(); + expect(result.limit).toBeDefined(); + + // Verify our test user is in the list + const foundUser = result.users.find(u => u.id === testUser.id); + expect(foundUser).toBeDefined(); + + console.log(`✅ Listed ${result.users.length} users`); + }, 30000); + }); + + describe('Category Management', () => { + test('should create a category successfully', async () => { + const testData = blogTestHelper.generateTestData(); + + testCategory = await blogTestHelper.createCategory(testData.category); + + expect(testCategory).toBeDefined(); + expect(testCategory.id).toBeDefined(); + expect(testCategory.name).toBe(testData.category.name); + expect(testCategory.description).toBe(testData.category.description); + expect(testCategory.color).toBe(testData.category.color); + + console.log(`✅ Created category: ${testCategory.name} (${testCategory.id})`); + }, 30000); + + test('should retrieve category from all nodes', async () => { + expect(testCategory?.id).toBeDefined(); + + for (const node of blogTestHelper.getNodes()) { + const retrievedCategory = await blogTestHelper.getCategory(testCategory.id!, node.id); + + expect(retrievedCategory).toBeDefined(); + expect(retrievedCategory!.id).toBe(testCategory.id); + expect(retrievedCategory!.name).toBe(testCategory.name); + + console.log(`✅ Retrieved category from ${node.id}: ${retrievedCategory!.name}`); + } + }, 30000); + + test('should list all categories', async () => { + const result = await blogTestHelper.listCategories(); + + expect(result).toBeDefined(); + expect(result.categories).toBeInstanceOf(Array); + expect(result.categories.length).toBeGreaterThan(0); + + // Verify our test category is in the list + const foundCategory = result.categories.find(c => c.id === testCategory.id); + expect(foundCategory).toBeDefined(); + + console.log(`✅ Listed ${result.categories.length} categories`); + }, 30000); + }); + + describe('Post Management', () => { + test('should create a post successfully', async () => { + expect(testUser?.id).toBeDefined(); + expect(testCategory?.id).toBeDefined(); + + const testData = blogTestHelper.generateTestData(); + const postData = testData.post(testUser.id!, testCategory.id!); + + testPost = await blogTestHelper.createPost(postData); + + expect(testPost).toBeDefined(); + expect(testPost.id).toBeDefined(); + expect(testPost.title).toBe(postData.title); + expect(testPost.content).toBe(postData.content); + expect(testPost.authorId).toBe(testUser.id); + expect(testPost.categoryId).toBe(testCategory.id); + expect(testPost.status).toBe('draft'); + + console.log(`✅ Created post: ${testPost.title} (${testPost.id})`); + }, 30000); + + test('should retrieve post with relationships from all nodes', async () => { + expect(testPost?.id).toBeDefined(); + + for (const node of blogTestHelper.getNodes()) { + const retrievedPost = await blogTestHelper.getPost(testPost.id!, node.id); + + expect(retrievedPost).toBeDefined(); + expect(retrievedPost!.id).toBe(testPost.id); + expect(retrievedPost!.title).toBe(testPost.title); + expect(retrievedPost!.authorId).toBe(testUser.id); + expect(retrievedPost!.categoryId).toBe(testCategory.id); + + console.log(`✅ Retrieved post from ${node.id}: ${retrievedPost!.title}`); + } + }, 30000); + + test('should publish post and update status', async () => { + expect(testPost?.id).toBeDefined(); + + const publishedPost = await blogTestHelper.publishPost(testPost.id!); + + expect(publishedPost).toBeDefined(); + expect(publishedPost.status).toBe('published'); + expect(publishedPost.publishedAt).toBeDefined(); + + // Verify status change is replicated across nodes + await blogTestHelper.waitForDataReplication(async () => { + for (const node of blogTestHelper.getNodes()) { + const post = await blogTestHelper.getPost(testPost.id!, node.id); + if (!post || post.status !== 'published') { + return false; + } + } + return true; + }, 15000); + + console.log(`✅ Published post: ${publishedPost.title}`); + }, 30000); + + test('should like post and increment count', async () => { + expect(testPost?.id).toBeDefined(); + + const result = await blogTestHelper.likePost(testPost.id!); + + expect(result).toBeDefined(); + expect(result.likeCount).toBeGreaterThan(0); + + console.log(`✅ Liked post, count: ${result.likeCount}`); + }, 30000); + }); + + describe('Comment Management', () => { + test('should create a comment successfully', async () => { + expect(testPost?.id).toBeDefined(); + expect(testUser?.id).toBeDefined(); + + const testData = blogTestHelper.generateTestData(); + const commentData = testData.comment(testPost.id!, testUser.id!); + + testComment = await blogTestHelper.createComment(commentData); + + expect(testComment).toBeDefined(); + expect(testComment.id).toBeDefined(); + expect(testComment.content).toBe(commentData.content); + expect(testComment.postId).toBe(testPost.id); + expect(testComment.authorId).toBe(testUser.id); + + console.log(`✅ Created comment: ${testComment.id}`); + }, 30000); + + test('should retrieve post comments from all nodes', async () => { + expect(testPost?.id).toBeDefined(); + expect(testComment?.id).toBeDefined(); + + for (const node of blogTestHelper.getNodes()) { + const result = await blogTestHelper.getPostComments(testPost.id!, node.id); + + expect(result).toBeDefined(); + expect(result.comments).toBeInstanceOf(Array); + + // Find our test comment + const foundComment = result.comments.find(c => c.id === testComment.id); + expect(foundComment).toBeDefined(); + expect(foundComment!.content).toBe(testComment.content); + + console.log(`✅ Retrieved ${result.comments.length} comments from ${node.id}`); + } + }, 30000); + }); + + describe('Data Metrics', () => { + test('should get data metrics from all nodes', async () => { + for (const node of blogTestHelper.getNodes()) { + const metrics = await blogTestHelper.getDataMetrics(node.id); + + expect(metrics).toBeDefined(); + expect(metrics.nodeId).toBe(node.id); + expect(metrics.counts).toBeDefined(); + expect(metrics.counts.users).toBeGreaterThan(0); + expect(metrics.counts.categories).toBeGreaterThan(0); + expect(metrics.counts.posts).toBeGreaterThan(0); + expect(metrics.counts.comments).toBeGreaterThan(0); + + console.log(`✅ ${node.id} metrics:`, JSON.stringify(metrics.counts, null, 2)); + } + }, 30000); + }); +}); diff --git a/tests/real-integration/blog-scenario/tests/cross-node-operations.test.ts b/tests/real-integration/blog-scenario/tests/cross-node-operations.test.ts new file mode 100644 index 0000000..45ef03a --- /dev/null +++ b/tests/real-integration/blog-scenario/tests/cross-node-operations.test.ts @@ -0,0 +1,285 @@ +import { describe, test, expect, beforeAll } from '@jest/globals'; +import { blogTestHelper, TestUser, TestCategory, TestPost, TestComment } from './setup'; + +describe('Cross-Node Operations', () => { + let users: TestUser[] = []; + let categories: TestCategory[] = []; + let posts: TestPost[] = []; + + beforeAll(async () => { + console.log('🔄 Waiting for all nodes to be ready...'); + await blogTestHelper.waitForNodesReady(); + console.log('✅ All nodes are ready for cross-node testing'); + }, 60000); + + describe('Distributed Content Creation', () => { + test('should create users on different nodes', async () => { + const nodes = blogTestHelper.getNodes(); + + // Create one user on each node + for (let i = 0; i < nodes.length; i++) { + const testData = blogTestHelper.generateTestData(); + const user = await blogTestHelper.createUser(testData.user, nodes[i].id); + + expect(user).toBeDefined(); + expect(user.id).toBeDefined(); + users.push(user); + + console.log(`✅ Created user ${user.username} on ${nodes[i].id}`); + } + + expect(users).toHaveLength(3); + }, 45000); + + test('should verify users are replicated across all nodes', async () => { + // Wait for replication + await blogTestHelper.sleep(3000); + + for (const user of users) { + for (const node of blogTestHelper.getNodes()) { + const retrievedUser = await blogTestHelper.getUser(user.id!, node.id); + + expect(retrievedUser).toBeDefined(); + expect(retrievedUser!.id).toBe(user.id); + expect(retrievedUser!.username).toBe(user.username); + + console.log(`✅ User ${user.username} found on ${node.id}`); + } + } + }, 45000); + + test('should create categories on different nodes', async () => { + const nodes = blogTestHelper.getNodes(); + + for (let i = 0; i < nodes.length; i++) { + const testData = blogTestHelper.generateTestData(); + const category = await blogTestHelper.createCategory(testData.category, nodes[i].id); + + expect(category).toBeDefined(); + expect(category.id).toBeDefined(); + categories.push(category); + + console.log(`✅ Created category ${category.name} on ${nodes[i].id}`); + } + + expect(categories).toHaveLength(3); + }, 45000); + + test('should create posts with cross-node relationships', async () => { + const nodes = blogTestHelper.getNodes(); + + // Create posts where author and category are from different nodes + for (let i = 0; i < nodes.length; i++) { + const authorIndex = i; + const categoryIndex = (i + 1) % nodes.length; // Use next node's category + const nodeIndex = (i + 2) % nodes.length; // Create on third node + + const testData = blogTestHelper.generateTestData(); + const postData = testData.post(users[authorIndex].id!, categories[categoryIndex].id!); + + const post = await blogTestHelper.createPost(postData, nodes[nodeIndex].id); + + expect(post).toBeDefined(); + expect(post.id).toBeDefined(); + expect(post.authorId).toBe(users[authorIndex].id); + expect(post.categoryId).toBe(categories[categoryIndex].id); + posts.push(post); + + console.log( + `✅ Created post "${post.title}" on ${nodes[nodeIndex].id} ` + + `(author from node-${authorIndex + 1}, category from node-${categoryIndex + 1})` + ); + } + + expect(posts).toHaveLength(3); + }, 45000); + + test('should verify cross-node posts are accessible from all nodes', async () => { + // Wait for replication + await blogTestHelper.sleep(3000); + + for (const post of posts) { + for (const node of blogTestHelper.getNodes()) { + const retrievedPost = await blogTestHelper.getPost(post.id!, node.id); + + expect(retrievedPost).toBeDefined(); + expect(retrievedPost!.id).toBe(post.id); + expect(retrievedPost!.title).toBe(post.title); + expect(retrievedPost!.authorId).toBe(post.authorId); + expect(retrievedPost!.categoryId).toBe(post.categoryId); + } + } + + console.log('✅ All cross-node posts are accessible from all nodes'); + }, 45000); + }); + + describe('Concurrent Operations', () => { + test('should handle concurrent likes on same post from different nodes', async () => { + const post = posts[0]; + const nodes = blogTestHelper.getNodes(); + + // Perform concurrent likes from all nodes + const likePromises = nodes.map(node => + blogTestHelper.likePost(post.id!, node.id) + ); + + const results = await Promise.all(likePromises); + + // All should succeed + results.forEach(result => { + expect(result).toBeDefined(); + expect(result.likeCount).toBeGreaterThan(0); + }); + + // Wait for eventual consistency + await blogTestHelper.sleep(3000); + + // Verify final like count is consistent across nodes + const finalCounts: number[] = []; + for (const node of nodes) { + const updatedPost = await blogTestHelper.getPost(post.id!, node.id); + expect(updatedPost).toBeDefined(); + finalCounts.push(updatedPost!.likeCount || 0); + } + + // All nodes should have the same final count + const uniqueCounts = [...new Set(finalCounts)]; + expect(uniqueCounts).toHaveLength(1); + + console.log(`✅ Concurrent likes handled, final count: ${finalCounts[0]}`); + }, 45000); + + test('should handle simultaneous comment creation', async () => { + const post = posts[1]; + const nodes = blogTestHelper.getNodes(); + + // Create comments simultaneously from different nodes + const commentPromises = nodes.map((node, index) => { + const testData = blogTestHelper.generateTestData(); + const commentData = testData.comment(post.id!, users[index].id!); + return blogTestHelper.createComment(commentData, node.id); + }); + + const comments = await Promise.all(commentPromises); + + // All comments should be created successfully + comments.forEach((comment, index) => { + expect(comment).toBeDefined(); + expect(comment.id).toBeDefined(); + expect(comment.postId).toBe(post.id); + expect(comment.authorId).toBe(users[index].id); + }); + + // Wait for replication + await blogTestHelper.sleep(3000); + + // Verify all comments are visible from all nodes + for (const node of nodes) { + const result = await blogTestHelper.getPostComments(post.id!, node.id); + expect(result.comments.length).toBeGreaterThanOrEqual(3); + + // Verify all our comments are present + for (const comment of comments) { + const found = result.comments.find(c => c.id === comment.id); + expect(found).toBeDefined(); + } + } + + console.log(`✅ Created ${comments.length} simultaneous comments`); + }, 45000); + }); + + describe('Load Distribution', () => { + test('should distribute read operations across nodes', async () => { + const readCounts = new Map(); + const totalReads = 30; + + // Perform multiple reads and track which nodes are used + for (let i = 0; i < totalReads; i++) { + const randomPost = posts[Math.floor(Math.random() * posts.length)]; + const node = blogTestHelper.getRandomNode(); + + const post = await blogTestHelper.getPost(randomPost.id!, node.id); + expect(post).toBeDefined(); + + readCounts.set(node.id, (readCounts.get(node.id) || 0) + 1); + } + + // Verify reads were distributed across nodes + const nodeIds = blogTestHelper.getNodes().map(n => n.id); + nodeIds.forEach(nodeId => { + const count = readCounts.get(nodeId) || 0; + console.log(`${nodeId}: ${count} reads`); + expect(count).toBeGreaterThan(0); // Each node should have at least one read + }); + + console.log('✅ Read operations distributed across all nodes'); + }, 45000); + + test('should verify consistent data across all read operations', async () => { + // Read the same post from all nodes multiple times + const post = posts[0]; + const readResults: TestPost[] = []; + + for (let i = 0; i < 10; i++) { + for (const node of blogTestHelper.getNodes()) { + const result = await blogTestHelper.getPost(post.id!, node.id); + expect(result).toBeDefined(); + readResults.push(result!); + } + } + + // Verify all reads return identical data + readResults.forEach(result => { + expect(result.id).toBe(post.id); + expect(result.title).toBe(post.title); + expect(result.content).toBe(post.content); + expect(result.authorId).toBe(post.authorId); + expect(result.categoryId).toBe(post.categoryId); + }); + + console.log(`✅ ${readResults.length} read operations returned consistent data`); + }, 45000); + }); + + describe('Network Metrics', () => { + test('should show network connectivity between nodes', async () => { + for (const node of blogTestHelper.getNodes()) { + const metrics = await blogTestHelper.getNodeMetrics(node.id); + + expect(metrics).toBeDefined(); + expect(metrics.nodeId).toBe(node.id); + + console.log(`📊 ${node.id} framework metrics:`, { + services: metrics.services, + environment: metrics.environment, + features: metrics.features + }); + } + }, 30000); + + test('should verify data consistency across all nodes', async () => { + const allMetrics = []; + + for (const node of blogTestHelper.getNodes()) { + const metrics = await blogTestHelper.getDataMetrics(node.id); + allMetrics.push(metrics); + + console.log(`📊 ${node.id} data counts:`, metrics.counts); + } + + // Verify all nodes have the same data counts (eventual consistency) + const firstCounts = allMetrics[0].counts; + allMetrics.forEach((metrics, index) => { + expect(metrics.counts.users).toBe(firstCounts.users); + expect(metrics.counts.categories).toBe(firstCounts.categories); + expect(metrics.counts.posts).toBe(firstCounts.posts); + + console.log(`✅ Node ${index + 1} data counts match reference`); + }); + + console.log('✅ Data consistency verified across all nodes'); + }, 30000); + }); +}); diff --git a/tests/real-integration/blog-scenario/tests/setup.ts b/tests/real-integration/blog-scenario/tests/setup.ts new file mode 100644 index 0000000..9cc2dce --- /dev/null +++ b/tests/real-integration/blog-scenario/tests/setup.ts @@ -0,0 +1,306 @@ +import axios, { AxiosResponse } from 'axios'; + +export interface TestNode { + id: string; + baseUrl: string; + port: number; +} + +export interface TestUser { + id?: string; + username: string; + email: string; + displayName?: string; + avatar?: string; + roles?: string[]; +} + +export interface TestCategory { + id?: string; + name: string; + description?: string; + color?: string; +} + +export interface TestPost { + id?: string; + title: string; + content: string; + excerpt?: string; + authorId: string; + categoryId?: string; + tags?: string[]; + status?: 'draft' | 'published' | 'archived'; +} + +export interface TestComment { + id?: string; + content: string; + postId: string; + authorId: string; + parentId?: string; +} + +export class BlogTestHelper { + private nodes: TestNode[]; + private timeout: number; + + constructor() { + this.nodes = [ + { id: 'blog-node-1', baseUrl: 'http://blog-node-1:3000', port: 3000 }, + { id: 'blog-node-2', baseUrl: 'http://blog-node-2:3000', port: 3000 }, + { id: 'blog-node-3', baseUrl: 'http://blog-node-3:3000', port: 3000 } + ]; + this.timeout = 30000; // 30 seconds + } + + getNodes(): TestNode[] { + return this.nodes; + } + + getRandomNode(): TestNode { + return this.nodes[Math.floor(Math.random() * this.nodes.length)]; + } + + async waitForNodesReady(): Promise { + const maxRetries = 30; + const retryDelay = 1000; + + for (const node of this.nodes) { + let retries = 0; + let healthy = false; + + while (retries < maxRetries && !healthy) { + try { + const response = await axios.get(`${node.baseUrl}/health`, { + timeout: 5000 + }); + + if (response.status === 200 && response.data.status === 'healthy') { + console.log(`✅ Node ${node.id} is healthy`); + healthy = true; + } + } catch (error) { + retries++; + console.log(`⏳ Waiting for ${node.id} to be ready (attempt ${retries}/${maxRetries})`); + await this.sleep(retryDelay); + } + } + + if (!healthy) { + throw new Error(`Node ${node.id} failed to become healthy after ${maxRetries} attempts`); + } + } + + // Additional wait for inter-node connectivity + console.log('⏳ Waiting for nodes to establish connectivity...'); + await this.sleep(5000); + } + + async sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // User operations + async createUser(user: TestUser, nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.post(`${node.baseUrl}/api/users`, user, { + timeout: this.timeout + }); + return response.data; + } + + async getUser(userId: string, nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + try { + const response = await axios.get(`${node.baseUrl}/api/users/${userId}`, { + timeout: this.timeout + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + return null; + } + throw error; + } + } + + async listUsers(nodeId?: string, params?: any): Promise<{ users: TestUser[], page: number, limit: number }> { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.get(`${node.baseUrl}/api/users`, { + params, + timeout: this.timeout + }); + return response.data; + } + + // Category operations + async createCategory(category: TestCategory, nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.post(`${node.baseUrl}/api/categories`, category, { + timeout: this.timeout + }); + return response.data; + } + + async getCategory(categoryId: string, nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + try { + const response = await axios.get(`${node.baseUrl}/api/categories/${categoryId}`, { + timeout: this.timeout + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + return null; + } + throw error; + } + } + + async listCategories(nodeId?: string): Promise<{ categories: TestCategory[] }> { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.get(`${node.baseUrl}/api/categories`, { + timeout: this.timeout + }); + return response.data; + } + + // Post operations + async createPost(post: TestPost, nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.post(`${node.baseUrl}/api/posts`, post, { + timeout: this.timeout + }); + return response.data; + } + + async getPost(postId: string, nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + try { + const response = await axios.get(`${node.baseUrl}/api/posts/${postId}`, { + timeout: this.timeout + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + return null; + } + throw error; + } + } + + async publishPost(postId: string, nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.post(`${node.baseUrl}/api/posts/${postId}/publish`, {}, { + timeout: this.timeout + }); + return response.data; + } + + async likePost(postId: string, nodeId?: string): Promise<{ likeCount: number }> { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.post(`${node.baseUrl}/api/posts/${postId}/like`, {}, { + timeout: this.timeout + }); + return response.data; + } + + // Comment operations + async createComment(comment: TestComment, nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.post(`${node.baseUrl}/api/comments`, comment, { + timeout: this.timeout + }); + return response.data; + } + + async getPostComments(postId: string, nodeId?: string): Promise<{ comments: TestComment[] }> { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.get(`${node.baseUrl}/api/posts/${postId}/comments`, { + timeout: this.timeout + }); + return response.data; + } + + // Metrics and health + async getNodeMetrics(nodeId: string): Promise { + const node = this.getNodeById(nodeId); + const response = await axios.get(`${node.baseUrl}/api/metrics/framework`, { + timeout: this.timeout + }); + return response.data; + } + + async getDataMetrics(nodeId?: string): Promise { + const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode(); + const response = await axios.get(`${node.baseUrl}/api/metrics/data`, { + timeout: this.timeout + }); + return response.data; + } + + // Utility methods + private getNodeById(nodeId: string): TestNode { + const node = this.nodes.find(n => n.id === nodeId); + if (!node) { + throw new Error(`Node with id ${nodeId} not found`); + } + return node; + } + + async waitForDataReplication( + checkFunction: () => Promise, + maxWaitMs: number = 10000, + intervalMs: number = 500 + ): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitMs) { + if (await checkFunction()) { + return; + } + await this.sleep(intervalMs); + } + + throw new Error(`Data replication timeout after ${maxWaitMs}ms`); + } + + generateTestData() { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(7); + + return { + user: { + username: `testuser_${random}`, + email: `test_${random}@example.com`, + displayName: `Test User ${random}`, + roles: ['user'] + } as TestUser, + + category: { + name: `Test Category ${random}`, + description: `Test category created at ${timestamp}`, + color: '#ff0000' + } as TestCategory, + + post: (authorId: string, categoryId?: string) => ({ + title: `Test Post ${random}`, + content: `This is test content created at ${timestamp}`, + excerpt: `Test excerpt ${random}`, + authorId, + categoryId, + tags: ['test', 'integration'], + status: 'draft' as const + } as TestPost), + + comment: (postId: string, authorId: string) => ({ + content: `Test comment created at ${timestamp}`, + postId, + authorId + } as TestComment) + }; + } +} + +export const blogTestHelper = new BlogTestHelper(); diff --git a/tests/real-integration/shared/infrastructure/DockerNodeManager.ts b/tests/real-integration/shared/infrastructure/DockerNodeManager.ts new file mode 100644 index 0000000..68513cc --- /dev/null +++ b/tests/real-integration/shared/infrastructure/DockerNodeManager.ts @@ -0,0 +1,170 @@ +import { spawn, ChildProcess } from 'child_process'; +import { ApiClient } from '../utils/ApiClient'; +import { SyncWaiter } from '../utils/SyncWaiter'; + +export interface NodeConfig { + nodeId: string; + apiPort: number; + ipfsPort: number; + nodeType: string; +} + +export interface DockerComposeConfig { + composeFile: string; + scenario: string; + nodes: NodeConfig[]; +} + +export class DockerNodeManager { + private process: ChildProcess | null = null; + private apiClients: ApiClient[] = []; + private syncWaiter: SyncWaiter; + + constructor(private config: DockerComposeConfig) { + // Create API clients for each node + this.apiClients = this.config.nodes.map(node => + new ApiClient(`http://localhost:${node.apiPort}`) + ); + + this.syncWaiter = new SyncWaiter(this.apiClients); + } + + async startCluster(): Promise { + console.log(`🚀 Starting ${this.config.scenario} cluster...`); + + try { + // Start docker-compose + this.process = spawn('docker-compose', [ + '-f', this.config.composeFile, + 'up', + '--build', + '--force-recreate' + ], { + stdio: 'pipe', + cwd: process.cwd() + }); + + // Log output + this.process.stdout?.on('data', (data) => { + console.log(`[DOCKER] ${data.toString().trim()}`); + }); + + this.process.stderr?.on('data', (data) => { + console.error(`[DOCKER ERROR] ${data.toString().trim()}`); + }); + + // Wait for nodes to be ready + const ready = await this.syncWaiter.waitForNodesReady(120000); + if (!ready) { + throw new Error('Nodes failed to become ready'); + } + + // Wait for peer connections + const connected = await this.syncWaiter.waitForPeerConnections( + this.config.nodes.length - 1, // Each node should connect to all others + 60000 + ); + + if (!connected) { + throw new Error('Nodes failed to establish peer connections'); + } + + console.log(`✅ ${this.config.scenario} cluster started successfully`); + return true; + + } catch (error) { + console.error(`❌ Failed to start cluster: ${error.message}`); + await this.stopCluster(); + return false; + } + } + + async stopCluster(): Promise { + console.log(`🛑 Stopping ${this.config.scenario} cluster...`); + + try { + if (this.process) { + this.process.kill('SIGTERM'); + + // Wait for graceful shutdown + await new Promise((resolve) => { + this.process?.on('exit', resolve); + setTimeout(resolve, 10000); // Force kill after 10s + }); + } + + // Clean up docker containers and volumes + const cleanup = spawn('docker-compose', [ + '-f', this.config.composeFile, + 'down', + '-v', + '--remove-orphans' + ], { + stdio: 'inherit', + cwd: process.cwd() + }); + + await new Promise((resolve) => { + cleanup.on('exit', resolve); + }); + + console.log(`✅ ${this.config.scenario} cluster stopped`); + + } catch (error) { + console.error(`❌ Error stopping cluster: ${error.message}`); + } + } + + getApiClient(nodeIndex: number): ApiClient { + if (nodeIndex >= this.apiClients.length) { + throw new Error(`Node index ${nodeIndex} is out of range`); + } + return this.apiClients[nodeIndex]; + } + + getSyncWaiter(): SyncWaiter { + return this.syncWaiter; + } + + async waitForSync(timeout: number = 10000): Promise { + await this.syncWaiter.waitForSync(timeout); + } + + async getNetworkMetrics(): Promise { + const metrics = await this.syncWaiter.getSyncMetrics(); + + return { + totalNodes: this.config.nodes.length, + readyNodes: metrics.length, + averagePeers: metrics.length > 0 + ? metrics.reduce((sum, m) => sum + m.peerCount, 0) / metrics.length + : 0, + nodeMetrics: metrics + }; + } + + async logClusterStatus(): Promise { + console.log(`\n📋 ${this.config.scenario} Cluster Status:`); + console.log(`Nodes: ${this.config.nodes.length}`); + + const networkMetrics = await this.getNetworkMetrics(); + console.log(`Ready: ${networkMetrics.readyNodes}/${networkMetrics.totalNodes}`); + console.log(`Average Peers: ${networkMetrics.averagePeers.toFixed(1)}`); + + await this.syncWaiter.logSyncStatus(); + } + + async healthCheck(): Promise { + try { + const results = await Promise.all( + this.apiClients.map(client => client.health()) + ); + + return results.every(result => + result.status === 200 && result.data?.status === 'healthy' + ); + } catch (error) { + return false; + } + } +} \ No newline at end of file diff --git a/tests/real-integration/shared/infrastructure/docker/Dockerfile.bootstrap b/tests/real-integration/shared/infrastructure/docker/Dockerfile.bootstrap new file mode 100644 index 0000000..9021087 --- /dev/null +++ b/tests/real-integration/shared/infrastructure/docker/Dockerfile.bootstrap @@ -0,0 +1,18 @@ +# Bootstrap node for IPFS peer discovery +FROM ipfs/kubo:v0.24.0 + +# Copy swarm key +COPY tests/real-integration/blog-scenario/docker/swarm.key /data/ipfs/swarm.key + +# Copy configuration script +COPY tests/real-integration/blog-scenario/docker/bootstrap-config.sh /usr/local/bin/bootstrap-config.sh +USER root +RUN chmod +x /usr/local/bin/bootstrap-config.sh +USER ipfs + +# Expose IPFS ports +EXPOSE 4001 5001 8080 + +# Override the kubo entrypoint and start IPFS daemon with custom config +ENTRYPOINT [] +CMD ["sh", "/usr/local/bin/bootstrap-config.sh"] diff --git a/tests/real-integration/shared/utils/ApiClient.ts b/tests/real-integration/shared/utils/ApiClient.ts new file mode 100644 index 0000000..3d9b360 --- /dev/null +++ b/tests/real-integration/shared/utils/ApiClient.ts @@ -0,0 +1,123 @@ +import fetch from 'node-fetch'; + +export interface ApiResponse { + data?: T; + error?: string; + status: number; +} + +export class ApiClient { + constructor(private baseUrl: string) {} + + async get(path: string): Promise> { + try { + const response = await fetch(`${this.baseUrl}${path}`); + const data = await response.json(); + + return { + data: response.ok ? data : undefined, + error: response.ok ? undefined : data.error || 'Request failed', + status: response.status + }; + } catch (error) { + return { + error: error.message, + status: 0 + }; + } + } + + async post(path: string, body: any): Promise> { + try { + const response = await fetch(`${this.baseUrl}${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); + + const data = await response.json(); + + return { + data: response.ok ? data : undefined, + error: response.ok ? undefined : data.error || 'Request failed', + status: response.status + }; + } catch (error) { + return { + error: error.message, + status: 0 + }; + } + } + + async put(path: string, body: any): Promise> { + try { + const response = await fetch(`${this.baseUrl}${path}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); + + const data = await response.json(); + + return { + data: response.ok ? data : undefined, + error: response.ok ? undefined : data.error || 'Request failed', + status: response.status + }; + } catch (error) { + return { + error: error.message, + status: 0 + }; + } + } + + async delete(path: string): Promise> { + try { + const response = await fetch(`${this.baseUrl}${path}`, { + method: 'DELETE' + }); + + const data = response.status === 204 ? {} : await response.json(); + + return { + data: response.ok ? data : undefined, + error: response.ok ? undefined : data.error || 'Request failed', + status: response.status + }; + } catch (error) { + return { + error: error.message, + status: 0 + }; + } + } + + async health(): Promise> { + return this.get('/health'); + } + + async waitForHealth(timeout: number = 30000): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + try { + const response = await this.health(); + if (response.status === 200 && response.data?.status === 'healthy') { + return true; + } + } catch (error) { + // Continue waiting + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + return false; + } +} \ No newline at end of file diff --git a/tests/real-integration/shared/utils/SyncWaiter.ts b/tests/real-integration/shared/utils/SyncWaiter.ts new file mode 100644 index 0000000..ea1f3a6 --- /dev/null +++ b/tests/real-integration/shared/utils/SyncWaiter.ts @@ -0,0 +1,166 @@ +import { ApiClient } from './ApiClient'; + +export interface SyncMetrics { + nodeId: string; + peerCount: number; + dataCount: { + users: number; + posts: number; + comments: number; + categories: number; + }; +} + +export class SyncWaiter { + constructor(private apiClients: ApiClient[]) {} + + async waitForSync(timeout: number = 10000): Promise { + await new Promise(resolve => setTimeout(resolve, timeout)); + } + + async waitForPeerConnections(minPeers: number = 2, timeout: number = 30000): Promise { + const startTime = Date.now(); + + console.log(`Waiting for nodes to connect to at least ${minPeers} peers...`); + + while (Date.now() - startTime < timeout) { + let allConnected = true; + + for (const client of this.apiClients) { + try { + const health = await client.health(); + if (!health.data || health.data.peers < minPeers) { + allConnected = false; + break; + } + } catch (error) { + allConnected = false; + break; + } + } + + if (allConnected) { + console.log('✅ All nodes have sufficient peer connections'); + return true; + } + + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + console.log('❌ Timeout waiting for peer connections'); + return false; + } + + async waitForDataConsistency( + dataType: 'users' | 'posts' | 'comments' | 'categories', + expectedCount: number, + timeout: number = 15000, + tolerance: number = 0 + ): Promise { + const startTime = Date.now(); + + console.log(`Waiting for ${dataType} count to reach ${expectedCount} across all nodes...`); + + while (Date.now() - startTime < timeout) { + let isConsistent = true; + const counts: number[] = []; + + for (const client of this.apiClients) { + try { + const response = await client.get('/api/metrics/data'); + if (response.data && response.data.counts) { + const count = response.data.counts[dataType]; + counts.push(count); + + if (Math.abs(count - expectedCount) > tolerance) { + isConsistent = false; + } + } else { + isConsistent = false; + break; + } + } catch (error) { + isConsistent = false; + break; + } + } + + if (isConsistent) { + console.log(`✅ Data consistency achieved: ${dataType} = ${expectedCount} across all nodes`); + return true; + } + + console.log(`Data counts: ${counts.join(', ')}, expected: ${expectedCount}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + console.log(`❌ Timeout waiting for data consistency: ${dataType}`); + return false; + } + + async getSyncMetrics(): Promise { + const metrics: SyncMetrics[] = []; + + for (const client of this.apiClients) { + try { + const [healthResponse, dataResponse] = await Promise.all([ + client.health(), + client.get('/api/metrics/data') + ]); + + if (healthResponse.data && dataResponse.data) { + metrics.push({ + nodeId: healthResponse.data.nodeId, + peerCount: healthResponse.data.peers, + dataCount: dataResponse.data.counts + }); + } + } catch (error) { + console.warn(`Failed to get metrics from node: ${error.message}`); + } + } + + return metrics; + } + + async logSyncStatus(): Promise { + console.log('\n📊 Current Sync Status:'); + const metrics = await this.getSyncMetrics(); + + metrics.forEach(metric => { + console.log(`Node: ${metric.nodeId}`); + console.log(` Peers: ${metric.peerCount}`); + console.log(` Data: Users=${metric.dataCount.users}, Posts=${metric.dataCount.posts}, Comments=${metric.dataCount.comments}, Categories=${metric.dataCount.categories}`); + }); + console.log(''); + } + + async waitForNodesReady(timeout: number = 60000): Promise { + const startTime = Date.now(); + + console.log('Waiting for all nodes to be ready...'); + + while (Date.now() - startTime < timeout) { + let allReady = true; + + for (let i = 0; i < this.apiClients.length; i++) { + const isReady = await this.apiClients[i].waitForHealth(5000); + if (!isReady) { + console.log(`Node ${i} not ready yet...`); + allReady = false; + break; + } + } + + if (allReady) { + console.log('✅ All nodes are ready'); + return true; + } + + await new Promise(resolve => setTimeout(resolve, 3000)); + } + + console.log('❌ Timeout waiting for nodes to be ready'); + return false; + } +} \ No newline at end of file diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..6160d4b --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,10 @@ +// Test setup file +import 'reflect-metadata'; + +// Global test configuration +jest.setTimeout(30000); + +// Setup global test utilities +global.beforeEach(() => { + jest.clearAllMocks(); +}); diff --git a/tests/unit/core/DatabaseManager.test.ts b/tests/unit/core/DatabaseManager.test.ts new file mode 100644 index 0000000..4fecfa1 --- /dev/null +++ b/tests/unit/core/DatabaseManager.test.ts @@ -0,0 +1,440 @@ +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { DatabaseManager, UserMappingsData } from '../../../src/framework/core/DatabaseManager'; +import { FrameworkOrbitDBService } from '../../../src/framework/services/OrbitDBService'; +import { ModelRegistry } from '../../../src/framework/core/ModelRegistry'; +import { createMockServices } from '../../mocks/services'; +import { BaseModel } from '../../../src/framework/models/BaseModel'; +import { Model, Field } from '../../../src/framework/models/decorators'; + +// Test models for DatabaseManager testing +@Model({ + scope: 'global', + type: 'docstore' +}) +class GlobalTestModel extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; +} + +@Model({ + scope: 'user', + type: 'keyvalue' +}) +class UserTestModel extends BaseModel { + @Field({ type: 'string', required: true }) + name: string; +} + +describe('DatabaseManager', () => { + let databaseManager: DatabaseManager; + let mockOrbitDBService: FrameworkOrbitDBService; + let mockDatabase: any; + let mockOrbitDB: any; + + beforeEach(() => { + const mockServices = createMockServices(); + mockOrbitDBService = mockServices.orbitDBService; + + // Create mock database + mockDatabase = { + address: { toString: () => 'mock-address-123' }, + set: jest.fn().mockResolvedValue(undefined), + get: jest.fn().mockResolvedValue(null), + put: jest.fn().mockResolvedValue('mock-hash'), + add: jest.fn().mockResolvedValue('mock-hash'), + del: jest.fn().mockResolvedValue(undefined), + query: jest.fn().mockReturnValue([]), + iterator: jest.fn().mockReturnValue({ + collect: jest.fn().mockReturnValue([]) + }), + all: jest.fn().mockReturnValue({}), + value: 0, + id: 'mock-counter-id', + inc: jest.fn().mockResolvedValue(undefined) + }; + + mockOrbitDB = { + open: jest.fn().mockResolvedValue(mockDatabase) + }; + + // Mock OrbitDB service methods + jest.spyOn(mockOrbitDBService, 'openDatabase').mockResolvedValue(mockDatabase); + jest.spyOn(mockOrbitDBService, 'getOrbitDB').mockReturnValue(mockOrbitDB); + + // Mock ModelRegistry + jest.spyOn(ModelRegistry, 'getGlobalModels').mockReturnValue([ + { modelName: 'GlobalTestModel', dbType: 'docstore' } + ]); + jest.spyOn(ModelRegistry, 'getUserScopedModels').mockReturnValue([ + { modelName: 'UserTestModel', dbType: 'keyvalue' } + ]); + + databaseManager = new DatabaseManager(mockOrbitDBService); + jest.clearAllMocks(); + }); + + describe('Initialization', () => { + it('should initialize all databases correctly', async () => { + await databaseManager.initializeAllDatabases(); + + // Should create global databases + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith( + 'global-globaltestmodel', + 'docstore' + ); + + // Should create system directory shards + for (let i = 0; i < 4; i++) { + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith( + `global-user-directory-shard-${i}`, + 'keyvalue' + ); + } + }); + + it('should not initialize databases twice', async () => { + await databaseManager.initializeAllDatabases(); + const firstCallCount = (mockOrbitDBService.openDatabase as jest.Mock).mock.calls.length; + + await databaseManager.initializeAllDatabases(); + const secondCallCount = (mockOrbitDBService.openDatabase as jest.Mock).mock.calls.length; + + expect(secondCallCount).toBe(firstCallCount); + }); + + it('should handle database creation errors', async () => { + jest.spyOn(mockOrbitDBService, 'openDatabase').mockRejectedValueOnce(new Error('Creation failed')); + + await expect(databaseManager.initializeAllDatabases()).rejects.toThrow('Creation failed'); + }); + }); + + describe('User Database Management', () => { + beforeEach(async () => { + // Initialize global databases first + await databaseManager.initializeAllDatabases(); + }); + + it('should create user databases correctly', async () => { + const userId = 'test-user-123'; + + const userMappings = await databaseManager.createUserDatabases(userId); + + expect(userMappings).toBeInstanceOf(UserMappingsData); + expect(userMappings.userId).toBe(userId); + expect(userMappings.databases).toHaveProperty('usertestmodelDB'); + + // Should create mappings database + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith( + `${userId}-mappings`, + 'keyvalue' + ); + + // Should create user model database + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith( + `${userId}-usertestmodel`, + 'keyvalue' + ); + + // Should store mappings in database + expect(mockDatabase.set).toHaveBeenCalledWith('mappings', expect.any(Object)); + }); + + it('should retrieve user mappings from cache', async () => { + const userId = 'test-user-456'; + + // Create user databases first + const originalMappings = await databaseManager.createUserDatabases(userId); + jest.clearAllMocks(); + + // Get mappings again - should come from cache + const cachedMappings = await databaseManager.getUserMappings(userId); + + expect(cachedMappings).toBe(originalMappings); + expect(mockDatabase.get).not.toHaveBeenCalled(); + }); + + it('should retrieve user mappings from global directory', async () => { + const userId = 'test-user-789'; + const mappingsAddress = 'mock-mappings-address'; + const mappingsData = { usertestmodelDB: 'mock-db-address' }; + + // Mock directory shard return + mockDatabase.get + .mockResolvedValueOnce(mappingsAddress) // From directory shard + .mockResolvedValueOnce(mappingsData); // From mappings DB + + const userMappings = await databaseManager.getUserMappings(userId); + + expect(userMappings).toBeInstanceOf(UserMappingsData); + expect(userMappings.userId).toBe(userId); + expect(userMappings.databases).toEqual(mappingsData); + + // Should open mappings database + expect(mockOrbitDB.open).toHaveBeenCalledWith(mappingsAddress); + }); + + it('should handle user not found in directory', async () => { + const userId = 'nonexistent-user'; + + // Mock directory shard returning null + mockDatabase.get.mockResolvedValue(null); + + await expect(databaseManager.getUserMappings(userId)).rejects.toThrow( + `User ${userId} not found in directory` + ); + }); + + it('should get user database correctly', async () => { + const userId = 'test-user-db'; + const modelName = 'UserTestModel'; + + // Create user databases first + await databaseManager.createUserDatabases(userId); + + const userDB = await databaseManager.getUserDatabase(userId, modelName); + + expect(userDB).toBe(mockDatabase); + }); + + it('should handle missing user database', async () => { + const userId = 'test-user-missing'; + const modelName = 'NonExistentModel'; + + // Create user databases first + await databaseManager.createUserDatabases(userId); + + await expect(databaseManager.getUserDatabase(userId, modelName)).rejects.toThrow( + `Database not found for user ${userId} and model ${modelName}` + ); + }); + }); + + describe('Global Database Management', () => { + beforeEach(async () => { + await databaseManager.initializeAllDatabases(); + }); + + it('should get global database correctly', async () => { + const globalDB = await databaseManager.getGlobalDatabase('GlobalTestModel'); + + expect(globalDB).toBe(mockDatabase); + }); + + it('should handle missing global database', async () => { + await expect(databaseManager.getGlobalDatabase('NonExistentModel')).rejects.toThrow( + 'Global database not found for model: NonExistentModel' + ); + }); + + it('should get global directory shards', async () => { + const shards = await databaseManager.getGlobalDirectoryShards(); + + expect(shards).toHaveLength(4); + expect(shards.every(shard => shard === mockDatabase)).toBe(true); + }); + }); + + describe('Database Operations', () => { + beforeEach(async () => { + await databaseManager.initializeAllDatabases(); + }); + + describe('getAllDocuments', () => { + it('should get all documents from eventlog', async () => { + const mockDocs = [{ id: '1', data: 'test' }]; + mockDatabase.iterator.mockReturnValue({ + collect: jest.fn().mockReturnValue(mockDocs) + }); + + const docs = await databaseManager.getAllDocuments(mockDatabase, 'eventlog'); + + expect(docs).toEqual(mockDocs); + expect(mockDatabase.iterator).toHaveBeenCalled(); + }); + + it('should get all documents from keyvalue', async () => { + const mockData = { key1: { id: '1' }, key2: { id: '2' } }; + mockDatabase.all.mockReturnValue(mockData); + + const docs = await databaseManager.getAllDocuments(mockDatabase, 'keyvalue'); + + expect(docs).toEqual([{ id: '1' }, { id: '2' }]); + expect(mockDatabase.all).toHaveBeenCalled(); + }); + + it('should get all documents from docstore', async () => { + const mockDocs = [{ id: '1' }, { id: '2' }]; + mockDatabase.query.mockReturnValue(mockDocs); + + const docs = await databaseManager.getAllDocuments(mockDatabase, 'docstore'); + + expect(docs).toEqual(mockDocs); + expect(mockDatabase.query).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should get documents from counter', async () => { + mockDatabase.value = 42; + mockDatabase.id = 'counter-123'; + + const docs = await databaseManager.getAllDocuments(mockDatabase, 'counter'); + + expect(docs).toEqual([{ value: 42, id: 'counter-123' }]); + }); + + it('should handle unsupported database type', async () => { + await expect( + databaseManager.getAllDocuments(mockDatabase, 'unsupported' as any) + ).rejects.toThrow('Unsupported database type: unsupported'); + }); + }); + + describe('addDocument', () => { + it('should add document to eventlog', async () => { + const data = { content: 'test' }; + mockDatabase.add.mockResolvedValue('hash123'); + + const result = await databaseManager.addDocument(mockDatabase, 'eventlog', data); + + expect(result).toBe('hash123'); + expect(mockDatabase.add).toHaveBeenCalledWith(data); + }); + + it('should add document to keyvalue', async () => { + const data = { id: 'key1', content: 'test' }; + + const result = await databaseManager.addDocument(mockDatabase, 'keyvalue', data); + + expect(result).toBe('key1'); + expect(mockDatabase.set).toHaveBeenCalledWith('key1', data); + }); + + it('should add document to docstore', async () => { + const data = { id: 'doc1', content: 'test' }; + mockDatabase.put.mockResolvedValue('hash123'); + + const result = await databaseManager.addDocument(mockDatabase, 'docstore', data); + + expect(result).toBe('hash123'); + expect(mockDatabase.put).toHaveBeenCalledWith(data); + }); + + it('should increment counter', async () => { + const data = { amount: 5 }; + mockDatabase.id = 'counter-123'; + + const result = await databaseManager.addDocument(mockDatabase, 'counter', data); + + expect(result).toBe('counter-123'); + expect(mockDatabase.inc).toHaveBeenCalledWith(5); + }); + }); + + describe('updateDocument', () => { + it('should update document in keyvalue', async () => { + const data = { id: 'key1', content: 'updated' }; + + await databaseManager.updateDocument(mockDatabase, 'keyvalue', 'key1', data); + + expect(mockDatabase.set).toHaveBeenCalledWith('key1', data); + }); + + it('should update document in docstore', async () => { + const data = { id: 'doc1', content: 'updated' }; + + await databaseManager.updateDocument(mockDatabase, 'docstore', 'doc1', data); + + expect(mockDatabase.put).toHaveBeenCalledWith(data); + }); + + it('should add new entry for append-only stores', async () => { + const data = { id: 'event1', content: 'updated' }; + mockDatabase.add.mockResolvedValue('hash123'); + + await databaseManager.updateDocument(mockDatabase, 'eventlog', 'event1', data); + + expect(mockDatabase.add).toHaveBeenCalledWith(data); + }); + }); + + describe('deleteDocument', () => { + it('should delete document from keyvalue', async () => { + await databaseManager.deleteDocument(mockDatabase, 'keyvalue', 'key1'); + + expect(mockDatabase.del).toHaveBeenCalledWith('key1'); + }); + + it('should delete document from docstore', async () => { + await databaseManager.deleteDocument(mockDatabase, 'docstore', 'doc1'); + + expect(mockDatabase.del).toHaveBeenCalledWith('doc1'); + }); + + it('should add deletion marker for append-only stores', async () => { + mockDatabase.add.mockResolvedValue('hash123'); + + await databaseManager.deleteDocument(mockDatabase, 'eventlog', 'event1'); + + expect(mockDatabase.add).toHaveBeenCalledWith({ + _deleted: true, + id: 'event1', + deletedAt: expect.any(Number) + }); + }); + }); + }); + + describe('Shard Index Calculation', () => { + it('should calculate consistent shard indices', async () => { + await databaseManager.initializeAllDatabases(); + + const userId1 = 'user-123'; + const userId2 = 'user-456'; + + // Create users and verify they're stored in shards + await databaseManager.createUserDatabases(userId1); + await databaseManager.createUserDatabases(userId2); + + // The shard index should be consistent for the same user + const calls = (mockDatabase.set as jest.Mock).mock.calls; + const user1Calls = calls.filter(call => call[0] === userId1); + const user2Calls = calls.filter(call => call[0] === userId2); + + expect(user1Calls).toHaveLength(1); + expect(user2Calls).toHaveLength(1); + }); + }); + + describe('Error Handling', () => { + it('should handle database operation errors', async () => { + await databaseManager.initializeAllDatabases(); + + mockDatabase.put.mockRejectedValue(new Error('Database error')); + + await expect( + databaseManager.addDocument(mockDatabase, 'docstore', { id: 'test' }) + ).rejects.toThrow('Database error'); + }); + + it('should handle missing global directory', async () => { + // Don't initialize databases + const userId = 'test-user'; + + await expect(databaseManager.getUserMappings(userId)).rejects.toThrow( + 'Global directory not initialized' + ); + }); + }); + + describe('Cleanup', () => { + it('should stop and clear all resources', async () => { + await databaseManager.initializeAllDatabases(); + await databaseManager.createUserDatabases('test-user'); + + await databaseManager.stop(); + + // After stopping, initialization should be required again + await expect(databaseManager.getGlobalDatabase('GlobalTestModel')).rejects.toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/decorators/decorators.test.ts b/tests/unit/decorators/decorators.test.ts new file mode 100644 index 0000000..1bb339b --- /dev/null +++ b/tests/unit/decorators/decorators.test.ts @@ -0,0 +1,478 @@ +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { BaseModel } from '../../../src/framework/models/BaseModel'; +import { + Model, + Field, + BelongsTo, + HasMany, + HasOne, + ManyToMany, + BeforeCreate, + AfterCreate, + BeforeUpdate, + AfterUpdate, + BeforeDelete, + AfterDelete, + getFieldConfig, + getRelationshipConfig, + getHooks +} from '../../../src/framework/models/decorators'; + +describe('Decorators', () => { + describe('@Model Decorator', () => { + it('should define model metadata correctly', () => { + @Model({ + scope: 'global', + type: 'docstore', + sharding: { + strategy: 'hash', + count: 4, + key: 'id' + } + }) + class TestModel extends BaseModel {} + + expect(TestModel.scope).toBe('global'); + expect(TestModel.storeType).toBe('docstore'); + expect(TestModel.sharding).toEqual({ + strategy: 'hash', + count: 4, + key: 'id' + }); + }); + + it('should apply default model configuration', () => { + @Model({}) + class DefaultModel extends BaseModel {} + + expect(DefaultModel.scope).toBe('global'); + expect(DefaultModel.storeType).toBe('docstore'); + }); + + it('should register model with ModelRegistry', () => { + @Model({ + scope: 'user', + type: 'eventlog' + }) + class RegistryModel extends BaseModel {} + + // The model should be automatically registered + expect(RegistryModel.scope).toBe('user'); + expect(RegistryModel.storeType).toBe('eventlog'); + }); + }); + + describe('@Field Decorator', () => { + @Model({}) + class FieldTestModel extends BaseModel { + @Field({ type: 'string', required: true }) + requiredField: string; + + @Field({ type: 'number', required: false, default: 42 }) + defaultField: number; + + @Field({ + type: 'string', + required: true, + validate: (value: string) => value.length >= 3, + transform: (value: string) => value.toLowerCase() + }) + validatedField: string; + + @Field({ type: 'array', required: false, default: [] }) + arrayField: string[]; + + @Field({ type: 'boolean', required: false, default: true }) + booleanField: boolean; + + @Field({ type: 'object', required: false }) + objectField: Record; + } + + it('should define field metadata correctly', () => { + const requiredFieldConfig = getFieldConfig(FieldTestModel, 'requiredField'); + expect(requiredFieldConfig).toEqual({ + type: 'string', + required: true + }); + + const defaultFieldConfig = getFieldConfig(FieldTestModel, 'defaultField'); + expect(defaultFieldConfig).toEqual({ + type: 'number', + required: false, + default: 42 + }); + }); + + it('should handle field validation configuration', () => { + const validatedFieldConfig = getFieldConfig(FieldTestModel, 'validatedField'); + + expect(validatedFieldConfig.type).toBe('string'); + expect(validatedFieldConfig.required).toBe(true); + expect(typeof validatedFieldConfig.validate).toBe('function'); + expect(typeof validatedFieldConfig.transform).toBe('function'); + }); + + it('should apply field validation', () => { + const validatedFieldConfig = getFieldConfig(FieldTestModel, 'validatedField'); + + expect(validatedFieldConfig.validate!('test')).toBe(true); + expect(validatedFieldConfig.validate!('hi')).toBe(false); // Less than 3 characters + }); + + it('should apply field transformation', () => { + const validatedFieldConfig = getFieldConfig(FieldTestModel, 'validatedField'); + + expect(validatedFieldConfig.transform!('TEST')).toBe('test'); + expect(validatedFieldConfig.transform!('MixedCase')).toBe('mixedcase'); + }); + + it('should handle different field types', () => { + const arrayFieldConfig = getFieldConfig(FieldTestModel, 'arrayField'); + expect(arrayFieldConfig.type).toBe('array'); + expect(arrayFieldConfig.default).toEqual([]); + + const booleanFieldConfig = getFieldConfig(FieldTestModel, 'booleanField'); + expect(booleanFieldConfig.type).toBe('boolean'); + expect(booleanFieldConfig.default).toBe(true); + + const objectFieldConfig = getFieldConfig(FieldTestModel, 'objectField'); + expect(objectFieldConfig.type).toBe('object'); + expect(objectFieldConfig.required).toBe(false); + }); + }); + + describe('Relationship Decorators', () => { + @Model({}) + class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true }) + userId: string; + + @BelongsTo(() => User, 'userId') + user: any; + } + + @Model({}) + class Profile extends BaseModel { + @Field({ type: 'string', required: true }) + userId: string; + + @BelongsTo(() => User, 'userId') + user: any; + } + + @Model({}) + class Role extends BaseModel { + @Field({ type: 'string', required: true }) + name: string; + + @ManyToMany(() => User, 'user_roles', 'roleId', 'userId') + users: any[]; + } + + @Model({}) + class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + @HasMany(() => Post, 'userId') + posts: any[]; + + @HasOne(() => Profile, 'userId') + profile: any; + + @ManyToMany(() => Role, 'user_roles', 'userId', 'roleId') + roles: any[]; + } + + it('should define BelongsTo relationships correctly', () => { + const relationships = getRelationshipConfig(Post); + const userRelation = relationships.find(r => r.propertyKey === 'user'); + + expect(userRelation).toBeDefined(); + expect(userRelation?.type).toBe('belongsTo'); + expect(userRelation?.targetModel()).toBe(User); + expect(userRelation?.foreignKey).toBe('userId'); + }); + + it('should define HasMany relationships correctly', () => { + const relationships = getRelationshipConfig(User); + const postsRelation = relationships.find(r => r.propertyKey === 'posts'); + + expect(postsRelation).toBeDefined(); + expect(postsRelation?.type).toBe('hasMany'); + expect(postsRelation?.targetModel()).toBe(Post); + expect(postsRelation?.foreignKey).toBe('userId'); + }); + + it('should define HasOne relationships correctly', () => { + const relationships = getRelationshipConfig(User); + const profileRelation = relationships.find(r => r.propertyKey === 'profile'); + + expect(profileRelation).toBeDefined(); + expect(profileRelation?.type).toBe('hasOne'); + expect(profileRelation?.targetModel()).toBe(Profile); + expect(profileRelation?.foreignKey).toBe('userId'); + }); + + it('should define ManyToMany relationships correctly', () => { + const relationships = getRelationshipConfig(User); + const rolesRelation = relationships.find(r => r.propertyKey === 'roles'); + + expect(rolesRelation).toBeDefined(); + expect(rolesRelation?.type).toBe('manyToMany'); + expect(rolesRelation?.targetModel()).toBe(Role); + expect(rolesRelation?.through).toBe('user_roles'); + expect(rolesRelation?.foreignKey).toBe('userId'); + expect(rolesRelation?.otherKey).toBe('roleId'); + }); + + it('should support relationship options', () => { + @Model({}) + class TestModel extends BaseModel { + @HasMany(() => Post, 'userId', { + cache: true, + eager: false, + orderBy: 'createdAt', + limit: 10 + }) + posts: Post[]; + } + + const relationships = getRelationshipConfig(TestModel); + const postsRelation = relationships.find(r => r.propertyKey === 'posts'); + + expect(postsRelation?.options).toEqual({ + cache: true, + eager: false, + orderBy: 'createdAt', + limit: 10 + }); + }); + }); + + describe('Hook Decorators', () => { + let hookCallOrder: string[] = []; + + @Model({}) + class HookTestModel extends BaseModel { + @Field({ type: 'string', required: true }) + name: string; + + @BeforeCreate() + beforeCreateHook() { + hookCallOrder.push('beforeCreate'); + } + + @AfterCreate() + afterCreateHook() { + hookCallOrder.push('afterCreate'); + } + + @BeforeUpdate() + beforeUpdateHook() { + hookCallOrder.push('beforeUpdate'); + } + + @AfterUpdate() + afterUpdateHook() { + hookCallOrder.push('afterUpdate'); + } + + @BeforeDelete() + beforeDeleteHook() { + hookCallOrder.push('beforeDelete'); + } + + @AfterDelete() + afterDeleteHook() { + hookCallOrder.push('afterDelete'); + } + } + + beforeEach(() => { + hookCallOrder = []; + }); + + it('should register lifecycle hooks correctly', () => { + const hooks = getHooks(HookTestModel); + + expect(hooks.beforeCreate).toContain('beforeCreateHook'); + expect(hooks.afterCreate).toContain('afterCreateHook'); + expect(hooks.beforeUpdate).toContain('beforeUpdateHook'); + expect(hooks.afterUpdate).toContain('afterUpdateHook'); + expect(hooks.beforeDelete).toContain('beforeDeleteHook'); + expect(hooks.afterDelete).toContain('afterDeleteHook'); + }); + + it('should support multiple hooks of the same type', () => { + @Model({}) + class MultiHookModel extends BaseModel { + @BeforeCreate() + firstBeforeCreate() { + hookCallOrder.push('first'); + } + + @BeforeCreate() + secondBeforeCreate() { + hookCallOrder.push('second'); + } + } + + const hooks = getHooks(MultiHookModel); + expect(hooks.beforeCreate).toHaveLength(2); + expect(hooks.beforeCreate).toContain('firstBeforeCreate'); + expect(hooks.beforeCreate).toContain('secondBeforeCreate'); + }); + }); + + describe('Complex Decorator Combinations', () => { + it('should handle models with all decorator types', () => { + @Model({ + scope: 'user', + type: 'docstore', + sharding: { + strategy: 'user', + count: 2, + key: 'userId' + } + }) + class ComplexModel extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true }) + userId: string; + + @Field({ + type: 'array', + required: false, + default: [], + transform: (tags: string[]) => tags.map(t => t.toLowerCase()) + }) + tags: string[]; + + @BelongsTo(() => User, 'userId') + user: User; + + @BeforeCreate() + setDefaults() { + this.tags = this.tags || []; + } + + @BeforeUpdate() + updateTimestamp() { + // Update logic + } + } + + // Check model configuration + expect(ComplexModel.scope).toBe('user'); + expect(ComplexModel.storeType).toBe('docstore'); + expect(ComplexModel.sharding).toEqual({ + strategy: 'user', + count: 2, + key: 'userId' + }); + + // Check field configuration + const titleConfig = getFieldConfig(ComplexModel, 'title'); + expect(titleConfig.required).toBe(true); + + const tagsConfig = getFieldConfig(ComplexModel, 'tags'); + expect(tagsConfig.default).toEqual([]); + expect(typeof tagsConfig.transform).toBe('function'); + + // Check relationships + const relationships = getRelationshipConfig(ComplexModel); + const userRelation = relationships.find(r => r.propertyKey === 'user'); + expect(userRelation?.type).toBe('belongsTo'); + + // Check hooks + const hooks = getHooks(ComplexModel); + expect(hooks.beforeCreate).toContain('setDefaults'); + expect(hooks.beforeUpdate).toContain('updateTimestamp'); + }); + }); + + describe('Decorator Error Handling', () => { + it('should handle invalid field types', () => { + expect(() => { + @Model({}) + class InvalidFieldModel extends BaseModel { + @Field({ type: 'invalid-type' as any, required: true }) + invalidField: any; + } + }).toThrow(); + }); + + it('should handle invalid model scope', () => { + expect(() => { + @Model({ scope: 'invalid-scope' as any }) + class InvalidScopeModel extends BaseModel {} + }).toThrow(); + }); + + it('should handle invalid store type', () => { + expect(() => { + @Model({ type: 'invalid-store' as any }) + class InvalidStoreModel extends BaseModel {} + }).toThrow(); + }); + }); + + describe('Metadata Inheritance', () => { + @Model({ + scope: 'global', + type: 'docstore' + }) + class BaseTestModel extends BaseModel { + @Field({ type: 'string', required: true }) + baseField: string; + + @BeforeCreate() + baseHook() { + // Base hook + } + } + + @Model({ + scope: 'user', // Override scope + type: 'eventlog' // Override type + }) + class ExtendedTestModel extends BaseTestModel { + @Field({ type: 'number', required: false }) + extendedField: number; + + @BeforeCreate() + extendedHook() { + // Extended hook + } + } + + it('should inherit field metadata from parent class', () => { + const baseFieldConfig = getFieldConfig(ExtendedTestModel, 'baseField'); + expect(baseFieldConfig).toBeDefined(); + expect(baseFieldConfig.type).toBe('string'); + expect(baseFieldConfig.required).toBe(true); + + const extendedFieldConfig = getFieldConfig(ExtendedTestModel, 'extendedField'); + expect(extendedFieldConfig).toBeDefined(); + expect(extendedFieldConfig.type).toBe('number'); + }); + + it('should override model configuration in child class', () => { + expect(ExtendedTestModel.scope).toBe('user'); + expect(ExtendedTestModel.storeType).toBe('eventlog'); + }); + + it('should inherit and extend hooks', () => { + const hooks = getHooks(ExtendedTestModel); + expect(hooks.beforeCreate).toContain('baseHook'); + expect(hooks.beforeCreate).toContain('extendedHook'); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/migrations/MigrationManager.test.ts b/tests/unit/migrations/MigrationManager.test.ts new file mode 100644 index 0000000..d7fe415 --- /dev/null +++ b/tests/unit/migrations/MigrationManager.test.ts @@ -0,0 +1,680 @@ +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { + MigrationManager, + Migration, + MigrationOperation, + MigrationResult, + MigrationValidator, + MigrationLogger +} from '../../../src/framework/migrations/MigrationManager'; +import { FieldConfig } from '../../../src/framework/types/models'; +import { createMockServices } from '../../mocks/services'; + +describe('MigrationManager', () => { + let migrationManager: MigrationManager; + let mockDatabaseManager: any; + let mockShardManager: any; + let mockLogger: MigrationLogger; + + const createTestMigration = (overrides: Partial = {}): Migration => ({ + id: 'test-migration-1', + version: '1.0.0', + name: 'Test Migration', + description: 'A test migration for unit testing', + targetModels: ['TestModel'], + up: [ + { + type: 'add_field', + modelName: 'TestModel', + fieldName: 'newField', + fieldConfig: { + type: 'string', + required: false, + default: 'default-value' + } as FieldConfig + } + ], + down: [ + { + type: 'remove_field', + modelName: 'TestModel', + fieldName: 'newField' + } + ], + createdAt: Date.now(), + ...overrides + }); + + beforeEach(() => { + const mockServices = createMockServices(); + + mockDatabaseManager = { + getAllDocuments: jest.fn().mockResolvedValue([]), + addDocument: jest.fn().mockResolvedValue('mock-id'), + updateDocument: jest.fn().mockResolvedValue(undefined), + deleteDocument: jest.fn().mockResolvedValue(undefined), + }; + + mockShardManager = { + getAllShards: jest.fn().mockReturnValue([]), + getShardForKey: jest.fn().mockReturnValue({ name: 'shard-0', database: {} }), + }; + + mockLogger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn() + }; + + migrationManager = new MigrationManager(mockDatabaseManager, mockShardManager, mockLogger); + + jest.clearAllMocks(); + }); + + describe('Migration Registration', () => { + it('should register a valid migration', () => { + const migration = createTestMigration(); + + migrationManager.registerMigration(migration); + + const registered = migrationManager.getMigration(migration.id); + expect(registered).toEqual(migration); + expect(mockLogger.info).toHaveBeenCalledWith( + `Registered migration: ${migration.name} (${migration.version})`, + expect.objectContaining({ + migrationId: migration.id, + targetModels: migration.targetModels + }) + ); + }); + + it('should throw error for invalid migration structure', () => { + const invalidMigration = createTestMigration({ + id: '', // Invalid - empty ID + }); + + expect(() => migrationManager.registerMigration(invalidMigration)).toThrow( + 'Migration must have id, version, and name' + ); + }); + + it('should throw error for migration without target models', () => { + const invalidMigration = createTestMigration({ + targetModels: [] // Invalid - empty target models + }); + + expect(() => migrationManager.registerMigration(invalidMigration)).toThrow( + 'Migration must specify target models' + ); + }); + + it('should throw error for migration without up operations', () => { + const invalidMigration = createTestMigration({ + up: [] // Invalid - no up operations + }); + + expect(() => migrationManager.registerMigration(invalidMigration)).toThrow( + 'Migration must have at least one up operation' + ); + }); + + it('should throw error for duplicate version with different ID', () => { + const migration1 = createTestMigration({ id: 'migration-1', version: '1.0.0' }); + const migration2 = createTestMigration({ id: 'migration-2', version: '1.0.0' }); + + migrationManager.registerMigration(migration1); + + expect(() => migrationManager.registerMigration(migration2)).toThrow( + 'Migration version 1.0.0 already exists with different ID' + ); + }); + + it('should allow registering same migration with same ID', () => { + const migration = createTestMigration(); + + migrationManager.registerMigration(migration); + migrationManager.registerMigration(migration); // Should not throw + + expect(migrationManager.getMigrations()).toHaveLength(1); + }); + }); + + describe('Migration Retrieval', () => { + beforeEach(() => { + const migration1 = createTestMigration({ id: 'migration-1', version: '1.0.0' }); + const migration2 = createTestMigration({ id: 'migration-2', version: '2.0.0' }); + const migration3 = createTestMigration({ id: 'migration-3', version: '1.5.0' }); + + migrationManager.registerMigration(migration1); + migrationManager.registerMigration(migration2); + migrationManager.registerMigration(migration3); + }); + + it('should get all migrations sorted by version', () => { + const migrations = migrationManager.getMigrations(); + + expect(migrations).toHaveLength(3); + expect(migrations[0].version).toBe('1.0.0'); + expect(migrations[1].version).toBe('1.5.0'); + expect(migrations[2].version).toBe('2.0.0'); + }); + + it('should get migration by ID', () => { + const migration = migrationManager.getMigration('migration-2'); + + expect(migration).toBeDefined(); + expect(migration?.version).toBe('2.0.0'); + }); + + it('should return null for non-existent migration', () => { + const migration = migrationManager.getMigration('non-existent'); + + expect(migration).toBeNull(); + }); + + it('should get pending migrations', () => { + // Mock applied migrations (empty for this test) + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + + const pending = migrationManager.getPendingMigrations(); + + expect(pending).toHaveLength(3); + }); + + it('should filter pending migrations by model', () => { + const migration4 = createTestMigration({ + id: 'migration-4', + version: '3.0.0', + targetModels: ['OtherModel'] + }); + migrationManager.registerMigration(migration4); + + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + + const pending = migrationManager.getPendingMigrations('TestModel'); + + expect(pending).toHaveLength(3); // Should exclude migration-4 + expect(pending.every(m => m.targetModels.includes('TestModel'))).toBe(true); + }); + }); + + describe('Migration Operations', () => { + it('should validate add_field operation', () => { + const operation: MigrationOperation = { + type: 'add_field', + modelName: 'TestModel', + fieldName: 'newField', + fieldConfig: { type: 'string', required: false } + }; + + expect(() => (migrationManager as any).validateOperation(operation)).not.toThrow(); + }); + + it('should validate remove_field operation', () => { + const operation: MigrationOperation = { + type: 'remove_field', + modelName: 'TestModel', + fieldName: 'oldField' + }; + + expect(() => (migrationManager as any).validateOperation(operation)).not.toThrow(); + }); + + it('should validate rename_field operation', () => { + const operation: MigrationOperation = { + type: 'rename_field', + modelName: 'TestModel', + fieldName: 'oldField', + newFieldName: 'newField' + }; + + expect(() => (migrationManager as any).validateOperation(operation)).not.toThrow(); + }); + + it('should validate transform_data operation', () => { + const operation: MigrationOperation = { + type: 'transform_data', + modelName: 'TestModel', + transformer: (data: any) => data + }; + + expect(() => (migrationManager as any).validateOperation(operation)).not.toThrow(); + }); + + it('should reject invalid operation type', () => { + const operation: MigrationOperation = { + type: 'invalid_type' as any, + modelName: 'TestModel' + }; + + expect(() => (migrationManager as any).validateOperation(operation)).toThrow( + 'Invalid operation type: invalid_type' + ); + }); + + it('should reject operation without model name', () => { + const operation: MigrationOperation = { + type: 'add_field', + modelName: '' + }; + + expect(() => (migrationManager as any).validateOperation(operation)).toThrow( + 'Operation must specify modelName' + ); + }); + }); + + describe('Migration Execution', () => { + let migration: Migration; + + beforeEach(() => { + migration = createTestMigration(); + migrationManager.registerMigration(migration); + + // Mock helper methods + jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([ + { id: 'record-1', name: 'Test 1' }, + { id: 'record-2', name: 'Test 2' } + ]); + jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined); + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined); + }); + + it('should run migration successfully', async () => { + const result = await migrationManager.runMigration(migration.id); + + expect(result.success).toBe(true); + expect(result.migrationId).toBe(migration.id); + expect(result.recordsProcessed).toBe(2); + expect(result.rollbackAvailable).toBe(true); + expect(mockLogger.info).toHaveBeenCalledWith( + `Migration completed: ${migration.name}`, + expect.objectContaining({ + migrationId: migration.id, + recordsProcessed: 2 + }) + ); + }); + + it('should perform dry run without modifying data', async () => { + jest.spyOn(migrationManager as any, 'countRecordsForModel').mockResolvedValue(2); + + const result = await migrationManager.runMigration(migration.id, { dryRun: true }); + + expect(result.success).toBe(true); + expect(result.warnings).toContain('This was a dry run - no data was actually modified'); + expect(mockLogger.info).toHaveBeenCalledWith( + `Performing dry run for migration: ${migration.name}` + ); + }); + + it('should throw error for non-existent migration', async () => { + await expect(migrationManager.runMigration('non-existent')).rejects.toThrow( + 'Migration non-existent not found' + ); + }); + + it('should throw error for already running migration', async () => { + // Mock a longer running migration by delaying the getAllRecordsForModel call + jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockImplementation(() => { + return new Promise(resolve => setTimeout(() => resolve([]), 100)); + }); + + // Start first migration (don't await) + const promise1 = migrationManager.runMigration(migration.id); + + // Wait a small amount to ensure the first migration has started + await new Promise(resolve => setTimeout(resolve, 10)); + + // Try to start same migration again + await expect(migrationManager.runMigration(migration.id)).rejects.toThrow( + `Migration ${migration.id} is already running` + ); + + // Clean up first migration + await promise1; + }); + + it('should handle migration with dependencies', async () => { + const dependentMigration = createTestMigration({ + id: 'dependent-migration', + version: '2.0.0', + dependencies: ['test-migration-1'] + }); + + migrationManager.registerMigration(dependentMigration); + + // Mock that dependency is not applied + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + + await expect(migrationManager.runMigration(dependentMigration.id)).rejects.toThrow( + 'Migration dependency not satisfied: test-migration-1' + ); + }); + }); + + describe('Migration Rollback', () => { + let migration: Migration; + + beforeEach(() => { + migration = createTestMigration(); + migrationManager.registerMigration(migration); + + jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([ + { id: 'record-1', name: 'Test 1', newField: 'default-value' }, + { id: 'record-2', name: 'Test 2', newField: 'default-value' } + ]); + jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined); + jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined); + }); + + it('should rollback applied migration', async () => { + // Mock that migration was applied + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([ + { migrationId: migration.id, success: true } + ]); + + const result = await migrationManager.rollbackMigration(migration.id); + + expect(result.success).toBe(true); + expect(result.migrationId).toBe(migration.id); + expect(result.rollbackAvailable).toBe(false); + expect(mockLogger.info).toHaveBeenCalledWith( + `Rollback completed: ${migration.name}`, + expect.objectContaining({ migrationId: migration.id }) + ); + }); + + it('should throw error for non-existent migration rollback', async () => { + await expect(migrationManager.rollbackMigration('non-existent')).rejects.toThrow( + 'Migration non-existent not found' + ); + }); + + it('should throw error for unapplied migration rollback', async () => { + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + + await expect(migrationManager.rollbackMigration(migration.id)).rejects.toThrow( + `Migration ${migration.id} has not been applied` + ); + }); + + it('should handle migration without rollback operations', async () => { + const migrationWithoutRollback = createTestMigration({ + id: 'no-rollback', + version: '2.0.0', + down: [] + }); + migrationManager.registerMigration(migrationWithoutRollback); + + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([ + { migrationId: 'no-rollback', success: true } + ]); + + await expect(migrationManager.rollbackMigration('no-rollback')).rejects.toThrow( + 'Migration has no rollback operations defined' + ); + }); + }); + + describe('Batch Migration Operations', () => { + beforeEach(() => { + const migration1 = createTestMigration({ id: 'migration-1', version: '1.0.0' }); + const migration2 = createTestMigration({ id: 'migration-2', version: '2.0.0' }); + const migration3 = createTestMigration({ id: 'migration-3', version: '3.0.0' }); + + migrationManager.registerMigration(migration1); + migrationManager.registerMigration(migration2); + migrationManager.registerMigration(migration3); + + jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([]); + jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined); + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined); + }); + + it('should run all pending migrations', async () => { + const results = await migrationManager.runPendingMigrations(); + + expect(results).toHaveLength(3); + expect(results.every(r => r.success)).toBe(true); + expect(mockLogger.info).toHaveBeenCalledWith( + 'Running 3 pending migrations', + expect.objectContaining({ modelName: undefined, dryRun: undefined }) + ); + }); + + it('should run pending migrations for specific model', async () => { + const migration4 = createTestMigration({ + id: 'migration-4', + version: '4.0.0', + targetModels: ['OtherModel'] + }); + migrationManager.registerMigration(migration4); + + const results = await migrationManager.runPendingMigrations({ modelName: 'TestModel' }); + + expect(results).toHaveLength(3); // Should exclude migration-4 + }); + + it('should stop on error when specified', async () => { + // Make second migration fail + jest.spyOn(migrationManager, 'runMigration') + .mockResolvedValueOnce({ success: true } as MigrationResult) + .mockRejectedValueOnce(new Error('Migration failed')); + + await expect( + migrationManager.runPendingMigrations({ stopOnError: true }) + ).rejects.toThrow('Migration failed'); + }); + + it('should continue on error when not specified', async () => { + // Make second migration fail + jest.spyOn(migrationManager, 'runMigration') + .mockResolvedValueOnce({ success: true } as MigrationResult) + .mockRejectedValueOnce(new Error('Migration failed')) + .mockResolvedValueOnce({ success: true } as MigrationResult); + + const results = await migrationManager.runPendingMigrations({ stopOnError: false }); + + expect(results).toHaveLength(2); // Only successful migrations + expect(mockLogger.error).toHaveBeenCalledWith( + 'Skipping failed migration: migration-2', + expect.objectContaining({ error: expect.any(Error) }) + ); + }); + }); + + describe('Migration Validation', () => { + it('should run pre-migration validators', async () => { + const validator: MigrationValidator = { + name: 'Test Validator', + description: 'Tests migration validity', + validate: jest.fn().mockResolvedValue({ + valid: true, + errors: [], + warnings: ['Test warning'] + }) + }; + + const migration = createTestMigration({ + validators: [validator] + }); + + migrationManager.registerMigration(migration); + jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([]); + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined); + + await migrationManager.runMigration(migration.id); + + expect(validator.validate).toHaveBeenCalled(); + expect(mockLogger.debug).toHaveBeenCalledWith( + `Running pre-migration validator: ${validator.name}` + ); + }); + + it('should fail migration on validation error', async () => { + const validator: MigrationValidator = { + name: 'Failing Validator', + description: 'Always fails', + validate: jest.fn().mockResolvedValue({ + valid: false, + errors: ['Validation failed'], + warnings: [] + }) + }; + + const migration = createTestMigration({ + validators: [validator] + }); + + migrationManager.registerMigration(migration); + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + + await expect(migrationManager.runMigration(migration.id)).rejects.toThrow( + 'Pre-migration validation failed: Validation failed' + ); + }); + }); + + describe('Migration Progress and Monitoring', () => { + it('should track migration progress', async () => { + const migration = createTestMigration(); + migrationManager.registerMigration(migration); + + jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([ + { id: 'record-1' } + ]); + jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined); + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined); + + // Mock a longer running migration by adding a delay + jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockImplementation(() => { + return new Promise(resolve => setTimeout(() => resolve([{ id: 'record-1' }]), 50)); + }); + + const migrationPromise = migrationManager.runMigration(migration.id); + + // Wait a bit to ensure migration has started + await new Promise(resolve => setTimeout(resolve, 10)); + + // Check progress while migration is running + const progress = migrationManager.getMigrationProgress(migration.id); + expect(progress).toBeDefined(); + expect(progress?.status).toBe('running'); + + await migrationPromise; + + // Progress should be cleared after completion + const finalProgress = migrationManager.getMigrationProgress(migration.id); + expect(finalProgress).toBeNull(); + }); + + it('should get active migrations', async () => { + const migration1 = createTestMigration({ id: 'migration-1', version: '1.0.0' }); + const migration2 = createTestMigration({ id: 'migration-2', version: '2.0.0' }); + + migrationManager.registerMigration(migration1); + migrationManager.registerMigration(migration2); + + jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]); + jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined); + + // Mock longer running migrations + jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockImplementation(() => { + return new Promise(resolve => setTimeout(() => resolve([]), 100)); + }); + jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined); + + // Start migrations but don't await + const promise1 = migrationManager.runMigration(migration1.id); + + // Wait a bit for the first migration to start + await new Promise(resolve => setTimeout(resolve, 10)); + + const promise2 = migrationManager.runMigration(migration2.id).catch(() => {}); + + // Wait a bit for the second migration to start (or fail) + await new Promise(resolve => setTimeout(resolve, 10)); + + const activeMigrations = migrationManager.getActiveMigrations(); + expect(activeMigrations.length).toBeGreaterThanOrEqual(1); + expect(activeMigrations.some(p => p.status === 'running')).toBe(true); + + await Promise.allSettled([promise1, promise2]); + }); + + it('should get migration history', () => { + // Manually add some history + const result1: MigrationResult = { + migrationId: 'migration-1', + success: true, + duration: 1000, + recordsProcessed: 10, + recordsModified: 5, + warnings: [], + errors: [], + rollbackAvailable: true + }; + + const result2: MigrationResult = { + migrationId: 'migration-2', + success: false, + duration: 500, + recordsProcessed: 5, + recordsModified: 0, + warnings: [], + errors: ['Test error'], + rollbackAvailable: false + }; + + (migrationManager as any).migrationHistory.set('migration-1', [result1]); + (migrationManager as any).migrationHistory.set('migration-2', [result2]); + + const allHistory = migrationManager.getMigrationHistory(); + expect(allHistory).toHaveLength(2); + + const specificHistory = migrationManager.getMigrationHistory('migration-1'); + expect(specificHistory).toEqual([result1]); + }); + }); + + describe('Version Comparison', () => { + it('should compare versions correctly', () => { + const compareVersions = (migrationManager as any).compareVersions.bind(migrationManager); + + expect(compareVersions('1.0.0', '2.0.0')).toBe(-1); + expect(compareVersions('2.0.0', '1.0.0')).toBe(1); + expect(compareVersions('1.0.0', '1.0.0')).toBe(0); + expect(compareVersions('1.2.0', '1.1.0')).toBe(1); + expect(compareVersions('1.0.1', '1.0.0')).toBe(1); + expect(compareVersions('1.0', '1.0.0')).toBe(0); + }); + }); + + describe('Field Value Conversion', () => { + it('should convert field values correctly', () => { + const convertFieldValue = (migrationManager as any).convertFieldValue.bind(migrationManager); + + expect(convertFieldValue('123', { type: 'number' })).toBe(123); + expect(convertFieldValue(123, { type: 'string' })).toBe('123'); + expect(convertFieldValue('true', { type: 'boolean' })).toBe(true); + expect(convertFieldValue('test', { type: 'array' })).toEqual(['test']); + expect(convertFieldValue(['test'], { type: 'array' })).toEqual(['test']); + expect(convertFieldValue(null, { type: 'string' })).toBeNull(); + }); + }); + + describe('Cleanup', () => { + it('should cleanup resources', async () => { + await migrationManager.cleanup(); + + expect(migrationManager.getActiveMigrations()).toHaveLength(0); + expect(mockLogger.info).toHaveBeenCalledWith('Cleaning up migration manager'); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/models/BaseModel.test.ts b/tests/unit/models/BaseModel.test.ts new file mode 100644 index 0000000..07e9b27 --- /dev/null +++ b/tests/unit/models/BaseModel.test.ts @@ -0,0 +1,478 @@ +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { BaseModel } from '../../../src/framework/models/BaseModel'; +import { Model, Field, BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate } from '../../../src/framework/models/decorators'; +import { createMockServices } from '../../mocks/services'; + +// Test model for testing BaseModel functionality +@Model({ + scope: 'global', + type: 'docstore' +}) +class TestUser extends BaseModel { + @Field({ type: 'string', required: true, unique: true }) + declare username: string; + + @Field({ + type: 'string', + required: true, + unique: true, + validate: (value: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(value); + } + }) + declare email: string; + + @Field({ type: 'number', required: false, default: 0 }) + declare score: number; + + @Field({ type: 'boolean', required: false, default: true }) + declare isActive: boolean; + + @Field({ type: 'array', required: false, default: [] }) + declare tags: string[]; + + @Field({ type: 'number', required: false }) + declare createdAt: number; + + @Field({ type: 'number', required: false }) + declare updatedAt: number; + + // Hook counters for testing + static beforeCreateCount = 0; + static afterCreateCount = 0; + static beforeUpdateCount = 0; + static afterUpdateCount = 0; + + @BeforeCreate() + beforeCreateHook() { + this.createdAt = Date.now(); + this.updatedAt = Date.now(); + TestUser.beforeCreateCount++; + } + + @AfterCreate() + afterCreateHook() { + TestUser.afterCreateCount++; + } + + @BeforeUpdate() + beforeUpdateHook() { + this.updatedAt = Date.now(); + TestUser.beforeUpdateCount++; + } + + @AfterUpdate() + afterUpdateHook() { + TestUser.afterUpdateCount++; + } + + // Custom validation method + validateEmail(): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(this.email); + } +} + +// Test model with validation +@Model({ + scope: 'user', + type: 'docstore' +}) +class TestPost extends BaseModel { + @Field({ + type: 'string', + required: true, + validate: (value: string) => { + if (value.length < 3) { + throw new Error('Title must be at least 3 characters'); + } + return true; + } + }) + declare title: string; + + @Field({ + type: 'string', + required: true, + validate: (value: string) => value.length <= 1000 + }) + declare content: string; + + @Field({ type: 'string', required: true }) + declare userId: string; + + @Field({ + type: 'array', + required: false, + default: [], + transform: (tags: string[]) => tags.map(tag => tag.toLowerCase()) + }) + declare tags: string[]; +} + +describe('BaseModel', () => { + let mockServices: any; + + beforeEach(() => { + mockServices = createMockServices(); + + // Clear the shared mock database to prevent test isolation issues + if ((globalThis as any).__mockDatabase) { + (globalThis as any).__mockDatabase.clear(); + } + + // Reset hook counters + TestUser.beforeCreateCount = 0; + TestUser.afterCreateCount = 0; + TestUser.beforeUpdateCount = 0; + TestUser.afterUpdateCount = 0; + + // Mock the framework initialization + jest.clearAllMocks(); + }); + + describe('Model Creation', () => { + it('should create a new model instance with required fields', () => { + const user = new TestUser(); + user.username = 'testuser'; + user.email = 'test@example.com'; + + expect(user.username).toBe('testuser'); + expect(user.email).toBe('test@example.com'); + expect(user.score).toBe(0); // Default value + expect(user.isActive).toBe(true); // Default value + expect(user.tags).toEqual([]); // Default value + }); + + it('should generate a unique ID for new instances', () => { + const user1 = new TestUser(); + const user2 = new TestUser(); + + expect(user1.id).toBeDefined(); + expect(user2.id).toBeDefined(); + expect(user1.id).not.toBe(user2.id); + }); + + it('should create instance using static create method', async () => { + const userData = { + username: 'alice', + email: 'alice@example.com', + score: 100 + }; + + const user = await TestUser.create(userData); + + expect(user).toBeInstanceOf(TestUser); + expect(user.username).toBe('alice'); + expect(user.email).toBe('alice@example.com'); + expect(user.score).toBe(100); + expect(user.isActive).toBe(true); // Default value + }); + }); + + describe('Validation', () => { + it('should validate required fields on create', async () => { + await expect(async () => { + await TestUser.create({ + // Missing required username and email + score: 50 + }); + }).rejects.toThrow(); + }); + + it('should validate field constraints', async () => { + await expect(async () => { + await TestPost.create({ + title: 'Hi', // Too short (< 3 characters) + content: 'Test content', + userId: 'user123' + }); + }).rejects.toThrow('Title must be at least 3 characters'); + }); + + it('should apply field transformations', async () => { + const post = await TestPost.create({ + title: 'Test Post', + content: 'Test content', + userId: 'user123', + tags: ['JavaScript', 'TypeScript', 'REACT'] + }); + + // Tags should be transformed to lowercase + expect(post.tags).toEqual(['javascript', 'typescript', 'react']); + }); + + it('should validate field types', async () => { + await expect(async () => { + await TestUser.create({ + username: 'testuser', + email: 'test@example.com', + score: 'invalid-number' as any // Wrong type + }); + }).rejects.toThrow(); + }); + }); + + describe('CRUD Operations', () => { + let user: TestUser; + + beforeEach(async () => { + user = await TestUser.create({ + username: 'testuser', + email: 'test@example.com', + score: 50 + }); + }); + + it('should save a model instance', async () => { + user.score = 100; + await user.save(); + + expect(user.score).toBe(100); + expect(TestUser.beforeUpdateCount).toBe(1); + expect(TestUser.afterUpdateCount).toBe(1); + }); + + it('should find a model by ID', async () => { + const foundUser = await TestUser.findById(user.id); + + expect(foundUser).toBeInstanceOf(TestUser); + expect(foundUser?.id).toBe(user.id); + expect(foundUser?.username).toBe(user.username); + }); + + it('should return null when model not found', async () => { + const foundUser = await TestUser.findById('non-existent-id'); + expect(foundUser).toBeNull(); + }); + + it('should find model by criteria', async () => { + const foundUser = await TestUser.findOne({ username: 'testuser' }); + + expect(foundUser).toBeInstanceOf(TestUser); + expect(foundUser?.username).toBe('testuser'); + }); + + it('should delete a model instance', async () => { + const userId = user.id; + await user.delete(); + + const foundUser = await TestUser.findById(userId); + expect(foundUser).toBeNull(); + }); + + it('should find all models', async () => { + // Create another user with unique username and email + await TestUser.create({ + username: 'testuser2', + email: 'testuser2@example.com' + }); + + const allUsers = await TestUser.findAll(); + expect(allUsers.length).toBeGreaterThanOrEqual(2); + expect(allUsers.every(u => u instanceof TestUser)).toBe(true); + }); + }); + + describe('Model Hooks', () => { + it('should execute beforeCreate and afterCreate hooks', async () => { + const initialBeforeCount = TestUser.beforeCreateCount; + const initialAfterCount = TestUser.afterCreateCount; + + const user = await TestUser.create({ + username: 'hooktest', + email: 'hook@example.com' + }); + + expect(TestUser.beforeCreateCount).toBe(initialBeforeCount + 1); + expect(TestUser.afterCreateCount).toBe(initialAfterCount + 1); + expect(user.createdAt).toBeDefined(); + expect(user.updatedAt).toBeDefined(); + }); + + it('should execute beforeUpdate and afterUpdate hooks', async () => { + const user = await TestUser.create({ + username: 'updatetest', + email: 'update@example.com' + }); + + const initialBeforeCount = TestUser.beforeUpdateCount; + const initialAfterCount = TestUser.afterUpdateCount; + const initialUpdatedAt = user.updatedAt; + + // Wait a bit to ensure different timestamp + await new Promise(resolve => setTimeout(resolve, 10)); + + user.score = 100; + await user.save(); + + expect(TestUser.beforeUpdateCount).toBe(initialBeforeCount + 1); + expect(TestUser.afterUpdateCount).toBe(initialAfterCount + 1); + expect(user.updatedAt).toBeGreaterThan(initialUpdatedAt!); + }); + }); + + describe('Serialization', () => { + it('should serialize to JSON correctly', async () => { + const user = await TestUser.create({ + username: 'serialtest', + email: 'serial@example.com', + score: 75, + tags: ['test', 'user'] + }); + + const json = user.toJSON(); + + expect(json).toMatchObject({ + id: user.id, + username: 'serialtest', + email: 'serial@example.com', + score: 75, + isActive: true, + tags: ['test', 'user'], + createdAt: expect.any(Number), + updatedAt: expect.any(Number) + }); + }); + + it('should create instance from JSON', () => { + const data = { + id: 'test-id', + username: 'fromjson', + email: 'json@example.com', + score: 80, + isActive: false, + tags: ['json'], + createdAt: Date.now(), + updatedAt: Date.now() + }; + + const user = TestUser.fromJSON(data); + + expect(user).toBeInstanceOf(TestUser); + expect(user.id).toBe('test-id'); + expect(user.username).toBe('fromjson'); + expect(user.email).toBe('json@example.com'); + expect(user.score).toBe(80); + expect(user.isActive).toBe(false); + expect(user.tags).toEqual(['json']); + }); + }); + + describe('Query Interface', () => { + it('should provide query interface', () => { + const queryBuilder = TestUser.query(); + + expect(queryBuilder).toBeDefined(); + expect(typeof queryBuilder.where).toBe('function'); + expect(typeof queryBuilder.find).toBe('function'); + expect(typeof queryBuilder.findOne).toBe('function'); + expect(typeof queryBuilder.count).toBe('function'); + }); + + it('should support method chaining in queries', () => { + const queryBuilder = TestUser.query() + .where('isActive', true) + .where('score', '>', 50) + .orderBy('username') + .limit(10); + + expect(queryBuilder).toBeDefined(); + // The query builder should return itself for chaining + expect(typeof queryBuilder.find).toBe('function'); + }); + }); + + describe('Field Modification Tracking', () => { + it('should track field modifications', async () => { + const user = await TestUser.create({ + username: 'tracktest', + email: 'track@example.com' + }); + + expect(user.isFieldModified('username')).toBe(false); + + user.username = 'newusername'; + expect(user.isFieldModified('username')).toBe(true); + + user.score = 100; + expect(user.isFieldModified('score')).toBe(true); + expect(user.isFieldModified('email')).toBe(false); + }); + + it('should get modified fields', async () => { + const user = await TestUser.create({ + username: 'modifytest', + email: 'modify@example.com' + }); + + user.username = 'newusername'; + user.score = 200; + + const modifiedFields = user.getModifiedFields(); + expect(modifiedFields).toContain('username'); + expect(modifiedFields).toContain('score'); + expect(modifiedFields).not.toContain('email'); + }); + + it('should clear modifications after save', async () => { + const user = await TestUser.create({ + username: 'cleartest', + email: 'clear@example.com' + }); + + user.username = 'newusername'; + expect(user.isFieldModified('username')).toBe(true); + + await user.save(); + expect(user.isFieldModified('username')).toBe(false); + }); + }); + + describe('Error Handling', () => { + it('should handle validation errors gracefully', async () => { + try { + await TestPost.create({ + // Missing required title + content: 'Test content', + userId: 'user123' + }); + fail('Should have thrown validation error'); + } catch (error: any) { + expect(error.message).toContain('required'); + } + }); + + it('should handle database errors gracefully', async () => { + // This would test database connection errors, timeouts, etc. + // For now, we'll test with a simple validation error + const user = new TestUser(); + user.username = 'test'; + + expect(() => { + user.email = 'invalid-email'; // Invalid email format + }).toThrow(); + }); + }); + + describe('Custom Methods', () => { + it('should support custom validation methods', async () => { + const user = await TestUser.create({ + username: 'emailtest', + email: 'valid@example.com' + }); + + expect(user.validateEmail()).toBe(true); + + // Test that setting an invalid email throws validation error + expect(() => { + user.email = 'invalid-email'; + }).toThrow('email failed custom validation'); + + // Email should still be the original valid value + expect(user.email).toBe('valid@example.com'); + expect(user.validateEmail()).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/relationships/RelationshipManager.test.ts b/tests/unit/relationships/RelationshipManager.test.ts new file mode 100644 index 0000000..5b0e56b --- /dev/null +++ b/tests/unit/relationships/RelationshipManager.test.ts @@ -0,0 +1,577 @@ +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { RelationshipManager, RelationshipLoadOptions } from '../../../src/framework/relationships/RelationshipManager'; +import { BaseModel } from '../../../src/framework/models/BaseModel'; +import { Model, Field, BelongsTo, HasMany, HasOne, ManyToMany } from '../../../src/framework/models/decorators'; +import { QueryBuilder } from '../../../src/framework/query/QueryBuilder'; +import { createMockServices } from '../../mocks/services'; + +// Test models for relationship testing +@Model({ + scope: 'user', + type: 'docstore' +}) +class Post extends BaseModel { + @Field({ type: 'string', required: true }) + title: string; + + @Field({ type: 'string', required: true }) + content: string; + + @Field({ type: 'string', required: true }) + userId: string; + + @BelongsTo(() => User, 'userId') + user: any; + + // Mock query methods + static where = jest.fn().mockReturnThis(); + static whereIn = jest.fn().mockReturnThis(); + static first = jest.fn(); + static exec = jest.fn(); +} + +@Model({ + scope: 'global', + type: 'docstore' +}) +class Profile extends BaseModel { + @Field({ type: 'string', required: true }) + bio: string; + + @Field({ type: 'string', required: true }) + userId: string; + + @BelongsTo(() => User, 'userId') + user: any; + + // Mock query methods + static where = jest.fn().mockReturnThis(); + static whereIn = jest.fn().mockReturnThis(); + static first = jest.fn(); + static exec = jest.fn(); +} + +@Model({ + scope: 'global', + type: 'docstore' +}) +class Role extends BaseModel { + @Field({ type: 'string', required: true }) + name: string; + + @ManyToMany(() => User, 'user_roles', 'roleId', 'userId') + users: any[]; + + // Mock query methods + static where = jest.fn().mockReturnThis(); + static whereIn = jest.fn().mockReturnThis(); + static first = jest.fn(); + static exec = jest.fn(); +} + +@Model({ + scope: 'global', + type: 'docstore' +}) +class User extends BaseModel { + @Field({ type: 'string', required: true }) + username: string; + + @Field({ type: 'string', required: true }) + email: string; + + @HasMany(() => Post, 'userId') + posts: any[]; + + @HasOne(() => Profile, 'userId') + profile: any; + + @ManyToMany(() => Role, 'user_roles', 'userId', 'roleId') + roles: any[]; + + // Mock query methods + static where = jest.fn().mockReturnThis(); + static whereIn = jest.fn().mockReturnThis(); + static first = jest.fn(); + static exec = jest.fn(); +} + +@Model({ + scope: 'global', + type: 'docstore' +}) +class UserRole extends BaseModel { + @Field({ type: 'string', required: true }) + userId: string; + + @Field({ type: 'string', required: true }) + roleId: string; + + // Mock query methods + static where = jest.fn().mockReturnThis(); + static whereIn = jest.fn().mockReturnThis(); + static first = jest.fn(); + static exec = jest.fn(); +} + +describe('RelationshipManager', () => { + let relationshipManager: RelationshipManager; + let mockFramework: any; + let user: User; + let post: Post; + let profile: Profile; + let role: Role; + + beforeEach(() => { + const mockServices = createMockServices(); + mockFramework = { + services: mockServices + }; + + relationshipManager = new RelationshipManager(mockFramework); + + // Create test instances + user = new User(); + user.id = 'user-123'; + user.username = 'testuser'; + user.email = 'test@example.com'; + + post = new Post(); + post.id = 'post-123'; + post.title = 'Test Post'; + post.content = 'Test content'; + post.userId = 'user-123'; + + profile = new Profile(); + profile.id = 'profile-123'; + profile.bio = 'Test bio'; + profile.userId = 'user-123'; + + role = new Role(); + role.id = 'role-123'; + role.name = 'admin'; + + // Clear all mocks + jest.clearAllMocks(); + }); + + describe('BelongsTo Relationships', () => { + it('should load belongsTo relationship correctly', async () => { + const mockUser = new User(); + mockUser.id = 'user-123'; + + User.first.mockResolvedValue(mockUser); + + const result = await relationshipManager.loadRelationship(post, 'user'); + + expect(User.where).toHaveBeenCalledWith('id', '=', 'user-123'); + expect(User.first).toHaveBeenCalled(); + expect(result).toBe(mockUser); + expect(post._loadedRelations.get('user')).toBe(mockUser); + }); + + it('should return null for belongsTo when foreign key is null', async () => { + post.userId = null as any; + + const result = await relationshipManager.loadRelationship(post, 'user'); + + expect(result).toBeNull(); + expect(User.where).not.toHaveBeenCalled(); + }); + + it('should apply constraints to belongsTo queries', async () => { + const mockUser = new User(); + User.first.mockResolvedValue(mockUser); + + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(mockUser) + }; + User.where.mockReturnValue(mockQueryBuilder); + + const options: RelationshipLoadOptions = { + constraints: (query) => query.where('isActive', true) + }; + + await relationshipManager.loadRelationship(post, 'user', options); + + expect(User.where).toHaveBeenCalledWith('id', '=', 'user-123'); + expect(options.constraints).toBeDefined(); + }); + }); + + describe('HasMany Relationships', () => { + it('should load hasMany relationship correctly', async () => { + const mockPosts = [ + { id: 'post-1', title: 'Post 1', userId: 'user-123' }, + { id: 'post-2', title: 'Post 2', userId: 'user-123' } + ]; + + Post.exec.mockResolvedValue(mockPosts); + + const result = await relationshipManager.loadRelationship(user, 'posts'); + + expect(Post.where).toHaveBeenCalledWith('userId', '=', 'user-123'); + expect(Post.exec).toHaveBeenCalled(); + expect(result).toEqual(mockPosts); + expect(user._loadedRelations.get('posts')).toEqual(mockPosts); + }); + + it('should return empty array for hasMany when local key is null', async () => { + user.id = null as any; + + const result = await relationshipManager.loadRelationship(user, 'posts'); + + expect(result).toEqual([]); + expect(Post.where).not.toHaveBeenCalled(); + }); + + it('should apply ordering and limits to hasMany queries', async () => { + const mockPosts = [{ id: 'post-1', title: 'Post 1' }]; + + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockPosts) + }; + Post.where.mockReturnValue(mockQueryBuilder); + + const options: RelationshipLoadOptions = { + orderBy: { field: 'createdAt', direction: 'desc' }, + limit: 5 + }; + + await relationshipManager.loadRelationship(user, 'posts', options); + + expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('createdAt', 'desc'); + expect(mockQueryBuilder.limit).toHaveBeenCalledWith(5); + }); + }); + + describe('HasOne Relationships', () => { + it('should load hasOne relationship correctly', async () => { + const mockProfile = { id: 'profile-1', bio: 'Test bio', userId: 'user-123' }; + + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([mockProfile]) + }; + Profile.where.mockReturnValue(mockQueryBuilder); + + const result = await relationshipManager.loadRelationship(user, 'profile'); + + expect(Profile.where).toHaveBeenCalledWith('userId', '=', 'user-123'); + expect(mockQueryBuilder.limit).toHaveBeenCalledWith(1); + expect(result).toBe(mockProfile); + }); + + it('should return null for hasOne when no results found', async () => { + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([]) + }; + Profile.where.mockReturnValue(mockQueryBuilder); + + const result = await relationshipManager.loadRelationship(user, 'profile'); + + expect(result).toBeNull(); + }); + }); + + describe('ManyToMany Relationships', () => { + it('should load manyToMany relationship correctly', async () => { + const mockJunctionRecords = [ + { userId: 'user-123', roleId: 'role-1' }, + { userId: 'user-123', roleId: 'role-2' } + ]; + const mockRoles = [ + { id: 'role-1', name: 'admin' }, + { id: 'role-2', name: 'editor' } + ]; + + // Mock UserRole (junction table) + const mockJunctionQuery = { + where: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockJunctionRecords) + }; + + // Mock Role query + const mockRoleQuery = { + whereIn: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockRoles) + }; + + UserRole.where.mockReturnValue(mockJunctionQuery); + Role.whereIn.mockReturnValue(mockRoleQuery); + + // Mock the relationship config to include the through model + const originalRelationships = User.relationships; + User.relationships = new Map(); + User.relationships.set('roles', { + type: 'manyToMany', + model: Role, + through: UserRole, + foreignKey: 'roleId', + otherKey: 'userId', + localKey: 'id', + propertyKey: 'roles' + }); + + const result = await relationshipManager.loadRelationship(user, 'roles'); + + expect(UserRole.where).toHaveBeenCalledWith('userId', '=', 'user-123'); + expect(Role.whereIn).toHaveBeenCalledWith('id', ['role-1', 'role-2']); + expect(result).toEqual(mockRoles); + + // Restore original relationships + User.relationships = originalRelationships; + }); + + it('should handle empty junction table for manyToMany', async () => { + const mockJunctionQuery = { + where: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([]) + }; + + UserRole.where.mockReturnValue(mockJunctionQuery); + + // Mock the relationship config + const originalRelationships = User.relationships; + User.relationships = new Map(); + User.relationships.set('roles', { + type: 'manyToMany', + model: Role, + through: UserRole, + foreignKey: 'roleId', + otherKey: 'userId', + localKey: 'id', + propertyKey: 'roles' + }); + + const result = await relationshipManager.loadRelationship(user, 'roles'); + + expect(result).toEqual([]); + + // Restore original relationships + User.relationships = originalRelationships; + }); + + it('should throw error for manyToMany without through model', async () => { + // Mock the relationship config without through model + const originalRelationships = User.relationships; + User.relationships = new Map(); + User.relationships.set('roles', { + type: 'manyToMany', + model: Role, + through: null as any, + foreignKey: 'roleId', + localKey: 'id', + propertyKey: 'roles' + }); + + await expect(relationshipManager.loadRelationship(user, 'roles')).rejects.toThrow( + 'Many-to-many relationships require a through model' + ); + + // Restore original relationships + User.relationships = originalRelationships; + }); + }); + + describe('Eager Loading', () => { + it('should eager load multiple relationships for multiple instances', async () => { + const users = [user, new User()]; + users[1].id = 'user-456'; + + const mockPosts = [ + { id: 'post-1', userId: 'user-123' }, + { id: 'post-2', userId: 'user-456' } + ]; + const mockProfiles = [ + { id: 'profile-1', userId: 'user-123' }, + { id: 'profile-2', userId: 'user-456' } + ]; + + // Mock hasMany query for posts + const mockPostQuery = { + whereIn: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockPosts) + }; + Post.whereIn.mockReturnValue(mockPostQuery); + + // Mock hasOne query for profiles + const mockProfileQuery = { + whereIn: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockProfiles) + }; + Profile.whereIn.mockReturnValue(mockProfileQuery); + + await relationshipManager.eagerLoadRelationships(users, ['posts', 'profile']); + + expect(Post.whereIn).toHaveBeenCalledWith('userId', ['user-123', 'user-456']); + expect(Profile.whereIn).toHaveBeenCalledWith('userId', ['user-123', 'user-456']); + + // Check that relationships were loaded on instances + expect(users[0]._loadedRelations.has('posts')).toBe(true); + expect(users[0]._loadedRelations.has('profile')).toBe(true); + expect(users[1]._loadedRelations.has('posts')).toBe(true); + expect(users[1]._loadedRelations.has('profile')).toBe(true); + }); + + it('should handle empty instances array', async () => { + await relationshipManager.eagerLoadRelationships([], ['posts']); + + expect(Post.whereIn).not.toHaveBeenCalled(); + }); + + it('should skip non-existent relationships during eager loading', async () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + await relationshipManager.eagerLoadRelationships([user], ['nonExistentRelation']); + + expect(consoleSpy).toHaveBeenCalledWith( + "Relationship 'nonExistentRelation' not found on User" + ); + + consoleSpy.mockRestore(); + }); + }); + + describe('Caching', () => { + it('should use cache when available', async () => { + const mockUser = new User(); + + // Mock cache hit + jest.spyOn(relationshipManager['cache'], 'get').mockReturnValue(mockUser); + jest.spyOn(relationshipManager['cache'], 'generateKey').mockReturnValue('cache-key'); + + const result = await relationshipManager.loadRelationship(post, 'user'); + + expect(result).toBe(mockUser); + expect(User.where).not.toHaveBeenCalled(); // Should not query database + }); + + it('should store in cache after loading', async () => { + const mockUser = new User({ id: 'test-user-id' }); + User.first.mockResolvedValue(mockUser); + + const setCacheSpy = jest.spyOn(relationshipManager['cache'], 'set'); + const generateKeySpy = jest.spyOn(relationshipManager['cache'], 'generateKey').mockReturnValue('cache-key'); + + await relationshipManager.loadRelationship(post, 'user'); + + expect(setCacheSpy).toHaveBeenCalledWith('cache-key', expect.any(User), 'User', 'belongsTo'); + expect(generateKeySpy).toHaveBeenCalled(); + }); + + it('should skip cache when useCache is false', async () => { + const mockUser = new User(); + User.first.mockResolvedValue(mockUser); + + const getCacheSpy = jest.spyOn(relationshipManager['cache'], 'get'); + const setCacheSpy = jest.spyOn(relationshipManager['cache'], 'set'); + + await relationshipManager.loadRelationship(post, 'user', { useCache: false }); + + expect(getCacheSpy).not.toHaveBeenCalled(); + expect(setCacheSpy).not.toHaveBeenCalled(); + }); + }); + + describe('Cache Management', () => { + it('should invalidate relationship cache for specific relationship', () => { + const invalidateSpy = jest.spyOn(relationshipManager['cache'], 'invalidate').mockReturnValue(true); + const generateKeySpy = jest.spyOn(relationshipManager['cache'], 'generateKey').mockReturnValue('cache-key'); + + const result = relationshipManager.invalidateRelationshipCache(user, 'posts'); + + expect(generateKeySpy).toHaveBeenCalledWith(user, 'posts'); + expect(invalidateSpy).toHaveBeenCalledWith('cache-key'); + expect(result).toBe(1); + }); + + it('should invalidate all cache for instance when no relationship specified', () => { + const invalidateByInstanceSpy = jest.spyOn(relationshipManager['cache'], 'invalidateByInstance').mockReturnValue(3); + + const result = relationshipManager.invalidateRelationshipCache(user); + + expect(invalidateByInstanceSpy).toHaveBeenCalledWith(user); + expect(result).toBe(3); + }); + + it('should invalidate cache by model name', () => { + const invalidateByModelSpy = jest.spyOn(relationshipManager['cache'], 'invalidateByModel').mockReturnValue(5); + + const result = relationshipManager.invalidateModelCache('User'); + + expect(invalidateByModelSpy).toHaveBeenCalledWith('User'); + expect(result).toBe(5); + }); + + it('should get cache statistics', () => { + const mockStats = { cache: { hitRate: 0.85 }, performance: { avgLoadTime: 50 } }; + jest.spyOn(relationshipManager['cache'], 'getStats').mockReturnValue(mockStats.cache); + jest.spyOn(relationshipManager['cache'], 'analyzePerformance').mockReturnValue(mockStats.performance); + + const result = relationshipManager.getRelationshipCacheStats(); + + expect(result).toEqual(mockStats); + }); + + it('should warmup cache', async () => { + const warmupSpy = jest.spyOn(relationshipManager['cache'], 'warmup').mockResolvedValue(); + + await relationshipManager.warmupRelationshipCache([user], ['posts']); + + expect(warmupSpy).toHaveBeenCalledWith([user], ['posts'], expect.any(Function)); + }); + + it('should cleanup expired cache', () => { + const cleanupSpy = jest.spyOn(relationshipManager['cache'], 'cleanup').mockReturnValue(10); + + const result = relationshipManager.cleanupExpiredCache(); + + expect(cleanupSpy).toHaveBeenCalled(); + expect(result).toBe(10); + }); + + it('should clear all cache', () => { + const clearSpy = jest.spyOn(relationshipManager['cache'], 'clear'); + + relationshipManager.clearRelationshipCache(); + + expect(clearSpy).toHaveBeenCalled(); + }); + }); + + describe('Error Handling', () => { + it('should throw error for non-existent relationship', async () => { + await expect(relationshipManager.loadRelationship(user, 'nonExistentRelation')).rejects.toThrow( + "Relationship 'nonExistentRelation' not found on User" + ); + }); + + it('should throw error for unsupported relationship type', async () => { + // Mock an invalid relationship type + const originalRelationships = User.relationships; + User.relationships = new Map(); + User.relationships.set('invalidRelation', { + type: 'unsupported' as any, + model: Post, + foreignKey: 'userId', + propertyKey: 'invalidRelation' + }); + + await expect(relationshipManager.loadRelationship(user, 'invalidRelation')).rejects.toThrow( + 'Unsupported relationship type: unsupported' + ); + + // Restore original relationships + User.relationships = originalRelationships; + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/sharding/ShardManager.test.ts b/tests/unit/sharding/ShardManager.test.ts new file mode 100644 index 0000000..27efbd8 --- /dev/null +++ b/tests/unit/sharding/ShardManager.test.ts @@ -0,0 +1,436 @@ +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { ShardManager, ShardInfo } from '../../../src/framework/sharding/ShardManager'; +import { FrameworkOrbitDBService } from '../../../src/framework/services/OrbitDBService'; +import { ShardingConfig } from '../../../src/framework/types/framework'; +import { createMockServices } from '../../mocks/services'; + +describe('ShardManager', () => { + let shardManager: ShardManager; + let mockOrbitDBService: FrameworkOrbitDBService; + let mockDatabase: any; + + beforeEach(() => { + const mockServices = createMockServices(); + mockOrbitDBService = mockServices.orbitDBService; + + // Create mock database + mockDatabase = { + address: { toString: () => 'mock-address-123' }, + set: jest.fn().mockResolvedValue(undefined), + get: jest.fn().mockResolvedValue(null), + del: jest.fn().mockResolvedValue(undefined), + put: jest.fn().mockResolvedValue('mock-hash'), + add: jest.fn().mockResolvedValue('mock-hash'), + query: jest.fn().mockReturnValue([]) + }; + + // Mock OrbitDB service methods + jest.spyOn(mockOrbitDBService, 'openDatabase').mockResolvedValue(mockDatabase); + + shardManager = new ShardManager(); + shardManager.setOrbitDBService(mockOrbitDBService); + + jest.clearAllMocks(); + }); + + describe('Initialization', () => { + it('should set OrbitDB service correctly', () => { + const newShardManager = new ShardManager(); + newShardManager.setOrbitDBService(mockOrbitDBService); + + // No direct way to test this, but we can verify it works in other tests + expect(newShardManager).toBeInstanceOf(ShardManager); + }); + + it('should throw error when OrbitDB service not set', async () => { + const newShardManager = new ShardManager(); + const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' }; + + await expect(newShardManager.createShards('TestModel', config)).rejects.toThrow( + 'OrbitDB service not initialized' + ); + }); + }); + + describe('Shard Creation', () => { + it('should create shards with hash strategy', async () => { + const config: ShardingConfig = { strategy: 'hash', count: 3, key: 'id' }; + + await shardManager.createShards('TestModel', config, 'docstore'); + + // Should create 3 shards + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledTimes(3); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('testmodel-shard-0', 'docstore'); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('testmodel-shard-1', 'docstore'); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('testmodel-shard-2', 'docstore'); + + const shards = shardManager.getAllShards('TestModel'); + expect(shards).toHaveLength(3); + expect(shards[0]).toMatchObject({ + name: 'testmodel-shard-0', + index: 0, + address: 'mock-address-123' + }); + }); + + it('should create shards with range strategy', async () => { + const config: ShardingConfig = { strategy: 'range', count: 2, key: 'name' }; + + await shardManager.createShards('RangeModel', config, 'keyvalue'); + + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledTimes(2); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('rangemodel-shard-0', 'keyvalue'); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('rangemodel-shard-1', 'keyvalue'); + }); + + it('should create shards with user strategy', async () => { + const config: ShardingConfig = { strategy: 'user', count: 4, key: 'userId' }; + + await shardManager.createShards('UserModel', config); + + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledTimes(4); + + const shards = shardManager.getAllShards('UserModel'); + expect(shards).toHaveLength(4); + }); + + it('should handle shard creation errors', async () => { + const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' }; + + jest.spyOn(mockOrbitDBService, 'openDatabase').mockRejectedValueOnce(new Error('Database creation failed')); + + await expect(shardManager.createShards('FailModel', config)).rejects.toThrow('Database creation failed'); + }); + }); + + describe('Shard Routing', () => { + beforeEach(async () => { + const config: ShardingConfig = { strategy: 'hash', count: 4, key: 'id' }; + await shardManager.createShards('TestModel', config); + }); + + it('should route keys to consistent shards with hash strategy', () => { + const key1 = 'user-123'; + const key2 = 'user-456'; + const key3 = 'user-123'; // Same as key1 + + const shard1 = shardManager.getShardForKey('TestModel', key1); + const shard2 = shardManager.getShardForKey('TestModel', key2); + const shard3 = shardManager.getShardForKey('TestModel', key3); + + // Same keys should route to same shards + expect(shard1.index).toBe(shard3.index); + + // Different keys may route to different shards + expect(shard1.index).toBeGreaterThanOrEqual(0); + expect(shard1.index).toBeLessThan(4); + expect(shard2.index).toBeGreaterThanOrEqual(0); + expect(shard2.index).toBeLessThan(4); + }); + + it('should route keys with range strategy', async () => { + const config: ShardingConfig = { strategy: 'range', count: 3, key: 'name' }; + await shardManager.createShards('RangeModel', config); + + const shardA = shardManager.getShardForKey('RangeModel', 'apple'); + const shardM = shardManager.getShardForKey('RangeModel', 'middle'); + const shardZ = shardManager.getShardForKey('RangeModel', 'zebra'); + + // Keys starting with different letters should potentially route to different shards + expect(shardA.index).toBeGreaterThanOrEqual(0); + expect(shardA.index).toBeLessThan(3); + expect(shardM.index).toBeGreaterThanOrEqual(0); + expect(shardM.index).toBeLessThan(3); + expect(shardZ.index).toBeGreaterThanOrEqual(0); + expect(shardZ.index).toBeLessThan(3); + }); + + it('should handle user strategy routing', async () => { + const config: ShardingConfig = { strategy: 'user', count: 2, key: 'userId' }; + await shardManager.createShards('UserModel', config); + + const shard1 = shardManager.getShardForKey('UserModel', 'user-abc'); + const shard2 = shardManager.getShardForKey('UserModel', 'user-def'); + const shard3 = shardManager.getShardForKey('UserModel', 'user-abc'); // Same as shard1 + + expect(shard1.index).toBe(shard3.index); + expect(shard1.index).toBeGreaterThanOrEqual(0); + expect(shard1.index).toBeLessThan(2); + }); + + it('should throw error for unsupported sharding strategy', async () => { + const config: ShardingConfig = { strategy: 'unsupported' as any, count: 2, key: 'id' }; + await shardManager.createShards('UnsupportedModel', config); + + expect(() => { + shardManager.getShardForKey('UnsupportedModel', 'test-key'); + }).toThrow('Unsupported sharding strategy: unsupported'); + }); + + it('should throw error when no shards exist for model', () => { + expect(() => { + shardManager.getShardForKey('NonExistentModel', 'test-key'); + }).toThrow('No shards found for model NonExistentModel'); + }); + + it('should throw error when no shard configuration exists', async () => { + // Manually clear the config to simulate this error + const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' }; + await shardManager.createShards('ConfigTestModel', config); + + // Access private property for testing (not ideal but necessary for this test) + (shardManager as any).shardConfigs.delete('ConfigTestModel'); + + expect(() => { + shardManager.getShardForKey('ConfigTestModel', 'test-key'); + }).toThrow('No shard configuration found for model ConfigTestModel'); + }); + }); + + describe('Shard Management', () => { + beforeEach(async () => { + const config: ShardingConfig = { strategy: 'hash', count: 3, key: 'id' }; + await shardManager.createShards('TestModel', config); + }); + + it('should get all shards for a model', () => { + const shards = shardManager.getAllShards('TestModel'); + + expect(shards).toHaveLength(3); + expect(shards[0].name).toBe('testmodel-shard-0'); + expect(shards[1].name).toBe('testmodel-shard-1'); + expect(shards[2].name).toBe('testmodel-shard-2'); + }); + + it('should return empty array for non-existent model', () => { + const shards = shardManager.getAllShards('NonExistentModel'); + expect(shards).toEqual([]); + }); + + it('should get shard by index', () => { + const shard0 = shardManager.getShardByIndex('TestModel', 0); + const shard1 = shardManager.getShardByIndex('TestModel', 1); + const shard2 = shardManager.getShardByIndex('TestModel', 2); + const shardInvalid = shardManager.getShardByIndex('TestModel', 5); + + expect(shard0?.index).toBe(0); + expect(shard1?.index).toBe(1); + expect(shard2?.index).toBe(2); + expect(shardInvalid).toBeUndefined(); + }); + + it('should get shard count', () => { + const count = shardManager.getShardCount('TestModel'); + expect(count).toBe(3); + + const nonExistentCount = shardManager.getShardCount('NonExistentModel'); + expect(nonExistentCount).toBe(0); + }); + + it('should get all models with shards', () => { + const models = shardManager.getAllModelsWithShards(); + expect(models).toContain('TestModel'); + }); + }); + + describe('Global Index Management', () => { + it('should create global index with shards', async () => { + await shardManager.createGlobalIndex('TestModel', 'username-index'); + + // Should create 4 index shards (default) + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledTimes(4); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('username-index-shard-0', 'keyvalue'); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('username-index-shard-1', 'keyvalue'); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('username-index-shard-2', 'keyvalue'); + expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('username-index-shard-3', 'keyvalue'); + + const indexShards = shardManager.getAllShards('username-index'); + expect(indexShards).toHaveLength(4); + }); + + it('should add to global index', async () => { + await shardManager.createGlobalIndex('TestModel', 'email-index'); + + await shardManager.addToGlobalIndex('email-index', 'user@example.com', 'user-123'); + + // Should call set on one of the index shards + expect(mockDatabase.set).toHaveBeenCalledWith('user@example.com', 'user-123'); + }); + + it('should get from global index', async () => { + await shardManager.createGlobalIndex('TestModel', 'id-index'); + + mockDatabase.get.mockResolvedValue('user-456'); + + const result = await shardManager.getFromGlobalIndex('id-index', 'lookup-key'); + + expect(result).toBe('user-456'); + expect(mockDatabase.get).toHaveBeenCalledWith('lookup-key'); + }); + + it('should remove from global index', async () => { + await shardManager.createGlobalIndex('TestModel', 'remove-index'); + + await shardManager.removeFromGlobalIndex('remove-index', 'key-to-remove'); + + expect(mockDatabase.del).toHaveBeenCalledWith('key-to-remove'); + }); + + it('should handle missing global index', async () => { + await expect( + shardManager.addToGlobalIndex('non-existent-index', 'key', 'value') + ).rejects.toThrow('Global index non-existent-index not found'); + + await expect( + shardManager.getFromGlobalIndex('non-existent-index', 'key') + ).rejects.toThrow('Global index non-existent-index not found'); + + await expect( + shardManager.removeFromGlobalIndex('non-existent-index', 'key') + ).rejects.toThrow('Global index non-existent-index not found'); + }); + + it('should handle global index operation errors', async () => { + await shardManager.createGlobalIndex('TestModel', 'error-index'); + + mockDatabase.set.mockRejectedValue(new Error('Database error')); + mockDatabase.get.mockRejectedValue(new Error('Database error')); + mockDatabase.del.mockRejectedValue(new Error('Database error')); + + await expect( + shardManager.addToGlobalIndex('error-index', 'key', 'value') + ).rejects.toThrow('Database error'); + + const result = await shardManager.getFromGlobalIndex('error-index', 'key'); + expect(result).toBeNull(); // Should return null on error + + await expect( + shardManager.removeFromGlobalIndex('error-index', 'key') + ).rejects.toThrow('Database error'); + }); + }); + + describe('Query Operations', () => { + beforeEach(async () => { + const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' }; + await shardManager.createShards('QueryModel', config); + }); + + it('should query all shards', async () => { + const mockQueryFn = jest.fn() + .mockResolvedValueOnce([{ id: '1', name: 'test1' }]) + .mockResolvedValueOnce([{ id: '2', name: 'test2' }]); + + const results = await shardManager.queryAllShards('QueryModel', mockQueryFn); + + expect(mockQueryFn).toHaveBeenCalledTimes(2); + expect(results).toEqual([ + { id: '1', name: 'test1' }, + { id: '2', name: 'test2' } + ]); + }); + + it('should handle query errors gracefully', async () => { + const mockQueryFn = jest.fn() + .mockResolvedValueOnce([{ id: '1', name: 'test1' }]) + .mockRejectedValueOnce(new Error('Query failed')); + + const results = await shardManager.queryAllShards('QueryModel', mockQueryFn); + + expect(results).toEqual([{ id: '1', name: 'test1' }]); + }); + + it('should throw error when querying non-existent model', async () => { + const mockQueryFn = jest.fn(); + + await expect( + shardManager.queryAllShards('NonExistentModel', mockQueryFn) + ).rejects.toThrow('No shards found for model NonExistentModel'); + }); + }); + + describe('Statistics and Monitoring', () => { + beforeEach(async () => { + const config: ShardingConfig = { strategy: 'hash', count: 3, key: 'id' }; + await shardManager.createShards('StatsModel', config); + }); + + it('should get shard statistics', () => { + const stats = shardManager.getShardStatistics('StatsModel'); + + expect(stats).toEqual({ + modelName: 'StatsModel', + shardCount: 3, + shards: [ + { name: 'statsmodel-shard-0', index: 0, address: 'mock-address-123' }, + { name: 'statsmodel-shard-1', index: 1, address: 'mock-address-123' }, + { name: 'statsmodel-shard-2', index: 2, address: 'mock-address-123' } + ] + }); + }); + + it('should return null for non-existent model statistics', () => { + const stats = shardManager.getShardStatistics('NonExistentModel'); + expect(stats).toBeNull(); + }); + + it('should list all models with shards', async () => { + const config1: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' }; + const config2: ShardingConfig = { strategy: 'range', count: 3, key: 'name' }; + + await shardManager.createShards('Model1', config1); + await shardManager.createShards('Model2', config2); + + const models = shardManager.getAllModelsWithShards(); + + expect(models).toContain('StatsModel'); // From beforeEach + expect(models).toContain('Model1'); + expect(models).toContain('Model2'); + expect(models.length).toBeGreaterThanOrEqual(3); + }); + }); + + describe('Hash Function Consistency', () => { + it('should produce consistent hash results', () => { + // Test the hash function directly by creating shards and checking consistency + const testKeys = ['user-123', 'user-456', 'user-789', 'user-abc', 'user-def']; + const shardCount = 4; + + // Get shard indices for each key multiple times + const config: ShardingConfig = { strategy: 'hash', count: shardCount, key: 'id' }; + + return shardManager.createShards('HashTestModel', config).then(() => { + testKeys.forEach(key => { + const shard1 = shardManager.getShardForKey('HashTestModel', key); + const shard2 = shardManager.getShardForKey('HashTestModel', key); + const shard3 = shardManager.getShardForKey('HashTestModel', key); + + // Same key should always route to same shard + expect(shard1.index).toBe(shard2.index); + expect(shard2.index).toBe(shard3.index); + + // Shard index should be within valid range + expect(shard1.index).toBeGreaterThanOrEqual(0); + expect(shard1.index).toBeLessThan(shardCount); + }); + }); + }); + }); + + describe('Cleanup', () => { + it('should stop and clear all resources', async () => { + const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' }; + await shardManager.createShards('CleanupModel', config); + await shardManager.createGlobalIndex('CleanupModel', 'cleanup-index'); + + expect(shardManager.getAllShards('CleanupModel')).toHaveLength(2); + expect(shardManager.getAllShards('cleanup-index')).toHaveLength(4); + + await shardManager.stop(); + + expect(shardManager.getAllShards('CleanupModel')).toHaveLength(0); + expect(shardManager.getAllShards('cleanup-index')).toHaveLength(0); + expect(shardManager.getAllModelsWithShards()).toHaveLength(0); + }); + }); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f7c463c..f68923d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,8 @@ "moduleResolution": "bundler", /* Skip type checking of declaration files. */ "skipLibCheck": true, + /* Perform compilation without referencing other files. */ + "isolatedModules": true, /* Removes comments from the project's output JavaScript code. */ "removeComments": true, /* Enables experimental support for emitting type metadata for decorators which works with the module reflect-metadata. */ @@ -51,7 +53,7 @@ // }, }, "include": ["src/**/*", "orbitdb.d.ts", "types.d.ts"], - "exclude": ["coverage", "dist", "eslint.config.js", "node_modules"], + "exclude": ["coverage", "dist", "eslint.config.js", "node_modules", "tests"], "ts-node": { "esm": true } diff --git a/tsconfig.tests.json b/tsconfig.tests.json new file mode 100644 index 0000000..32b0a65 --- /dev/null +++ b/tsconfig.tests.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": ["jest", "node"] + }, + "include": ["tests/**/*", "src/**/*"], + "exclude": ["node_modules", "dist", "coverage"] +} \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index 830f18f..d7f324f 100644 --- a/types.d.ts +++ b/types.d.ts @@ -3,352 +3,5 @@ // Definitions by: Debros Team declare module '@debros/network' { - import { Request, Response, NextFunction } from 'express'; - - // Config types - export interface DebrosConfig { - env: { - fingerprint: string; - port: number; - }; - ipfs: { - swarm: { - port: number; - announceAddresses: string[]; - listenAddresses: string[]; - connectAddresses: string[]; - }; - blockstorePath: string; - bootstrap: string[]; - privateKey?: string; - serviceDiscovery?: { - topic: string; - heartbeatInterval: number; - }; - }; - orbitdb: { - directory: string; - }; - logger: { - level: string; - file?: string; - }; - } - - export interface ValidationResult { - valid: boolean; - errors?: string[]; - } - - // Core configuration - export const config: DebrosConfig; - export const defaultConfig: DebrosConfig; - export function validateConfig(config: Partial): ValidationResult; - - // Store types - export enum StoreType { - KEYVALUE = 'keyvalue', - DOCSTORE = 'docstore', - FEED = 'feed', - EVENTLOG = 'eventlog', - COUNTER = 'counter', - } - - // Error handling - export enum ErrorCode { - NOT_INITIALIZED = 'ERR_NOT_INITIALIZED', - INITIALIZATION_FAILED = 'ERR_INIT_FAILED', - DOCUMENT_NOT_FOUND = 'ERR_DOC_NOT_FOUND', - INVALID_SCHEMA = 'ERR_INVALID_SCHEMA', - OPERATION_FAILED = 'ERR_OPERATION_FAILED', - TRANSACTION_FAILED = 'ERR_TRANSACTION_FAILED', - FILE_NOT_FOUND = 'ERR_FILE_NOT_FOUND', - INVALID_PARAMETERS = 'ERR_INVALID_PARAMS', - CONNECTION_ERROR = 'ERR_CONNECTION', - STORE_TYPE_ERROR = 'ERR_STORE_TYPE', - } - - export class DBError extends Error { - code: ErrorCode; - details?: any; - constructor(code: ErrorCode, message: string, details?: any); - } - - // Schema validation - export interface SchemaDefinition { - type: string; - required?: boolean; - pattern?: string; - min?: number; - max?: number; - enum?: any[]; - items?: SchemaDefinition; // For arrays - properties?: Record; // For objects - } - - export interface CollectionSchema { - properties: Record; - required?: string[]; - } - - // Database types - export interface DocumentMetadata { - createdAt: number; - updatedAt: number; - } - - export interface Document extends DocumentMetadata { - [key: string]: any; - } - - export interface CreateResult { - id: string; - hash: string; - } - - export interface UpdateResult { - id: string; - hash: string; - } - - export interface FileUploadResult { - cid: string; - } - - export interface FileMetadata { - filename?: string; - size: number; - uploadedAt: number; - [key: string]: any; - } - - export interface FileResult { - data: Buffer; - metadata: FileMetadata | null; - } - - export interface ListOptions { - limit?: number; - offset?: number; - sort?: { field: string; order: 'asc' | 'desc' }; - connectionId?: string; - storeType?: StoreType; - } - - export interface QueryOptions extends ListOptions { - indexBy?: string; - } - - export interface PaginatedResult { - documents: T[]; - total: number; - hasMore: boolean; - } - - // Transaction API - export class Transaction { - create(collection: string, id: string, data: T): Transaction; - update(collection: string, id: string, data: Partial): Transaction; - delete(collection: string, id: string): Transaction; - commit(): Promise<{ success: boolean; results: any[] }>; - } - - // Metrics tracking - export interface Metrics { - operations: { - creates: number; - reads: number; - updates: number; - deletes: number; - queries: number; - fileUploads: number; - fileDownloads: number; - }; - performance: { - totalOperationTime: number; - operationCount: number; - averageOperationTime: number; - }; - errors: { - count: number; - byCode: Record; - }; - cacheStats: { - hits: number; - misses: number; - }; - startTime: number; - } - - // Database Operations - export function initDB(connectionId?: string): Promise; - export function create>( - collection: string, - id: string, - data: Omit, - options?: { connectionId?: string; storeType?: StoreType }, - ): Promise; - export function get>( - collection: string, - id: string, - options?: { connectionId?: string; skipCache?: boolean; storeType?: StoreType }, - ): Promise; - export function update>( - collection: string, - id: string, - data: Partial>, - options?: { connectionId?: string; upsert?: boolean; storeType?: StoreType }, - ): Promise; - export function remove( - collection: string, - id: string, - options?: { connectionId?: string; storeType?: StoreType }, - ): Promise; - export function list>( - collection: string, - options?: ListOptions, - ): Promise>; - export function query>( - collection: string, - filter: (doc: T) => boolean, - options?: QueryOptions, - ): Promise>; - - // Schema operations - export function defineSchema(collection: string, schema: CollectionSchema): void; - - // Transaction operations - export function createTransaction(connectionId?: string): Transaction; - export function commitTransaction( - transaction: Transaction, - ): Promise<{ success: boolean; results: any[] }>; - - // Index operations - export function createIndex( - collection: string, - field: string, - options?: { connectionId?: string; storeType?: StoreType }, - ): Promise; - - // Event Subscription API - export interface DocumentCreatedEvent { - collection: string; - id: string; - document: any; - } - - export interface DocumentUpdatedEvent { - collection: string; - id: string; - document: any; - previous: any; - } - - export interface DocumentDeletedEvent { - collection: string; - id: string; - document: any; - } - - export type DBEventType = 'document:created' | 'document:updated' | 'document:deleted'; - - export function subscribe( - event: 'document:created', - callback: (data: DocumentCreatedEvent) => void, - ): () => void; - export function subscribe( - event: 'document:updated', - callback: (data: DocumentUpdatedEvent) => void, - ): () => void; - export function subscribe( - event: 'document:deleted', - callback: (data: DocumentDeletedEvent) => void, - ): () => void; - export function subscribe(event: DBEventType, callback: (data: any) => void): () => void; - - // File operations - export function uploadFile( - fileData: Buffer, - options?: { filename?: string; connectionId?: string; metadata?: Record }, - ): Promise; - export function getFile(cid: string, options?: { connectionId?: string }): Promise; - export function deleteFile(cid: string, options?: { connectionId?: string }): Promise; - - // Connection management - export function closeConnection(connectionId: string): Promise; - - // Metrics - // Stop - export function stopDB(): Promise; - - // Logger - export interface LoggerOptions { - level?: string; - file?: string; - service?: string; - } - export const logger: any; - export function createServiceLogger(name: string, options?: LoggerOptions): any; - export function createDebrosLogger(options?: LoggerOptions): any; - - // Load Balancer - export interface LoadBalancerControllerModule { - getNodeInfo: (req: Request, res: Response, next: NextFunction) => void; - getOptimalPeer: (req: Request, res: Response, next: NextFunction) => void; - getAllPeers: (req: Request, res: Response, next: NextFunction) => void; - } - export const loadBalancerController: LoadBalancerControllerModule; - - export const getConnectedPeers: () => Map< - string, - { - lastSeen: number; - load: number; - publicAddress: string; - fingerprint: string; - } - >; - - export const logPeersStatus: () => void; - - // Default export - const defaultExport: { - config: DebrosConfig; - validateConfig: typeof validateConfig; - db: { - init: typeof initDB; - create: typeof create; - get: typeof get; - update: typeof update; - remove: typeof remove; - list: typeof list; - query: typeof query; - createIndex: typeof createIndex; - createTransaction: typeof createTransaction; - commitTransaction: typeof commitTransaction; - subscribe: typeof subscribe; - uploadFile: typeof uploadFile; - getFile: typeof getFile; - deleteFile: typeof deleteFile; - defineSchema: typeof defineSchema; - closeConnection: typeof closeConnection; - stop: typeof stopDB; - ErrorCode: typeof ErrorCode; - StoreType: typeof StoreType; - }; - loadBalancerController: LoadBalancerControllerModule; - getConnectedPeers: () => Map< - string, - { - lastSeen: number; - load: number; - publicAddress: string; - fingerprint: string; - } - >; - logPeersStatus: () => void; - logger: any; - createServiceLogger: typeof createServiceLogger; - }; - export default defaultExport; + }