feat(laws,opendata): add local law storage and migrate OpenData persistence to smartdb-backed local storage

This commit is contained in:
2026-04-17 11:51:02 +00:00
parent 79e74a34ed
commit 73801f785a
40 changed files with 8514 additions and 7266 deletions
-1
View File
@@ -1 +0,0 @@
/cache
-67
View File
@@ -1,67 +0,0 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "opendata"
+12 -6
View File
@@ -1,5 +1,5 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "gitlab.com", "githost": "gitlab.com",
@@ -26,13 +26,19 @@
"business registry", "business registry",
"data retrieval" "data retrieval"
] ]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
} }
}, },
"npmci": { "@git.zone/tsdoc": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n" "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": []
} }
} }
+1
View File
@@ -0,0 +1 @@
Important: Also read .nogit/AGENTS.md
-1
View File
@@ -1 +0,0 @@
Read .nogit/CLAUDE.md
+8
View File
@@ -1,5 +1,13 @@
# Changelog # Changelog
## 2026-04-17 - 3.6.0 - feat(laws,opendata)
add local law storage and migrate OpenData persistence to smartdb-backed local storage
- introduces a new LawService export with searchable LawRecord persistence for German, EU, and US laws
- replaces OpenData startup dependency on external MongoDB environment configuration with embedded local smartdb bootstrap
- hardens Handelsregister downloads and integration tests with better skip handling and downloaded file detection
- updates build and dependency configuration to newer git.zone and push.rocks packages and switches project metadata to .smartconfig.json
## 2025-11-07 - 3.5.0 - feat(stocks) ## 2025-11-07 - 3.5.0 - feat(stocks)
Add provider fetch limits, intraday incremental fetch, cache deduplication, and provider safety/warning improvements Add provider fetch limits, intraday incremental fetch, cache deduplication, and provider safety/warning improvements
Generated
+2787 -2206
View File
File diff suppressed because it is too large Load Diff
+15 -13
View File
@@ -10,32 +10,34 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "(tstest test/ --verbose)", "test": "(tstest test/ --verbose)",
"build": "(tsbuild --web --allowimplicitany)", "build": "(tsbuild)",
"buildDocs": "(tsdoc)" "buildDocs": "(tsdoc)"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.6.8", "@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.5.1", "@git.zone/tsbundle": "^2.10.0",
"@git.zone/tsrun": "^1.6.2", "@git.zone/tsrun": "^2.0.2",
"@git.zone/tstest": "^2.7.0", "@git.zone/tstest": "^3.6.3",
"@types/node": "^22.14.0" "@types/node": "25.6.0"
}, },
"dependencies": { "dependencies": {
"@push.rocks/lik": "^6.2.2", "@push.rocks/lik": "^6.2.2",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartarchive": "^4.2.2", "@push.rocks/smartarchive": "^5.2.1",
"@push.rocks/smartarray": "^1.1.0", "@push.rocks/smartarray": "^1.1.0",
"@push.rocks/smartbrowser": "^2.0.8", "@push.rocks/smartbrowser": "^2.0.8",
"@push.rocks/smartdata": "^5.16.4", "@push.rocks/smartdata": "^7.1.7",
"@push.rocks/smartdb": "^2.7.0",
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfile": "^11.2.7", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^4.3.4", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartstream": "^3.2.5", "@push.rocks/smartstream": "^3.4.0",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartxml": "^1.1.1", "@push.rocks/smartxml": "^2.0.0",
"@tsclass/tsclass": "^9.3.0" "@tsclass/tsclass": "^9.3.0"
}, },
"repository": { "repository": {
@@ -58,7 +60,7 @@
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
"npmextra.json", ".smartconfig.json",
"readme.md" "readme.md"
], ],
"keywords": [ "keywords": [
+3792 -3958
View File
File diff suppressed because it is too large Load Diff
+306 -688
View File
File diff suppressed because it is too large Load Diff
-193
View File
@@ -1,193 +0,0 @@
# Stock Prices Module Implementation Plan
Command to reread guidelines: Read /home/philkunz/.claude/CLAUDE.md
## Overview
Implementation of a stocks module for fetching current stock prices using various APIs. The architecture will support multiple providers, but we'll start with implementing only Yahoo Finance API. The design will make it easy to add additional providers (Alpha Vantage, IEX Cloud, etc.) in the future without changing the core architecture.
## Phase 1: Yahoo Finance Implementation
### 1.1 Research & Documentation
- [ ] Research Yahoo Finance API endpoints (no official API, using public endpoints)
- [ ] Document available data fields and formats
- [ ] Identify rate limits and restrictions
- [ ] Test endpoints manually with curl
### 1.2 Module Structure
```
ts/
├── index.ts # Main exports
├── plugins.ts # External dependencies
└── stocks/
├── index.ts # Stocks module exports
├── classes.stockservice.ts # Main StockPriceService class
├── interfaces/
│ ├── stockprice.ts # IStockPrice interface
│ └── provider.ts # IStockProvider interface (for all providers)
└── providers/
├── provider.yahoo.ts # Yahoo Finance implementation
└── (future: provider.alphavantage.ts, provider.iex.ts, etc.)
```
### 1.3 Core Interfaces
```typescript
// IStockPrice - Standardized stock price data
interface IStockPrice {
ticker: string;
price: number;
currency: string;
change: number;
changePercent: number;
timestamp: Date;
provider: string;
}
// IStockProvider - Provider implementation contract
interface IStockProvider {
name: string;
fetchPrice(ticker: string): Promise<IStockPrice>;
fetchPrices(tickers: string[]): Promise<IStockPrice[]>;
isAvailable(): Promise<boolean>;
}
```
### 1.4 Yahoo Finance Provider Implementation
- [ ] Create YahooFinanceProvider class
- [ ] Implement HTTP requests to Yahoo Finance endpoints
- [ ] Parse response data into IStockPrice format
- [ ] Handle errors and edge cases
- [ ] Add request throttling/rate limiting
### 1.5 Main Service Class
- [ ] Create StockPriceService class with provider registry
- [ ] Implement provider interface for pluggable providers
- [ ] Register Yahoo provider (with ability to add more later)
- [ ] Add method for single ticker lookup
- [ ] Add method for batch ticker lookup
- [ ] Implement error handling with graceful degradation
- [ ] Design fallback mechanism (ready for multiple providers)
## Phase 2: Core Features
### 2.1 Service Architecture
- [ ] Create provider registry pattern for managing multiple providers
- [ ] Implement provider priority and selection logic
- [ ] Design provider health check interface
- [ ] Create provider configuration system
- [ ] Implement provider discovery mechanism
- [ ] Add provider capability querying (which tickers/markets supported)
## Phase 3: Advanced Features
### 3.1 Caching System
- [ ] Design cache interface
- [ ] Implement in-memory cache with TTL
- [ ] Add cache invalidation logic
- [ ] Make cache configurable per ticker
### 3.2 Configuration
- [ ] Provider configuration (timeout, retry settings)
- [ ] Cache configuration (TTL, max entries)
- [ ] Request timeout configuration
- [ ] Error handling configuration
### 3.3 Error Handling
- [ ] Define custom error types
- [ ] Implement retry logic with exponential backoff
- [ ] Add circuit breaker pattern for failing providers
- [ ] Comprehensive error logging
## Phase 4: Testing
### 4.1 Unit Tests
- [ ] Test each provider independently
- [ ] Mock HTTP requests for predictable testing
- [ ] Test error scenarios
- [ ] Test data transformation logic
### 4.2 Integration Tests
- [ ] Test with real API calls (rate limit aware)
- [ ] Test provider fallback scenarios
- [ ] Test batch operations
- [ ] Test cache behavior
### 4.3 Performance Tests
- [ ] Measure response times
- [ ] Test concurrent request handling
- [ ] Validate cache effectiveness
## Implementation Order
1. **Week 1: Yahoo Finance Provider**
- Research and test Yahoo endpoints
- Implement basic provider and service
- Create core interfaces
- Basic error handling
2. **Week 2: Service Architecture**
- Create extensible provider system
- Implement provider interface
- Add provider registration
3. **Week 3: Advanced Features**
- Implement caching system
- Add configuration management
- Enhance error handling
4. **Week 4: Testing & Documentation**
- Write comprehensive tests
- Create usage documentation
- Performance optimization
## Dependencies
### Required
- `@push.rocks/smartrequest` - HTTP requests
- `@push.rocks/smartpromise` - Promise utilities
- `@push.rocks/smartlog` - Logging
### Development
- `@git.zone/tstest` - Testing framework
- `@git.zone/tsrun` - TypeScript execution
## API Endpoints Research
### Yahoo Finance
- Base URL: `https://query1.finance.yahoo.com/v8/finance/chart/{ticker}`
- No authentication required
- Returns JSON with price data
- Rate limits unknown (need to test)
- Alternative endpoints to explore:
- `/v7/finance/quote` - Simplified quote data
- `/v10/finance/quoteSummary` - Detailed company data
## Success Criteria
1. Can fetch current stock prices for any valid ticker
2. Extensible architecture for future providers
3. Response time < 1 second for cached data
4. Response time < 3 seconds for fresh data
5. Proper error handling and recovery
6. Comprehensive test coverage (>80%)
## Notes
- Yahoo Finance provides free stock data without authentication
- **Architecture designed for multiple providers**: While only implementing Yahoo Finance initially, all interfaces, classes, and patterns are designed to support multiple stock data providers
- The provider registry pattern allows adding new providers without modifying existing code
- Each provider implements the same IStockProvider interface for consistency
- Future providers can be added by simply creating a new provider class and registering it
- Implement proper TypeScript types for all data structures
- Follow the project's coding standards (prefix interfaces with 'I')
- Use plugins.ts for all external dependencies
- Keep filenames lowercase
- Write tests using @git.zone/tstest with smartexpect syntax
- Focus on clean, extensible architecture for future growth
## Future Provider Addition Example
When ready to add a new provider (e.g., Alpha Vantage), the process will be:
1. Create `ts/stocks/providers/provider.alphavantage.ts`
2. Implement the `IStockProvider` interface
3. Register the provider in the StockPriceService
4. No changes needed to existing code or interfaces
@@ -1,5 +1,6 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as opendata from '../ts/index.js'; import * as opendata from '../ts/index.js';
import * as plugins from '../ts/plugins.js';
// Test data // Test data
const testCryptos = ['BTC', 'ETH', 'USDT']; const testCryptos = ['BTC', 'ETH', 'USDT'];
@@ -8,6 +9,28 @@ const invalidCrypto = 'INVALID_CRYPTO_XYZ_12345';
let stockService: opendata.StockPriceService; let stockService: opendata.StockPriceService;
let coingeckoProvider: opendata.CoinGeckoProvider; let coingeckoProvider: opendata.CoinGeckoProvider;
let coingeckoAvailable = false;
let coingeckoSkipReason = 'CoinGecko integration requirements are unavailable.';
type TSkipTools = {
skip: (reason?: string) => never;
};
const runCoinGeckoRequest = async <T>(toolsArg: TSkipTools, operation: () => Promise<T>): Promise<T> => {
try {
return await operation();
} catch (error) {
const errorMessage = plugins.getErrorMessage(error);
if (errorMessage.includes('Rate limit exceeded')) {
coingeckoAvailable = false;
coingeckoSkipReason = `Skipping CoinGecko integration tests: ${errorMessage}.`;
toolsArg.skip(coingeckoSkipReason);
}
throw error;
}
};
tap.test('should create StockPriceService instance', async () => { tap.test('should create StockPriceService instance', async () => {
stockService = new opendata.StockPriceService({ stockService = new opendata.StockPriceService({
@@ -23,22 +46,29 @@ tap.test('should create CoinGeckoProvider instance without API key', async () =>
expect(coingeckoProvider.name).toEqual('CoinGecko'); expect(coingeckoProvider.name).toEqual('CoinGecko');
expect(coingeckoProvider.requiresAuth).toEqual(false); expect(coingeckoProvider.requiresAuth).toEqual(false);
expect(coingeckoProvider.priority).toEqual(90); expect(coingeckoProvider.priority).toEqual(90);
coingeckoAvailable = await coingeckoProvider.isAvailable();
if (!coingeckoAvailable) {
coingeckoSkipReason = 'Skipping CoinGecko integration tests: provider is not reachable.';
}
}); });
tap.test('should register CoinGecko provider with the service', async () => { tap.test('should register CoinGecko provider with the service', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
stockService.register(coingeckoProvider); stockService.register(coingeckoProvider);
const providers = stockService.getAllProviders(); const providers = stockService.getAllProviders();
expect(providers).toContainEqual(coingeckoProvider); expect(providers).toContainEqual(coingeckoProvider);
expect(stockService.getProvider('CoinGecko')).toEqual(coingeckoProvider); expect(stockService.getProvider('CoinGecko')).toEqual(coingeckoProvider);
}); });
tap.test('should check CoinGecko provider health', async () => { tap.test('should check CoinGecko provider health', async (toolsArg) => {
const health = await stockService.checkProvidersHealth(); toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
const health = await runCoinGeckoRequest(toolsArg, async () => stockService.checkProvidersHealth());
expect(health.get('CoinGecko')).toEqual(true); expect(health.get('CoinGecko')).toEqual(true);
}); });
tap.test('should fetch single crypto price using ticker symbol (BTC)', async () => { tap.test('should fetch single crypto price using ticker symbol (BTC)', async (toolsArg) => {
const price = await stockService.getPrice({ ticker: 'BTC' }); toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
const price = await runCoinGeckoRequest(toolsArg, async () => stockService.getPrice({ ticker: 'BTC' }));
expect(price).toHaveProperty('ticker'); expect(price).toHaveProperty('ticker');
expect(price).toHaveProperty('price'); expect(price).toHaveProperty('price');
@@ -62,11 +92,12 @@ tap.test('should fetch single crypto price using ticker symbol (BTC)', async ()
console.log(` Change: ${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%`); console.log(` Change: ${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%`);
}); });
tap.test('should fetch single crypto price using CoinGecko ID (bitcoin)', async () => { tap.test('should fetch single crypto price using CoinGecko ID (bitcoin)', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
// Clear cache to ensure fresh fetch // Clear cache to ensure fresh fetch
stockService.clearCache(); stockService.clearCache();
const price = await stockService.getPrice({ ticker: 'bitcoin' }); const price = await runCoinGeckoRequest(toolsArg, async () => stockService.getPrice({ ticker: 'bitcoin' }));
expect(price.ticker).toEqual('BITCOIN'); expect(price.ticker).toEqual('BITCOIN');
expect(price.price).toBeGreaterThan(0); expect(price.price).toBeGreaterThan(0);
@@ -74,12 +105,13 @@ tap.test('should fetch single crypto price using CoinGecko ID (bitcoin)', async
expect(price.companyName).toInclude('Bitcoin'); expect(price.companyName).toInclude('Bitcoin');
}); });
tap.test('should fetch multiple crypto prices (batch)', async () => { tap.test('should fetch multiple crypto prices (batch)', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
stockService.clearCache(); stockService.clearCache();
const prices = await stockService.getPrices({ const prices = await runCoinGeckoRequest(toolsArg, async () => stockService.getPrices({
tickers: testCryptos tickers: testCryptos
}); }));
expect(prices).toBeArray(); expect(prices).toBeArray();
expect(prices.length).toEqual(testCryptos.length); expect(prices.length).toEqual(testCryptos.length);
@@ -98,19 +130,20 @@ tap.test('should fetch multiple crypto prices (batch)', async () => {
} }
}); });
tap.test('should fetch historical crypto prices', async () => { tap.test('should fetch historical crypto prices', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
// Add delay to avoid rate limiting // Add delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise(resolve => setTimeout(resolve, 3000));
const to = new Date(); const to = new Date();
const from = new Date(to.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago const from = new Date(to.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago
const prices = await stockService.getData({ const prices = await runCoinGeckoRequest(toolsArg, async () => stockService.getData({
type: 'historical', type: 'historical',
ticker: 'BTC', ticker: 'BTC',
from: from, from: from,
to: to to: to
}); }));
expect(prices).toBeArray(); expect(prices).toBeArray();
expect((prices as opendata.IStockPrice[]).length).toBeGreaterThan(0); expect((prices as opendata.IStockPrice[]).length).toBeGreaterThan(0);
@@ -143,16 +176,17 @@ tap.test('should fetch historical crypto prices', async () => {
expect(firstPrice.provider).toEqual('CoinGecko'); expect(firstPrice.provider).toEqual('CoinGecko');
}); });
tap.test('should fetch intraday crypto prices (hourly)', async () => { tap.test('should fetch intraday crypto prices (hourly)', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
// Add delay to avoid rate limiting // Add delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise(resolve => setTimeout(resolve, 3000));
const prices = await stockService.getData({ const prices = await runCoinGeckoRequest(toolsArg, async () => stockService.getData({
type: 'intraday', type: 'intraday',
ticker: 'ETH', ticker: 'ETH',
interval: '1hour', interval: '1hour',
limit: 12 // Last 12 hours limit: 12 // Last 12 hours
}); }));
expect(prices).toBeArray(); expect(prices).toBeArray();
expect((prices as opendata.IStockPrice[]).length).toBeGreaterThan(0); expect((prices as opendata.IStockPrice[]).length).toBeGreaterThan(0);
@@ -175,9 +209,10 @@ tap.test('should fetch intraday crypto prices (hourly)', async () => {
expect(firstPrice.provider).toEqual('CoinGecko'); expect(firstPrice.provider).toEqual('CoinGecko');
}); });
tap.test('should serve cached prices on subsequent requests', async () => { tap.test('should serve cached prices on subsequent requests', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
// First request - should hit the API // First request - should hit the API
const firstRequest = await stockService.getPrice({ ticker: 'BTC' }); const firstRequest = await runCoinGeckoRequest(toolsArg, async () => stockService.getPrice({ ticker: 'BTC' }));
// Second request - should be served from cache // Second request - should be served from cache
const secondRequest = await stockService.getPrice({ ticker: 'BTC' }); const secondRequest = await stockService.getPrice({ ticker: 'BTC' });
@@ -188,23 +223,26 @@ tap.test('should serve cached prices on subsequent requests', async () => {
expect(secondRequest.fetchedAt).toEqual(firstRequest.fetchedAt); expect(secondRequest.fetchedAt).toEqual(firstRequest.fetchedAt);
}); });
tap.test('should handle invalid crypto ticker gracefully', async () => { tap.test('should handle invalid crypto ticker gracefully', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
try { try {
await stockService.getPrice({ ticker: invalidCrypto }); await runCoinGeckoRequest(toolsArg, async () => stockService.getPrice({ ticker: invalidCrypto }));
throw new Error('Should have thrown an error for invalid ticker'); throw new Error('Should have thrown an error for invalid ticker');
} catch (error) { } catch (error) {
expect(error.message).toInclude('Failed to fetch'); expect(plugins.getErrorMessage(error)).toInclude('Failed to fetch');
} }
}); });
tap.test('should support market checking', async () => { tap.test('should support market checking', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
expect(coingeckoProvider.supportsMarket('CRYPTO')).toEqual(true); expect(coingeckoProvider.supportsMarket('CRYPTO')).toEqual(true);
expect(coingeckoProvider.supportsMarket('BTC')).toEqual(true); expect(coingeckoProvider.supportsMarket('BTC')).toEqual(true);
expect(coingeckoProvider.supportsMarket('ETH')).toEqual(true); expect(coingeckoProvider.supportsMarket('ETH')).toEqual(true);
expect(coingeckoProvider.supportsMarket('NASDAQ')).toEqual(false); expect(coingeckoProvider.supportsMarket('NASDAQ')).toEqual(false);
}); });
tap.test('should support ticker validation', async () => { tap.test('should support ticker validation', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
expect(coingeckoProvider.supportsTicker('BTC')).toEqual(true); expect(coingeckoProvider.supportsTicker('BTC')).toEqual(true);
expect(coingeckoProvider.supportsTicker('bitcoin')).toEqual(true); expect(coingeckoProvider.supportsTicker('bitcoin')).toEqual(true);
expect(coingeckoProvider.supportsTicker('wrapped-bitcoin')).toEqual(true); expect(coingeckoProvider.supportsTicker('wrapped-bitcoin')).toEqual(true);
@@ -212,11 +250,15 @@ tap.test('should support ticker validation', async () => {
expect(coingeckoProvider.supportsTicker('BTC@USD')).toEqual(false); expect(coingeckoProvider.supportsTicker('BTC@USD')).toEqual(false);
}); });
tap.test('should display provider statistics', async () => { tap.test('should display provider statistics', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
const stats = stockService.getProviderStats(); const stats = stockService.getProviderStats();
const coingeckoStats = stats.get('CoinGecko'); const coingeckoStats = stats.get('CoinGecko');
expect(coingeckoStats).toBeTruthy(); expect(coingeckoStats).toBeTruthy();
if (!coingeckoStats) {
throw new Error('Missing CoinGecko stats');
}
expect(coingeckoStats.successCount).toBeGreaterThan(0); expect(coingeckoStats.successCount).toBeGreaterThan(0);
console.log('\n📊 CoinGecko Provider Statistics:'); console.log('\n📊 CoinGecko Provider Statistics:');
@@ -227,14 +269,15 @@ tap.test('should display provider statistics', async () => {
} }
}); });
tap.test('should display crypto price dashboard', async () => { tap.test('should display crypto price dashboard', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
// Add delay to avoid rate limiting // Add delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise(resolve => setTimeout(resolve, 3000));
stockService.clearCache(); stockService.clearCache();
const cryptos = ['BTC', 'ETH', 'BNB', 'SOL', 'ADA']; const cryptos = ['BTC', 'ETH', 'BNB', 'SOL', 'ADA'];
const prices = await stockService.getPrices({ tickers: cryptos }); const prices = await runCoinGeckoRequest(toolsArg, async () => stockService.getPrices({ tickers: cryptos }));
console.log('\n╔═══════════════════════════════════════════════════════════╗'); console.log('\n╔═══════════════════════════════════════════════════════════╗');
console.log('║ 🌐 CRYPTOCURRENCY PRICE DASHBOARD ║'); console.log('║ 🌐 CRYPTOCURRENCY PRICE DASHBOARD ║');
@@ -253,7 +296,8 @@ tap.test('should display crypto price dashboard', async () => {
console.log(`Fetched at: ${prices[0].fetchedAt.toISOString()}`); console.log(`Fetched at: ${prices[0].fetchedAt.toISOString()}`);
}); });
tap.test('should clear cache', async () => { tap.test('should clear cache', async (toolsArg) => {
toolsArg.skipIf(!coingeckoAvailable, coingeckoSkipReason);
stockService.clearCache(); stockService.clearCache();
// Cache is cleared, no assertions needed // Cache is cleared, no assertions needed
}); });
+20 -6
View File
@@ -12,6 +12,8 @@ const testGermanBusinessDataDir = plugins.path.join(testNogitDir, 'germanbusines
const testOutputDir = plugins.path.join(testNogitDir, 'testoutput'); const testOutputDir = plugins.path.join(testNogitDir, 'testoutput');
let testOpenDataInstance: opendata.OpenData; let testOpenDataInstance: opendata.OpenData;
let handelsregisterStarted = false;
let handelsregisterSkipReason = 'Handelsregister integration requirements are unavailable.';
tap.test('first test', async () => { tap.test('first test', async () => {
testOpenDataInstance = new opendata.OpenData({ testOpenDataInstance = new opendata.OpenData({
@@ -23,32 +25,44 @@ tap.test('first test', async () => {
}); });
tap.test('should start the instance', async () => { tap.test('should start the instance', async () => {
try {
await testOpenDataInstance.start(); await testOpenDataInstance.start();
handelsregisterStarted = true;
} catch (error) {
handelsregisterSkipReason = `Skipping Handelsregister integration tests: ${plugins.getErrorMessage(error)}`;
console.warn(handelsregisterSkipReason);
}
}); });
const resultsSearch = tap.test('should get the data for a company', async () => { const resultsSearch = tap.test('should get the data for a company', async (toolsArg) => {
toolsArg.skipIf(!handelsregisterStarted, handelsregisterSkipReason);
const result = await testOpenDataInstance.handelsregister.searchCompany('LADR', 20); const result = await testOpenDataInstance.handelsregister.searchCompany('LADR', 20);
console.log(result); console.log(result);
return result; return result;
}); });
tap.test('should get the data for a specific company', async () => { tap.test('should get the data for a specific company', async (toolsArg) => {
let testCompany: BusinessRecord['data']['germanParsedRegistration'] = (await resultsSearch.testResultPromise)[0]['germanParsedRegistration']; toolsArg.skipIf(!handelsregisterStarted, handelsregisterSkipReason);
const searchResults = await resultsSearch.testResultPromise as BusinessRecord['data'][];
let testCompany: BusinessRecord['data']['germanParsedRegistration'] = searchResults[0].germanParsedRegistration;
console.log(`trying to find specific company with:`); console.log(`trying to find specific company with:`);
console.log(testCompany); console.log(testCompany);
const result = await testOpenDataInstance.handelsregister.getSpecificCompany(testCompany); const result = await testOpenDataInstance.handelsregister.getSpecificCompany(testCompany);
console.log(result); console.log(result);
await plugins.smartfs.directory(testOutputDir).create();
await Promise.all(result.files.map(async (file) => { await Promise.all(result.files.map(async (file) => {
await file.writeToDir(testOutputDir); await file.writeToDiskAtPath(
plugins.path.join(testOutputDir, plugins.path.basename(file.path))
);
})); }));
}); });
tap.test('should stop the instance', async (toolsArg) => { tap.test('should stop the instance', async (toolsArg) => {
toolsArg.skipIf(!handelsregisterStarted, handelsregisterSkipReason);
await testOpenDataInstance.stop(); await testOpenDataInstance.stop();
}); });
export default tap.start()
tap.start()
+129
View File
@@ -0,0 +1,129 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as opendata from '../ts/index.js';
import * as paths from '../ts/paths.js';
import * as plugins from '../ts/plugins.js';
const testDbFolder = plugins.path.join(paths.packageDir, '.nogit', 'law-smartdb-test');
let lawService: opendata.LawService;
tap.test('LawService - setup local smartdb', async () => {
await plugins.smartfs.directory(testDbFolder).recursive().delete().catch(() => {});
lawService = new opendata.LawService({
dbFolderPath: testDbFolder,
dbName: 'lawstest',
});
await lawService.start();
expect(lawService).toBeInstanceOf(opendata.LawService);
});
tap.test('LawService - sync and search Germany law', async () => {
const germanLaw = await lawService.syncLaw({
jurisdiction: 'de',
identifier: 'aeg',
});
expect(germanLaw.identifier).toEqual('aeg');
expect(germanLaw.title).toInclude('Eisenbahngesetz');
expect(germanLaw.text).toInclude('Ausgleichspflicht');
const results = await lawService.searchLaws({
jurisdiction: 'de',
query: 'Eisenbahngesetz',
limit: 5,
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].identifier).toEqual('aeg');
});
tap.test('LawService - sync and search EU law', async () => {
const euLaw = await lawService.syncLaw({
jurisdiction: 'eu',
identifier: '32024R1689',
language: 'EN',
});
expect(euLaw.identifier).toEqual('32024R1689');
expect(euLaw.title).toInclude('Artificial Intelligence Act');
expect(euLaw.text.toLowerCase()).toInclude('artificial intelligence');
const results = await lawService.searchLaws({
jurisdiction: 'eu',
query: 'Artificial Intelligence Act',
limit: 5,
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].identifier).toEqual('32024R1689');
});
tap.test('LawService - sync and search USA law', async () => {
const usLaw = await lawService.syncLaw({
jurisdiction: 'us',
identifier: 'PLAW-119publ1',
usCollection: 'PLAW',
});
expect(usLaw.identifier).toEqual('PLAW-119publ1');
expect(usLaw.shortTitle).toInclude('Laken Riley Act');
expect(usLaw.text).toInclude('To require the Secretary of Homeland Security');
const results = await lawService.searchLaws({
jurisdiction: 'us',
query: 'Laken Riley Act',
limit: 5,
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].identifier).toEqual('PLAW-119publ1');
});
tap.test('LawService - sync and search USA code citation', async () => {
const usCodeSection = await lawService.syncLaw({
jurisdiction: 'us',
identifier: '8 U.S.C. § 1226',
});
expect(usCodeSection.identifier).toEqual('8 USC 1226');
expect(usCodeSection.citation).toEqual('8 USC 1226');
expect(usCodeSection.title).toInclude('Apprehension and detention of aliens');
expect(usCodeSection.text).toInclude('Detention of criminal aliens');
const results = await lawService.searchLaws({
jurisdiction: 'us',
query: 'Apprehension and detention of aliens',
limit: 10,
});
expect(results.length).toBeGreaterThan(0);
expect(results.map((lawArg) => lawArg.identifier).includes('8 USC 1226')).toEqual(true);
});
tap.test('LawService - local lookup returns synced law', async () => {
const euLaw = await lawService.getLaw({
jurisdiction: 'eu',
identifier: '32024R1689',
language: 'EN',
});
expect(euLaw).toBeDefined();
expect(euLaw?.title).toInclude('Artificial Intelligence Act');
const usCodeLaw = await lawService.getLaw({
jurisdiction: 'us',
identifier: '8 USC 1226(c)',
});
expect(usCodeLaw).toBeDefined();
expect(usCodeLaw?.identifier).toEqual('8 USC 1226');
});
tap.test('LawService - teardown local smartdb', async () => {
await lawService.stop();
await plugins.smartfs.directory(testDbFolder).recursive().delete().catch(() => {});
});
export default tap.start();
@@ -13,6 +13,8 @@ const invalidTicker = 'INVALID_TICKER_XYZ';
let stockService: opendata.StockPriceService; let stockService: opendata.StockPriceService;
let marketstackProvider: opendata.MarketstackProvider; let marketstackProvider: opendata.MarketstackProvider;
let testQenv: plugins.qenv.Qenv; let testQenv: plugins.qenv.Qenv;
let marketstackAvailable = false;
let marketstackSkipReason = 'Marketstack integration requirements are unavailable.';
tap.test('should create StockPriceService instance', async () => { tap.test('should create StockPriceService instance', async () => {
stockService = new opendata.StockPriceService({ stockService = new opendata.StockPriceService({
@@ -27,6 +29,10 @@ tap.test('should create MarketstackProvider instance', async () => {
// Create qenv and get API key // Create qenv and get API key
testQenv = new plugins.qenv.Qenv(paths.packageDir, testNogitDir); testQenv = new plugins.qenv.Qenv(paths.packageDir, testNogitDir);
const apiKey = await testQenv.getEnvVarOnDemand('MARKETSTACK_COM_TOKEN'); const apiKey = await testQenv.getEnvVarOnDemand('MARKETSTACK_COM_TOKEN');
if (!apiKey) {
marketstackSkipReason = 'Skipping Marketstack integration tests: MARKETSTACK_COM_TOKEN not set.';
return;
}
marketstackProvider = new opendata.MarketstackProvider(apiKey, { marketstackProvider = new opendata.MarketstackProvider(apiKey, {
enabled: true, enabled: true,
@@ -37,13 +43,17 @@ tap.test('should create MarketstackProvider instance', async () => {
expect(marketstackProvider).toBeInstanceOf(opendata.MarketstackProvider); expect(marketstackProvider).toBeInstanceOf(opendata.MarketstackProvider);
expect(marketstackProvider.name).toEqual('Marketstack'); expect(marketstackProvider.name).toEqual('Marketstack');
expect(marketstackProvider.requiresAuth).toEqual(true); expect(marketstackProvider.requiresAuth).toEqual(true);
expect(marketstackProvider.priority).toEqual(80); expect(marketstackProvider.priority).toEqual(90);
marketstackAvailable = await marketstackProvider.isAvailable();
if (!marketstackAvailable) {
marketstackSkipReason = 'Skipping Marketstack integration tests: provider is not reachable.';
marketstackProvider = undefined as any;
}
} catch (error) { } catch (error) {
if (error.message.includes('MARKETSTACK_COM_TOKEN')) { if (plugins.getErrorMessage(error).includes('MARKETSTACK_COM_TOKEN')) {
console.log('⚠️ MARKETSTACK_COM_TOKEN not set - skipping Marketstack tests'); console.log('⚠️ MARKETSTACK_COM_TOKEN not set - skipping Marketstack tests');
tap.test('Marketstack token not available', async () => { marketstackSkipReason = 'Skipping Marketstack integration tests: MARKETSTACK_COM_TOKEN not set.';
expect(true).toEqual(true); // Skip gracefully
});
return; return;
} }
throw error; throw error;
@@ -64,7 +74,7 @@ tap.test('should register Marketstack provider with the service', async () => {
tap.test('should check provider health', async () => { tap.test('should check provider health', async () => {
if (!marketstackProvider) { if (!marketstackProvider) {
console.log('⚠️ Skipping - Marketstack provider not initialized'); console.log(`⚠️ ${marketstackSkipReason}`);
return; return;
} }
@@ -151,7 +161,7 @@ tap.test('should handle invalid ticker gracefully', async () => {
await stockService.getPrice({ ticker: invalidTicker }); await stockService.getPrice({ ticker: invalidTicker });
throw new Error('Should have thrown an error for invalid ticker'); throw new Error('Should have thrown an error for invalid ticker');
} catch (error) { } catch (error) {
expect(error.message).toInclude('Failed to fetch'); expect(plugins.getErrorMessage(error)).toInclude('Failed to fetch');
console.log('✓ Invalid ticker handled correctly'); console.log('✓ Invalid ticker handled correctly');
} }
}); });
@@ -196,6 +206,9 @@ tap.test('should get provider statistics', async () => {
const marketstackStats = stats.get('Marketstack'); const marketstackStats = stats.get('Marketstack');
expect(marketstackStats).not.toEqual(undefined); expect(marketstackStats).not.toEqual(undefined);
if (!marketstackStats) {
throw new Error('Missing Marketstack stats');
}
expect(marketstackStats.successCount).toBeGreaterThan(0); expect(marketstackStats.successCount).toBeGreaterThan(0);
expect(marketstackStats.errorCount).toBeGreaterThanOrEqual(0); expect(marketstackStats.errorCount).toBeGreaterThanOrEqual(0);
@@ -280,7 +293,7 @@ tap.test('should fetch sample EOD data', async () => {
console.log(`Provider: Marketstack (EOD Data)`); console.log(`Provider: Marketstack (EOD Data)`);
console.log(`Last updated: ${new Date().toLocaleString()}\n`); console.log(`Last updated: ${new Date().toLocaleString()}\n`);
} catch (error) { } catch (error) {
console.log('Error fetching sample data:', error.message); console.log('Error fetching sample data:', plugins.getErrorMessage(error));
} }
expect(true).toEqual(true); expect(true).toEqual(true);
+34 -5
View File
@@ -11,6 +11,8 @@ const testDownloadDir = plugins.path.join(testNogitDir, 'downloads');
const testGermanBusinessDataDir = plugins.path.join(testNogitDir, 'germanbusinessdata'); const testGermanBusinessDataDir = plugins.path.join(testNogitDir, 'germanbusinessdata');
let testOpenDataInstance: opendata.OpenData; let testOpenDataInstance: opendata.OpenData;
let openDataStarted = false;
let openDataSkipReason = 'OpenData integration requirements are unavailable.';
tap.test('first test', async () => { tap.test('first test', async () => {
testOpenDataInstance = new opendata.OpenData({ testOpenDataInstance = new opendata.OpenData({
@@ -22,16 +24,43 @@ tap.test('first test', async () => {
}); });
tap.test('should start the instance', async () => { tap.test('should start the instance', async () => {
try {
await testOpenDataInstance.start(); await testOpenDataInstance.start();
openDataStarted = true;
} catch (error) {
openDataSkipReason = `Skipping OpenData integration tests: ${plugins.getErrorMessage(error)}`;
console.warn(openDataSkipReason);
}
}) })
tap.test('should build initial data', async () => { tap.test('should persist business records using local smartdb', async (toolsArg) => {
await testOpenDataInstance.buildInitialDb(); toolsArg.skipIf(!openDataStarted, openDataSkipReason);
const businessRecord = new testOpenDataInstance.CBusinessRecord();
businessRecord.id = await testOpenDataInstance.CBusinessRecord.getNewId();
businessRecord.data.name = `Test Company ${plugins.smartunique.uniSimple()}`;
businessRecord.data.germanParsedRegistration = {
court: 'Bremen',
type: 'HRB',
number: `${Date.now()}`,
};
await businessRecord.save();
const storedRecord = await BusinessRecord.getInstance({
id: businessRecord.id,
}); });
tap.test('should stop the instance', async () => { expect(storedRecord.id).toEqual(businessRecord.id);
expect(storedRecord.data.name).toEqual(businessRecord.data.name);
expect(storedRecord.data.germanParsedRegistration).toEqual(
businessRecord.data.germanParsedRegistration
);
});
tap.test('should stop the instance', async (toolsArg) => {
toolsArg.skipIf(!openDataStarted, openDataSkipReason);
await testOpenDataInstance.stop(); await testOpenDataInstance.stop();
}); });
export default tap.start()
tap.start()
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@fin.cx/opendata', name: '@fin.cx/opendata',
version: '3.5.0', version: '3.6.0',
description: 'A comprehensive TypeScript library for accessing business data and real-time financial information. Features include German company data management with MongoDB integration, JSONL bulk processing, automated Handelsregister interactions, and real-time stock market data from multiple providers.' description: 'A comprehensive TypeScript library for accessing business data and real-time financial information. Features include German company data management with MongoDB integration, JSONL bulk processing, automated Handelsregister interactions, and real-time stock market data from multiple providers.'
} }
+1 -1
View File
@@ -18,7 +18,7 @@ export class BusinessRecord extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE // INSTANCE
@plugins.smartdata.unI() @plugins.smartdata.unI()
id: string; id!: string;
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
data: { data: {
+44 -19
View File
@@ -20,15 +20,44 @@ export class HandelsRegister {
this.uniqueDowloadFolder = plugins.path.join(this.downloadDir, plugins.smartunique.uniSimple()); this.uniqueDowloadFolder = plugins.path.join(this.downloadDir, plugins.smartunique.uniSimple());
} }
private async resetDownloadFolder() {
await plugins.smartfs.directory(this.uniqueDowloadFolder).recursive().delete().catch(() => {});
await plugins.smartfs.directory(this.uniqueDowloadFolder).create();
}
private async waitForDownloadedFile() {
for (let attempt = 0; attempt < 120; attempt++) {
const directoryEntries = await plugins.fs.readdir(this.uniqueDowloadFolder);
const fileName = directoryEntries.find(
(entry) => !entry.endsWith('.crdownload') && !entry.endsWith('.tmp')
);
if (fileName) {
const filePath = plugins.path.join(this.uniqueDowloadFolder, fileName);
const firstStat = await plugins.fs.stat(filePath);
await plugins.smartdelay.delayFor(500);
const secondStat = await plugins.fs.stat(filePath);
if (firstStat.size === secondStat.size) {
return filePath;
}
}
await plugins.smartdelay.delayFor(500);
}
throw new Error('Timed out while waiting for the download to finish.');
}
public async start() { public async start() {
// Start the browser // Start the browser
await plugins.smartfile.fs.ensureDir(this.uniqueDowloadFolder); await this.resetDownloadFolder();
await this.smartbrowserInstance.start(); await this.smartbrowserInstance.start();
} }
public async stop() { public async stop() {
// Stop the browser // Stop the browser
await plugins.smartfile.fs.remove(this.uniqueDowloadFolder); await plugins.smartfs.directory(this.uniqueDowloadFolder).recursive().delete();
await this.smartbrowserInstance.stop(); await this.smartbrowserInstance.stop();
} }
@@ -184,24 +213,16 @@ export class HandelsRegister {
throw new Error('Invalid file type'); throw new Error('Invalid file type');
} }
}, typeArg); }, typeArg);
const downloadedFilePath = await this.waitForDownloadedFile();
const renamedFilePath = plugins.path.join(
this.uniqueDowloadFolder,
typeArg === 'AD' ? 'ad.pdf' : 'si.xml'
);
await plugins.fs.rename(downloadedFilePath, renamedFilePath);
const file = await plugins.smartfileFactory.fromFilePath(renamedFilePath);
// Keep the download folder empty between requests.
await plugins.smartfile.fs.waitForFileToBeReady(this.uniqueDowloadFolder); await this.resetDownloadFolder();
const files = await plugins.smartfile.fs.fileTreeToObject(this.uniqueDowloadFolder, '**/*');
const file = files[0];
// lets clear the folder for the next download
await plugins.smartfile.fs.ensureEmptyDir(this.uniqueDowloadFolder);
switch (typeArg) {
case 'AD':
await file.rename(`ad.pdf`);
break;
case 'SI':
await file.rename(`si.xml`);
break;
break;
}
return file; return file;
} }
@@ -297,6 +318,10 @@ export class HandelsRegister {
*/ */
public async getSpecificCompany(companyArg: BusinessRecord['data']['germanParsedRegistration']) { public async getSpecificCompany(companyArg: BusinessRecord['data']['germanParsedRegistration']) {
return this.asyncExecutionStack.getExclusiveExecutionSlot(async () => { return this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
if (!companyArg?.type || !companyArg.number || !companyArg.court) {
throw new Error('A complete parsed German registration is required.');
}
const page = await this.getNewPage(); const page = await this.getNewPage();
await this.navigateToPage(page, 'Normal search'); await this.navigateToPage(page, 'Normal search');
await page.waitForSelector('#form\\:schlagwoerter', { timeout: 5000 }); await page.waitForSelector('#form\\:schlagwoerter', { timeout: 5000 });
+7 -6
View File
@@ -53,14 +53,15 @@ export class JsonlDataProcessor<T> {
dataUrlArg = 'https://daten.offeneregister.de/de_companies_ocdata.jsonl.bz2' dataUrlArg = 'https://daten.offeneregister.de/de_companies_ocdata.jsonl.bz2'
) { ) {
const done = plugins.smartpromise.defer(); const done = plugins.smartpromise.defer();
const dataExists = await plugins.smartfile.fs.isDirectory(this.germanBusinessDataDir); const dataExists = await plugins.smartfs.directory(this.germanBusinessDataDir).exists();
if (!dataExists) { if (!dataExists) {
await plugins.smartfile.fs.ensureDir(this.germanBusinessDataDir); await plugins.smartfs.directory(this.germanBusinessDataDir).create();
} else { } else {
} }
const smartarchive = await plugins.smartarchive.SmartArchive.fromArchiveUrl(dataUrlArg); const jsonlDataStream = await plugins.smartarchive.SmartArchive.create()
const jsonlDataStream = await smartarchive.exportToStreamOfStreamFiles(); .url(dataUrlArg)
.toStreamFiles();
let totalRecordsCounter = 0; let totalRecordsCounter = 0;
let nextRest: string = ''; let nextRest: string = '';
jsonlDataStream.pipe( jsonlDataStream.pipe(
@@ -74,11 +75,11 @@ export class JsonlDataProcessor<T> {
writeFunction: async (chunkArg: Buffer, streamToolsArg) => { writeFunction: async (chunkArg: Buffer, streamToolsArg) => {
const currentString = nextRest + chunkArg.toString(); const currentString = nextRest + chunkArg.toString();
const lines = currentString.split('\n'); const lines = currentString.split('\n');
nextRest = lines.pop(); nextRest = lines.pop() ?? '';
console.log(`Got another ${lines.length} records.`); console.log(`Got another ${lines.length} records.`);
const concurrentProcessor = new plugins.smartarray.ConcurrentProcessor<string>( const concurrentProcessor = new plugins.smartarray.ConcurrentProcessor<string>(
async (line) => { async (line) => {
let entry: T; let entry: T | undefined;
if (!line) return; if (!line) return;
try { try {
entry = JSON.parse(line); entry = JSON.parse(line);
+49 -16
View File
@@ -1,7 +1,6 @@
import { BusinessRecord } from './classes.businessrecord.js'; import { BusinessRecord } from './classes.businessrecord.js';
import { HandelsRegister } from './classes.handelsregister.js'; import { HandelsRegister } from './classes.handelsregister.js';
import { JsonlDataProcessor, type SeedEntryType } from './classes.jsonldata.js'; import { JsonlDataProcessor, type SeedEntryType } from './classes.jsonldata.js';
import * as paths from './paths.js';
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
export interface IOpenDataConfig { export interface IOpenDataConfig {
@@ -11,12 +10,13 @@ export interface IOpenDataConfig {
} }
export class OpenData { export class OpenData {
public db: plugins.smartdata.SmartdataDb; public db!: plugins.smartdata.SmartdataDb;
private serviceQenv: plugins.qenv.Qenv; private localSmartDb?: plugins.smartdb.LocalSmartDb;
private config: IOpenDataConfig; private config: IOpenDataConfig;
private started = false;
public jsonLDataProcessor: JsonlDataProcessor<SeedEntryType>; public jsonLDataProcessor!: JsonlDataProcessor<SeedEntryType>;
public handelsregister: HandelsRegister; public handelsregister!: HandelsRegister;
public CBusinessRecord = plugins.smartdata.setDefaultManagerForDoc(this, BusinessRecord); public CBusinessRecord = plugins.smartdata.setDefaultManagerForDoc(this, BusinessRecord);
@@ -28,22 +28,35 @@ export class OpenData {
throw new Error('@fin.cx/opendata: All directory paths are required (downloadDir, germanBusinessDataDir, nogitDir).'); throw new Error('@fin.cx/opendata: All directory paths are required (downloadDir, germanBusinessDataDir, nogitDir).');
} }
this.config = configArg; this.config = configArg;
this.serviceQenv = new plugins.qenv.Qenv(paths.packageDir, this.config.nogitDir);
} }
public async start() { public async start() {
// Ensure configured directories exist if (this.started) {
await plugins.smartfile.fs.ensureDir(this.config.nogitDir); return;
await plugins.smartfile.fs.ensureDir(this.config.downloadDir); }
await plugins.smartfile.fs.ensureDir(this.config.germanBusinessDataDir);
this.db = new plugins.smartdata.SmartdataDb({ // Ensure configured directories exist
mongoDbUrl: await this.serviceQenv.getEnvVarOnDemand('MONGODB_URL'), await plugins.smartfs.directory(this.config.nogitDir).create();
mongoDbName: await this.serviceQenv.getEnvVarOnDemand('MONGODB_NAME'), await plugins.smartfs.directory(this.config.downloadDir).create();
mongoDbUser: await this.serviceQenv.getEnvVarOnDemand('MONGODB_USER'), await plugins.smartfs.directory(this.config.germanBusinessDataDir).create();
mongoDbPass: await this.serviceQenv.getEnvVarOnDemand('MONGODB_PASS'),
this.localSmartDb = new plugins.smartdb.LocalSmartDb({
folderPath: plugins.path.join(this.config.nogitDir, 'opendata-smartdb'),
}); });
const connectionInfo = await this.localSmartDb.start();
this.db = new plugins.smartdata.SmartdataDb({
mongoDbUrl: connectionInfo.connectionUri,
mongoDbName: 'opendata',
});
try {
await this.db.init(); await this.db.init();
await this.db.mongoDb.collection('_opendata_bootstrap').insertOne({
createdAt: new Date(),
});
await this.db.mongoDb.collection('_opendata_bootstrap').deleteMany({});
this.jsonLDataProcessor = new JsonlDataProcessor( this.jsonLDataProcessor = new JsonlDataProcessor(
this.config.germanBusinessDataDir, this.config.germanBusinessDataDir,
async (entryArg) => { async (entryArg) => {
@@ -58,8 +71,19 @@ export class OpenData {
await businessRecord.save(); await businessRecord.save();
} }
); );
this.handelsregister = new HandelsRegister(this, this.config.downloadDir); this.handelsregister = new HandelsRegister(this, this.config.downloadDir);
await this.handelsregister.start(); await this.handelsregister.start();
this.started = true;
} catch (error) {
if (this.handelsregister) {
await this.handelsregister.stop().catch(() => {});
}
await this.db.close().catch(() => {});
await this.localSmartDb.stop().catch(() => {});
this.localSmartDb = undefined;
throw error;
}
} }
public async buildInitialDb() { public async buildInitialDb() {
@@ -81,7 +105,16 @@ export class OpenData {
public async stop() { public async stop() {
await this.db.close(); if (!this.started) {
return;
}
if (this.handelsregister) {
await this.handelsregister.stop(); await this.handelsregister.stop();
} }
await this.db.close();
await this.localSmartDb?.stop();
this.localSmartDb = undefined;
this.started = false;
}
} }
+1
View File
@@ -1,2 +1,3 @@
export * from './classes.main.opendata.js'; export * from './classes.main.opendata.js';
export * from './laws/index.js';
export * from './stocks/index.js'; export * from './stocks/index.js';
+81
View File
@@ -0,0 +1,81 @@
import * as plugins from '../plugins.js';
import type { TJurisdiction, TLawSource, TRawLawFormat } from './interfaces.law.js';
@plugins.smartdata.Manager()
export class LawRecord extends plugins.smartdata.SmartDataDbDoc<LawRecord, LawRecord> {
public static getByLookupKey = async (lookupKeyArg: string) => {
const lawRecords = await LawRecord.getInstances({
lookupKey: lookupKeyArg,
});
return lawRecords[0];
};
@plugins.smartdata.unI()
id!: string;
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
lookupKey!: string;
@plugins.smartdata.index()
@plugins.smartdata.svDb()
jurisdiction!: TJurisdiction;
@plugins.smartdata.index()
@plugins.smartdata.svDb()
source!: TLawSource;
@plugins.smartdata.searchable()
@plugins.smartdata.svDb()
identifier!: string;
@plugins.smartdata.searchable()
@plugins.smartdata.svDb()
title!: string;
@plugins.smartdata.searchable()
@plugins.smartdata.svDb()
shortTitle: string = '';
@plugins.smartdata.searchable()
@plugins.smartdata.svDb()
citation: string = '';
@plugins.smartdata.index()
@plugins.smartdata.svDb()
type: string = '';
@plugins.smartdata.index()
@plugins.smartdata.svDb()
language: string = '';
@plugins.smartdata.svDb()
sourceUrl!: string;
@plugins.smartdata.svDb()
rawFormat!: TRawLawFormat;
@plugins.smartdata.svDb()
rawBody!: string;
@plugins.smartdata.searchable()
@plugins.smartdata.svDb()
text!: string;
@plugins.smartdata.index()
@plugins.smartdata.svDb()
dateIssued: string = '';
@plugins.smartdata.index()
@plugins.smartdata.svDb()
lastModified: string = '';
@plugins.smartdata.svDb()
sourceMeta: Record<string, string> = {};
@plugins.smartdata.svDb()
fetchedAt: Date = new Date();
@plugins.smartdata.svDb()
syncedAt: Date = new Date();
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
export * from './interfaces.law.js';
export * from './classes.lawrecord.js';
export * from './classes.lawservice.js';
+48
View File
@@ -0,0 +1,48 @@
export type TJurisdiction = 'de' | 'eu' | 'us';
export type TLawSource =
| 'gesetze-im-internet'
| 'eur-lex'
| 'law-cornell-lii'
| 'govinfo-plaw'
| 'govinfo-uscode';
export type TRawLawFormat = 'xml' | 'html' | 'text' | 'json';
export type TUsLawCollection = 'PLAW' | 'USCODE';
export interface ILawServiceConfig {
dbFolderPath?: string;
dbName?: string;
govInfoApiKey?: string;
}
export interface ILawLookupRequest {
jurisdiction: TJurisdiction;
identifier: string;
language?: string;
usCollection?: TUsLawCollection;
forceSync?: boolean;
}
export interface ILawSyncRequest {
jurisdiction: TJurisdiction;
limit?: number;
offset?: number;
language?: string;
govInfoApiKey?: string;
usCollection?: TUsLawCollection;
since?: Date;
}
export interface ILawSearchRequest {
query: string;
jurisdiction?: TJurisdiction;
limit?: number;
}
export interface ILawSyncResult {
jurisdiction: TJurisdiction;
syncedCount: number;
identifiers: string[];
}
+19
View File
@@ -1,7 +1,9 @@
// node native scope // node native scope
import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
export { export {
fs,
path, path,
} }
@@ -14,23 +16,40 @@ import * as smartbrowser from '@push.rocks/smartbrowser';
import * as smartdata from '@push.rocks/smartdata'; import * as smartdata from '@push.rocks/smartdata';
import * as smartdelay from '@push.rocks/smartdelay'; import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
import * as smartlog from '@push.rocks/smartlog'; import * as smartlog from '@push.rocks/smartlog';
import * as smartpath from '@push.rocks/smartpath'; import * as smartpath from '@push.rocks/smartpath';
import * as smartpromise from '@push.rocks/smartpromise'; import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest'; import * as smartrequest from '@push.rocks/smartrequest';
import * as smartdb from '@push.rocks/smartdb';
import * as smartstream from '@push.rocks/smartstream'; import * as smartstream from '@push.rocks/smartstream';
import * as smartunique from '@push.rocks/smartunique'; import * as smartunique from '@push.rocks/smartunique';
import * as smartxml from '@push.rocks/smartxml'; import * as smartxml from '@push.rocks/smartxml';
const smartfs = new SmartFs(new SmartFsProviderNode());
const smartfileFactory = new smartfile.SmartFileFactory(smartfs);
const getErrorMessage = (error: unknown) => {
if (error instanceof Error) {
return error.message;
}
return String(error);
};
export { export {
getErrorMessage,
lik, lik,
qenv, qenv,
smartarchive, smartarchive,
smartarray, smartarray,
smartbrowser, smartbrowser,
smartdb,
smartdata, smartdata,
smartdelay, smartdelay,
smartfile, smartfile,
smartfileFactory,
smartfs,
smartlog, smartlog,
smartpath, smartpath,
smartpromise, smartpromise,
+1 -1
View File
@@ -166,7 +166,7 @@ export class FundamentalsService implements IFundamentalsProviderRegistry {
lastError = error as Error; lastError = error as Error;
console.warn( console.warn(
`Provider ${provider.name} failed for ${this.getRequestDescription(request)}: ${error.message}` `Provider ${provider.name} failed for ${this.getRequestDescription(request)}: ${plugins.getErrorMessage(error)}`
); );
} }
} }
+17 -7
View File
@@ -32,7 +32,17 @@ export class StockDataService {
private logger = console; private logger = console;
private config: Required<IStockDataServiceConfig> = { private config: {
cache: {
priceTTL: number;
fundamentalsTTL: number;
maxEntries: number;
};
timeout: {
price: number;
fundamentals: number;
};
} = {
cache: { cache: {
priceTTL: 24 * 60 * 60 * 1000, // 24 hours priceTTL: 24 * 60 * 60 * 1000, // 24 hours
fundamentalsTTL: 90 * 24 * 60 * 60 * 1000, // 90 days fundamentalsTTL: 90 * 24 * 60 * 60 * 1000, // 90 days
@@ -200,7 +210,7 @@ export class StockDataService {
entry.lastErrorTime = new Date(); entry.lastErrorTime = new Date();
lastError = error as Error; lastError = error as Error;
console.warn(`Provider ${provider.name} failed for ${ticker}: ${error.message}`); console.warn(`Provider ${provider.name} failed for ${ticker}: ${plugins.getErrorMessage(error)}`);
} }
} }
@@ -250,7 +260,7 @@ export class StockDataService {
entry.lastErrorTime = new Date(); entry.lastErrorTime = new Date();
lastError = error as Error; lastError = error as Error;
console.warn(`Provider ${provider.name} failed for batch prices: ${error.message}`); console.warn(`Provider ${provider.name} failed for batch prices: ${plugins.getErrorMessage(error)}`);
} }
} }
@@ -301,7 +311,7 @@ export class StockDataService {
entry.lastErrorTime = new Date(); entry.lastErrorTime = new Date();
lastError = error as Error; lastError = error as Error;
console.warn(`Provider ${provider.name} failed for ${ticker} fundamentals: ${error.message}`); console.warn(`Provider ${provider.name} failed for ${ticker} fundamentals: ${plugins.getErrorMessage(error)}`);
} }
} }
@@ -352,7 +362,7 @@ export class StockDataService {
entry.lastErrorTime = new Date(); entry.lastErrorTime = new Date();
lastError = error as Error; lastError = error as Error;
console.warn(`Provider ${provider.name} failed for batch fundamentals: ${error.message}`); console.warn(`Provider ${provider.name} failed for batch fundamentals: ${plugins.getErrorMessage(error)}`);
} }
} }
@@ -382,7 +392,7 @@ export class StockDataService {
fundamentals = this.enrichWithPrice(fundamentals, price.price); fundamentals = this.enrichWithPrice(fundamentals, price.price);
} }
} catch (error) { } catch (error) {
console.warn(`Failed to fetch fundamentals for ${normalizedRequest.ticker}: ${error.message}`); console.warn(`Failed to fetch fundamentals for ${normalizedRequest.ticker}: ${plugins.getErrorMessage(error)}`);
// Continue without fundamentals // Continue without fundamentals
} }
} }
@@ -426,7 +436,7 @@ export class StockDataService {
fundamentalsMap = new Map(fundamentals.map(f => [f.ticker, f])); fundamentalsMap = new Map(fundamentals.map(f => [f.ticker, f]));
} }
} catch (error) { } catch (error) {
console.warn(`Failed to fetch batch fundamentals: ${error.message}`); console.warn(`Failed to fetch batch fundamentals: ${plugins.getErrorMessage(error)}`);
// Continue without fundamentals // Continue without fundamentals
} }
} }
+1 -1
View File
@@ -205,7 +205,7 @@ export class StockPriceService implements IProviderRegistry {
lastError = error as Error; lastError = error as Error;
console.warn( console.warn(
`Provider ${provider.name} failed for ${this.getRequestDescription(request)}: ${error.message}` `Provider ${provider.name} failed for ${this.getRequestDescription(request)}: ${plugins.getErrorMessage(error)}`
); );
} }
} }
+9 -7
View File
@@ -45,7 +45,7 @@ export class MarketstackProvider implements IStockProvider {
public priority = 90; // Increased from 80 - now supports real-time intraday data during market hours public priority = 90; // Increased from 80 - now supports real-time intraday data during market hours
public readonly requiresAuth = true; public readonly requiresAuth = true;
public readonly rateLimit = { public readonly rateLimit = {
requestsPerMinute: undefined, // No per-minute limit specified requestsPerMinute: 0, // No per-minute limit specified
requestsPerDay: undefined // Varies by plan requestsPerDay: undefined // Varies by plan
}; };
@@ -99,8 +99,9 @@ export class MarketstackProvider implements IStockProvider {
} }
} catch (error) { } catch (error) {
// If intraday fails, fallback to EOD with warning // If intraday fails, fallback to EOD with warning
if (error.message?.includes('intraday') || error.message?.includes('Marketstack API error')) { const errorMessage = plugins.getErrorMessage(error);
this.logger.warn(`Intraday endpoint failed for ${request.ticker}, falling back to EOD:`, error.message); if (errorMessage.includes('intraday') || errorMessage.includes('Marketstack API error')) {
this.logger.warn(`Intraday endpoint failed for ${request.ticker}, falling back to EOD:`, errorMessage);
try { try {
return await this.fetchCurrentPriceEod(request); return await this.fetchCurrentPriceEod(request);
} catch (eodError) { } catch (eodError) {
@@ -245,7 +246,7 @@ export class MarketstackProvider implements IStockProvider {
return allPrices; return allPrices;
} catch (error) { } catch (error) {
this.logger.error(`Failed to fetch historical prices for ${request.ticker}:`, error); this.logger.error(`Failed to fetch historical prices for ${request.ticker}:`, error);
throw new Error(`Marketstack: Failed to fetch historical prices for ${request.ticker}: ${error.message}`); throw new Error(`Marketstack: Failed to fetch historical prices for ${request.ticker}: ${plugins.getErrorMessage(error)}`);
} }
} }
@@ -337,7 +338,7 @@ export class MarketstackProvider implements IStockProvider {
return allPrices; return allPrices;
} catch (error) { } catch (error) {
this.logger.error(`Failed to fetch intraday prices for ${request.ticker}:`, error); this.logger.error(`Failed to fetch intraday prices for ${request.ticker}:`, error);
throw new Error(`Marketstack: Failed to fetch intraday prices for ${request.ticker}: ${error.message}`); throw new Error(`Marketstack: Failed to fetch intraday prices for ${request.ticker}: ${plugins.getErrorMessage(error)}`);
} }
} }
@@ -359,8 +360,9 @@ export class MarketstackProvider implements IStockProvider {
} }
} catch (error) { } catch (error) {
// Fallback to EOD if intraday fails // Fallback to EOD if intraday fails
if (error.message?.includes('intraday') || error.message?.includes('Marketstack API error')) { const errorMessage = plugins.getErrorMessage(error);
this.logger.warn(`Intraday batch endpoint failed, falling back to EOD:`, error.message); if (errorMessage.includes('intraday') || errorMessage.includes('Marketstack API error')) {
this.logger.warn(`Intraday batch endpoint failed, falling back to EOD:`, errorMessage);
try { try {
return await this.fetchBatchCurrentPricesEod(request); return await this.fetchBatchCurrentPricesEod(request);
} catch (eodError) { } catch (eodError) {
+2 -2
View File
@@ -141,7 +141,7 @@ export class SecEdgarProvider implements IFundamentalsProvider {
return this.parseCompanyFacts(request.ticker, cik, companyFacts); return this.parseCompanyFacts(request.ticker, cik, companyFacts);
} catch (error) { } catch (error) {
this.logger.error(`Failed to fetch fundamentals for ${request.ticker}:`, error); this.logger.error(`Failed to fetch fundamentals for ${request.ticker}:`, error);
throw new Error(`SEC EDGAR: Failed to fetch fundamentals for ${request.ticker}: ${error.message}`); throw new Error(`SEC EDGAR: Failed to fetch fundamentals for ${request.ticker}: ${plugins.getErrorMessage(error)}`);
} }
} }
@@ -163,7 +163,7 @@ export class SecEdgarProvider implements IFundamentalsProvider {
results.push(fundamentals); results.push(fundamentals);
} catch (error) { } catch (error) {
this.logger.warn(`Failed to fetch fundamentals for ${ticker}:`, error); this.logger.warn(`Failed to fetch fundamentals for ${ticker}:`, error);
errors.push(`${ticker}: ${error.message}`); errors.push(`${ticker}: ${plugins.getErrorMessage(error)}`);
// Continue with other tickers // Continue with other tickers
} }
} }
+2 -2
View File
@@ -86,7 +86,7 @@ export class YahooFinanceProvider implements IStockProvider {
return stockPrice; return stockPrice;
} catch (error) { } catch (error) {
console.error(`Failed to fetch price for ${request.ticker}:`, error); console.error(`Failed to fetch price for ${request.ticker}:`, error);
throw new Error(`Yahoo Finance: Failed to fetch price for ${request.ticker}: ${error.message}`); throw new Error(`Yahoo Finance: Failed to fetch price for ${request.ticker}: ${plugins.getErrorMessage(error)}`);
} }
} }
@@ -145,7 +145,7 @@ export class YahooFinanceProvider implements IStockProvider {
return prices; return prices;
} catch (error) { } catch (error) {
console.error(`Failed to fetch batch prices:`, error); console.error(`Failed to fetch batch prices:`, error);
throw new Error(`Yahoo Finance: Failed to fetch batch prices: ${error.message}`); throw new Error(`Yahoo Finance: Failed to fetch batch prices: ${plugins.getErrorMessage(error)}`);
} }
} }
-2
View File
@@ -1,7 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",