Compare commits
10 Commits
b71946d3be
...
be3763d988
Author | SHA1 | Date | |
---|---|---|---|
![]() |
be3763d988 | ||
![]() |
dd920d26ce | ||
![]() |
44d19ad628 | ||
![]() |
d39b3c7003 | ||
![]() |
e00280aea9 | ||
![]() |
9e937231c8 | ||
![]() |
2553483ef9 | ||
![]() |
a667f98f25 | ||
![]() |
ef4be50b5a | ||
![]() |
a4cbda0fb9 |
5
.github/workflows/publish.yml
vendored
5
.github/workflows/publish.yml
vendored
@ -26,11 +26,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Publish to npm as alpha
|
|
||||||
run: npm publish --tag alpha --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish to npm as latest
|
- name: Publish to npm as latest
|
||||||
run: npm publish --tag latest --access public
|
run: npm publish --tag latest --access public
|
||||||
env:
|
env:
|
||||||
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
10
.lintstagedrc
Normal file
10
.lintstagedrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"*.{js,ts}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"*.{json,md}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
}
|
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
347
README.md
347
README.md
@ -1,14 +1,21 @@
|
|||||||
# @debros/netowrk
|
# @debros/network
|
||||||
|
|
||||||
Core networking functionality for the Debros decentralized network. This package provides essential IPFS, libp2p, and OrbitDB functionality to build decentralized applications on the Debros network.
|
Core networking functionality for the Debros decentralized network. This package provides a powerful database interface with advanced features built on IPFS and OrbitDB for decentralized applications.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Pre-configured IPFS/libp2p node setup
|
- Rich database-like API with TypeScript support
|
||||||
- Service discovery for peer-to-peer communication
|
- Multiple database store types (KeyValue, Document, Feed, Counter)
|
||||||
- OrbitDB integration for distributed databases
|
- Document operations with schema validation
|
||||||
- Consistent logging across network components
|
- Advanced querying with pagination, sorting and filtering
|
||||||
- Secure key generation
|
- Transaction support for batch operations
|
||||||
|
- Built-in file storage with metadata
|
||||||
|
- Real-time subscriptions for data changes
|
||||||
|
- Memory caching for performance
|
||||||
|
- Connection pooling for managing multiple database instances
|
||||||
|
- Index creation for faster queries
|
||||||
|
- Comprehensive error handling with error codes
|
||||||
|
- Performance metrics and monitoring
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -19,80 +26,296 @@ npm install @debros/network
|
|||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { initConfig, initIpfs, initOrbitDB, logger } from "@debros/network";
|
import { initDB, create, get, query, uploadFile, logger } from "@debros/network";
|
||||||
|
|
||||||
// Initialize with custom configuration (optional)
|
// Initialize the database service
|
||||||
const config = initConfig({
|
async function startApp() {
|
||||||
env: {
|
|
||||||
fingerprint: "my-unique-node-id",
|
|
||||||
port: 8080,
|
|
||||||
},
|
|
||||||
ipfs: {
|
|
||||||
bootstrapNodes: "node1,node2,node3",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the network node
|
|
||||||
async function startNode() {
|
|
||||||
try {
|
try {
|
||||||
// Initialize IPFS
|
// Initialize with default configuration
|
||||||
const ipfs = await initIpfs();
|
await initDB();
|
||||||
|
logger.info("Database initialized successfully");
|
||||||
// Initialize OrbitDB with the IPFS instance
|
|
||||||
const orbitdb = await initOrbitDB({ getHelia: () => ipfs });
|
// Create a new user document
|
||||||
|
const userId = 'user123';
|
||||||
// Create/open a database
|
const user = {
|
||||||
const db = await orbitDB("myDatabase", "feed");
|
username: 'johndoe',
|
||||||
|
walletAddress: '0x1234567890',
|
||||||
logger.info("Node started successfully");
|
avatar: null
|
||||||
|
};
|
||||||
return { ipfs, orbitdb, db };
|
|
||||||
|
const result = await create('users', userId, user);
|
||||||
|
logger.info(`Created user with ID: ${result.id}`);
|
||||||
|
|
||||||
|
// Get a user by ID
|
||||||
|
const retrievedUser = await get('users', userId);
|
||||||
|
logger.info('User:', retrievedUser);
|
||||||
|
|
||||||
|
// Query users with filtering
|
||||||
|
const activeUsers = await query('users',
|
||||||
|
user => user.isActive === true,
|
||||||
|
{ limit: 10, sort: { field: 'createdAt', order: 'desc' } }
|
||||||
|
);
|
||||||
|
logger.info(`Found ${activeUsers.total} active users`);
|
||||||
|
|
||||||
|
// Upload a file
|
||||||
|
const fileData = Buffer.from('File content');
|
||||||
|
const fileUpload = await uploadFile(fileData, { filename: 'document.txt' });
|
||||||
|
logger.info(`Uploaded file with CID: ${fileUpload.cid}`);
|
||||||
|
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to start node:", error);
|
logger.error("Failed to start app:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startNode();
|
startApp();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Database Store Types
|
||||||
|
|
||||||
The package provides sensible defaults but can be customized:
|
The library supports multiple OrbitDB store types, each optimized for different use cases:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { initConfig } from "@debros/network";
|
import { create, get, update, StoreType } from "@debros/network";
|
||||||
|
|
||||||
const customConfig = initConfig({
|
// Default KeyValue store (for general use)
|
||||||
env: {
|
await create('users', 'user1', { name: 'Alice' });
|
||||||
fingerprint: "unique-fingerprint",
|
|
||||||
port: 9000,
|
// Document store (better for complex documents with indexing)
|
||||||
},
|
await create('posts', 'post1', { title: 'Hello', content: '...' },
|
||||||
ipfs: {
|
{ storeType: StoreType.DOCSTORE }
|
||||||
blockstorePath: "./custom-blockstore",
|
);
|
||||||
serviceDiscovery: {
|
|
||||||
topic: "my-custom-topic",
|
// Feed/EventLog store (append-only, good for immutable logs)
|
||||||
heartbeatInterval: 10000,
|
await create('events', 'evt1', { type: 'login', user: 'alice' },
|
||||||
|
{ storeType: StoreType.FEED }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Counter store (for numeric counters)
|
||||||
|
await create('stats', 'visits', { value: 0 },
|
||||||
|
{ storeType: StoreType.COUNTER }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Increment a counter
|
||||||
|
await update('stats', 'visits', { increment: 1 },
|
||||||
|
{ storeType: StoreType.COUNTER }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get counter value
|
||||||
|
const stats = await get('stats', 'visits', { storeType: StoreType.COUNTER });
|
||||||
|
console.log(`Visit count: ${stats.value}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Schema Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineSchema, create } from "@debros/network";
|
||||||
|
|
||||||
|
// Define a schema
|
||||||
|
defineSchema('users', {
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
min: 3,
|
||||||
|
max: 20
|
||||||
},
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
pattern: '^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: 'number',
|
||||||
|
min: 18
|
||||||
|
}
|
||||||
},
|
},
|
||||||
orbitdb: {
|
required: ['username']
|
||||||
directory: "./custom-orbitdb",
|
});
|
||||||
},
|
|
||||||
|
// Document creation will be validated against the schema
|
||||||
|
await create('users', 'user1', {
|
||||||
|
username: 'alice',
|
||||||
|
email: 'alice@example.com',
|
||||||
|
age: 25
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
### Transactions
|
||||||
|
|
||||||
### Core Modules
|
```typescript
|
||||||
|
import { createTransaction, commitTransaction } from "@debros/network";
|
||||||
|
|
||||||
- **IPFS Service**: Setup and manage IPFS nodes
|
// Create a transaction
|
||||||
- **OrbitDB Service**: Distributed database management
|
const transaction = createTransaction();
|
||||||
- **Config**: Network configuration
|
|
||||||
- **Logger**: Consistent logging
|
|
||||||
|
|
||||||
### Main Exports
|
// Add multiple operations
|
||||||
|
transaction
|
||||||
|
.create('posts', 'post1', { title: 'Hello World', content: '...' })
|
||||||
|
.update('users', 'user1', { postCount: 1 })
|
||||||
|
.delete('drafts', 'draft1');
|
||||||
|
|
||||||
- `initConfig`: Configure the network node
|
// Commit all operations
|
||||||
- `ipfsService`: IPFS node management
|
const result = await commitTransaction(transaction);
|
||||||
- `orbitDBService`: OrbitDB operations
|
console.log(`Transaction completed with ${result.results.length} operations`);
|
||||||
- `logger`: Logging utilities
|
```
|
||||||
|
|
||||||
|
### Subscriptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { subscribe } from "@debros/network";
|
||||||
|
|
||||||
|
// Subscribe to document changes
|
||||||
|
const unsubscribe = subscribe('document:created', (data) => {
|
||||||
|
console.log(`New document created in ${data.collection}:`, data.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Later, unsubscribe
|
||||||
|
unsubscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination and Sorting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { list, query } from "@debros/network";
|
||||||
|
|
||||||
|
// List with pagination and sorting
|
||||||
|
const page1 = await list('users', {
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
sort: { field: 'createdAt', order: 'desc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query with pagination
|
||||||
|
const results = await query('users',
|
||||||
|
(user) => user.age > 21,
|
||||||
|
{ limit: 10, offset: 20 }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Found ${results.total} matches, showing ${results.documents.length}`);
|
||||||
|
console.log(`Has more pages: ${results.hasMore}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript Support
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { get, update, query } from "@debros/network";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-safe operations
|
||||||
|
const user = await get<User>('users', 'user1');
|
||||||
|
|
||||||
|
await update<User>('users', 'user1', { age: 26 });
|
||||||
|
|
||||||
|
const results = await query<User>('users',
|
||||||
|
(user) => user.age > 21
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { initDB, closeConnection } from "@debros/network";
|
||||||
|
|
||||||
|
// Create multiple connections
|
||||||
|
const conn1 = await initDB('connection1');
|
||||||
|
const conn2 = await initDB('connection2');
|
||||||
|
|
||||||
|
// Use specific connection
|
||||||
|
await create('users', 'user1', { name: 'Alice' }, { connectionId: conn1 });
|
||||||
|
|
||||||
|
// Close a specific connection
|
||||||
|
await closeConnection(conn1);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Metrics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getMetrics, resetMetrics } from "@debros/network";
|
||||||
|
|
||||||
|
// Get performance metrics
|
||||||
|
const metrics = getMetrics();
|
||||||
|
console.log('Operations:', metrics.operations);
|
||||||
|
console.log('Avg operation time:', metrics.performance.averageOperationTime, 'ms');
|
||||||
|
console.log('Cache hits/misses:', metrics.cacheStats);
|
||||||
|
|
||||||
|
// Reset metrics (e.g., after deployment)
|
||||||
|
resetMetrics();
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Core Database Operations
|
||||||
|
|
||||||
|
- `initDB(connectionId?: string): Promise<string>` - Initialize the database
|
||||||
|
- `create<T>(collection, id, data, options?): Promise<CreateResult>` - Create a document
|
||||||
|
- `get<T>(collection, id, options?): Promise<T | null>` - Get a document by ID
|
||||||
|
- `update<T>(collection, id, data, options?): Promise<UpdateResult>` - Update a document
|
||||||
|
- `remove(collection, id, options?): Promise<boolean>` - Delete a document
|
||||||
|
- `list<T>(collection, options?): Promise<PaginatedResult<T>>` - List documents with pagination
|
||||||
|
- `query<T>(collection, filter, options?): Promise<PaginatedResult<T>>` - Query documents
|
||||||
|
- `stopDB(): Promise<void>` - Stop the database service
|
||||||
|
|
||||||
|
### Store Types
|
||||||
|
|
||||||
|
- `StoreType.KEYVALUE` - Key-value pair storage (default)
|
||||||
|
- `StoreType.DOCSTORE` - Document storage with indexing
|
||||||
|
- `StoreType.FEED` - Append-only log
|
||||||
|
- `StoreType.EVENTLOG` - Alias for FEED
|
||||||
|
- `StoreType.COUNTER` - Numeric counter
|
||||||
|
|
||||||
|
### Schema Validation
|
||||||
|
|
||||||
|
- `defineSchema(collection, schema): void` - Define a schema for a collection
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
|
||||||
|
- `createTransaction(connectionId?): Transaction` - Create a new transaction
|
||||||
|
- `commitTransaction(transaction): Promise<{success, results}>` - Execute the transaction
|
||||||
|
- `Transaction.create<T>(collection, id, data): Transaction` - Add a create operation
|
||||||
|
- `Transaction.update<T>(collection, id, data): Transaction` - Add an update operation
|
||||||
|
- `Transaction.delete(collection, id): Transaction` - Add a delete operation
|
||||||
|
|
||||||
|
### Subscriptions
|
||||||
|
|
||||||
|
- `subscribe(event, callback): () => void` - Subscribe to events, returns unsubscribe function
|
||||||
|
|
||||||
|
### File Operations
|
||||||
|
|
||||||
|
- `uploadFile(fileData, options?): Promise<FileUploadResult>` - Upload a file
|
||||||
|
- `getFile(cid, options?): Promise<FileResult>` - Get a file by CID
|
||||||
|
- `deleteFile(cid, options?): Promise<boolean>` - Delete a file
|
||||||
|
|
||||||
|
### Connection Management
|
||||||
|
|
||||||
|
- `closeConnection(connectionId): Promise<boolean>` - Close a specific connection
|
||||||
|
|
||||||
|
### Indexes and Performance
|
||||||
|
|
||||||
|
- `createIndex(collection, field, options?): Promise<boolean>` - Create an index
|
||||||
|
- `getMetrics(): Metrics` - Get performance metrics
|
||||||
|
- `resetMetrics(): void` - Reset performance metrics
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { config, initDB } from "@debros/network";
|
||||||
|
|
||||||
|
// Configure (optional)
|
||||||
|
config.env.fingerprint = "my-unique-app-id";
|
||||||
|
config.env.port = 9000;
|
||||||
|
config.ipfs.blockstorePath = "./custom-path/blockstore";
|
||||||
|
config.orbitdb.directory = "./custom-path/orbitdb";
|
||||||
|
|
||||||
|
// Initialize with configuration
|
||||||
|
await initDB();
|
||||||
|
```
|
73
examples/abstract-database.ts
Normal file
73
examples/abstract-database.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { initDB, create, get, update, remove, list, query, uploadFile, getFile, deleteFile, stopDB, logger } from '../src';
|
||||||
|
|
||||||
|
// Alternative import method
|
||||||
|
// import debros from '../src';
|
||||||
|
// const { db } = debros;
|
||||||
|
|
||||||
|
async function databaseExample() {
|
||||||
|
try {
|
||||||
|
logger.info('Starting database example...');
|
||||||
|
|
||||||
|
// Initialize the database service (abstracts away IPFS and OrbitDB)
|
||||||
|
await initDB();
|
||||||
|
logger.info('Database service initialized');
|
||||||
|
|
||||||
|
// Create a new user document
|
||||||
|
const userId = 'user123';
|
||||||
|
const userData = {
|
||||||
|
username: 'johndoe',
|
||||||
|
walletAddress: '0x1234567890',
|
||||||
|
avatar: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResult = await create('users', userId, userData);
|
||||||
|
logger.info(`Created user with ID: ${createResult.id} and hash: ${createResult.hash}`);
|
||||||
|
|
||||||
|
// Retrieve the user
|
||||||
|
const user = await get('users', userId);
|
||||||
|
logger.info('Retrieved user:', user);
|
||||||
|
|
||||||
|
// Update the user
|
||||||
|
const updateResult = await update('users', userId, {
|
||||||
|
avatar: 'profile.jpg',
|
||||||
|
bio: 'Software developer'
|
||||||
|
});
|
||||||
|
logger.info(`Updated user with hash: ${updateResult.hash}`);
|
||||||
|
|
||||||
|
// Query users
|
||||||
|
const filteredUsers = await query('users', (user) => user.username === 'johndoe');
|
||||||
|
logger.info(`Found ${filteredUsers.length} matching users`);
|
||||||
|
|
||||||
|
// List all users
|
||||||
|
const allUsers = await list('users', { limit: 10 });
|
||||||
|
logger.info(`Retrieved ${allUsers.length} users`);
|
||||||
|
|
||||||
|
// Upload a file
|
||||||
|
const fileData = Buffer.from('This is a test file content');
|
||||||
|
const fileUpload = await uploadFile(fileData, { filename: 'test.txt' });
|
||||||
|
logger.info(`Uploaded file with CID: ${fileUpload.cid}`);
|
||||||
|
|
||||||
|
// Retrieve the file
|
||||||
|
const file = await getFile(fileUpload.cid);
|
||||||
|
logger.info('Retrieved file:', {
|
||||||
|
content: file.data.toString(),
|
||||||
|
metadata: file.metadata
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete the file
|
||||||
|
await deleteFile(fileUpload.cid);
|
||||||
|
logger.info('File deleted');
|
||||||
|
|
||||||
|
// Delete the user
|
||||||
|
await remove('users', userId);
|
||||||
|
logger.info('User deleted');
|
||||||
|
|
||||||
|
// Stop the database service
|
||||||
|
await stopDB();
|
||||||
|
logger.info('Database service stopped');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in database example:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseExample();
|
266
examples/advanced-database.ts
Normal file
266
examples/advanced-database.ts
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
import {
|
||||||
|
initDB,
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
list,
|
||||||
|
query,
|
||||||
|
uploadFile,
|
||||||
|
getFile,
|
||||||
|
createTransaction,
|
||||||
|
commitTransaction,
|
||||||
|
subscribe,
|
||||||
|
defineSchema,
|
||||||
|
getMetrics,
|
||||||
|
createIndex,
|
||||||
|
ErrorCode,
|
||||||
|
StoreType,
|
||||||
|
logger
|
||||||
|
} from '../src';
|
||||||
|
|
||||||
|
// Define a user schema
|
||||||
|
const userSchema = {
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
min: 3,
|
||||||
|
max: 20,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$',
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: 'number',
|
||||||
|
min: 18,
|
||||||
|
max: 120,
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['username'],
|
||||||
|
};
|
||||||
|
|
||||||
|
async function advancedDatabaseExample() {
|
||||||
|
try {
|
||||||
|
logger.info('Starting advanced database example...');
|
||||||
|
|
||||||
|
// Initialize the database service with a specific connection ID
|
||||||
|
const connectionId = await initDB('example-connection');
|
||||||
|
logger.info(`Database service initialized with connection ID: ${connectionId}`);
|
||||||
|
|
||||||
|
// Define schema for validation
|
||||||
|
defineSchema('users', userSchema);
|
||||||
|
logger.info('User schema defined');
|
||||||
|
|
||||||
|
// Create index for faster queries (works with docstore)
|
||||||
|
await createIndex('users', 'username', { storeType: StoreType.DOCSTORE });
|
||||||
|
logger.info('Created index on username field');
|
||||||
|
|
||||||
|
// Set up subscription for real-time updates
|
||||||
|
const unsubscribe = subscribe('document:created', (data) => {
|
||||||
|
logger.info('Document created:', data.collection, data.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create multiple users using a transaction
|
||||||
|
const transaction = createTransaction(connectionId);
|
||||||
|
|
||||||
|
transaction
|
||||||
|
.create('users', 'user1', {
|
||||||
|
username: 'alice',
|
||||||
|
email: 'alice@example.com',
|
||||||
|
age: 28,
|
||||||
|
roles: ['admin', 'user'],
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
.create('users', 'user2', {
|
||||||
|
username: 'bob',
|
||||||
|
email: 'bob@example.com',
|
||||||
|
age: 34,
|
||||||
|
roles: ['user'],
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
.create('users', 'user3', {
|
||||||
|
username: 'charlie',
|
||||||
|
email: 'charlie@example.com',
|
||||||
|
age: 45,
|
||||||
|
roles: ['moderator', 'user'],
|
||||||
|
isActive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const txResult = await commitTransaction(transaction);
|
||||||
|
logger.info(`Transaction committed with ${txResult.results.length} operations`);
|
||||||
|
|
||||||
|
// Get a specific user with type safety
|
||||||
|
interface User {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
roles: string[];
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using KeyValue store (default)
|
||||||
|
const user = await get<User>('users', 'user1');
|
||||||
|
logger.info('Retrieved user from KeyValue store:', user?.username, user?.email);
|
||||||
|
|
||||||
|
// Using DocStore
|
||||||
|
await create<User>(
|
||||||
|
'users_docstore',
|
||||||
|
'user1',
|
||||||
|
{
|
||||||
|
username: 'alice_doc',
|
||||||
|
email: 'alice_doc@example.com',
|
||||||
|
age: 28,
|
||||||
|
roles: ['admin', 'user'],
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{ storeType: StoreType.DOCSTORE }
|
||||||
|
);
|
||||||
|
|
||||||
|
const docStoreUser = await get<User>('users_docstore', 'user1', { storeType: StoreType.DOCSTORE });
|
||||||
|
logger.info('Retrieved user from DocStore:', docStoreUser?.username, docStoreUser?.email);
|
||||||
|
|
||||||
|
// Using Feed/EventLog store
|
||||||
|
await create<User>(
|
||||||
|
'users_feed',
|
||||||
|
'user1',
|
||||||
|
{
|
||||||
|
username: 'alice_feed',
|
||||||
|
email: 'alice_feed@example.com',
|
||||||
|
age: 28,
|
||||||
|
roles: ['admin', 'user'],
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{ storeType: StoreType.FEED }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the feed entry (creates a new entry with the same ID)
|
||||||
|
await update<User>(
|
||||||
|
'users_feed',
|
||||||
|
'user1',
|
||||||
|
{
|
||||||
|
roles: ['admin', 'user', 'tester'],
|
||||||
|
},
|
||||||
|
{ storeType: StoreType.FEED }
|
||||||
|
);
|
||||||
|
|
||||||
|
// List all entries in the feed
|
||||||
|
const feedUsers = await list<User>('users_feed', { storeType: StoreType.FEED });
|
||||||
|
logger.info(`Found ${feedUsers.total} feed entries:`);
|
||||||
|
feedUsers.documents.forEach(user => {
|
||||||
|
logger.info(`- ${user.username} (${user.email})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Using Counter store
|
||||||
|
await create(
|
||||||
|
'counters',
|
||||||
|
'visitors',
|
||||||
|
{ value: 100 },
|
||||||
|
{ storeType: StoreType.COUNTER }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Increment the counter
|
||||||
|
await update(
|
||||||
|
'counters',
|
||||||
|
'visitors',
|
||||||
|
{ increment: 5 },
|
||||||
|
{ storeType: StoreType.COUNTER }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the counter value
|
||||||
|
const counter = await get('counters', 'visitors', { storeType: StoreType.COUNTER });
|
||||||
|
logger.info(`Counter value: ${counter?.value}`);
|
||||||
|
|
||||||
|
// Update a user in KeyValue store
|
||||||
|
await update<User>('users', 'user1', {
|
||||||
|
roles: ['admin', 'user', 'tester'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query users from KeyValue store
|
||||||
|
const result = await query<User>(
|
||||||
|
'users',
|
||||||
|
(user) => user.isActive === true,
|
||||||
|
{
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
sort: { field: 'username', order: 'asc' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Found ${result.total} active users:`);
|
||||||
|
result.documents.forEach(user => {
|
||||||
|
logger.info(`- ${user.username} (${user.email})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all users from KeyValue store with pagination
|
||||||
|
const allUsers = await list<User>('users', {
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
sort: { field: 'age', order: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Listed ${allUsers.total} users sorted by age (desc):`);
|
||||||
|
allUsers.documents.forEach(user => {
|
||||||
|
logger.info(`- ${user.username}: ${user.age} years old`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload a file with metadata
|
||||||
|
const fileData = Buffer.from('This is a test file with advanced features.');
|
||||||
|
const fileUpload = await uploadFile(fileData, {
|
||||||
|
filename: 'advanced-test.txt',
|
||||||
|
metadata: {
|
||||||
|
contentType: 'text/plain',
|
||||||
|
tags: ['example', 'test'],
|
||||||
|
owner: 'user1',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Uploaded file with CID: ${fileUpload.cid}`);
|
||||||
|
|
||||||
|
// Retrieve the file
|
||||||
|
const file = await getFile(fileUpload.cid);
|
||||||
|
logger.info('Retrieved file content:', file.data.toString());
|
||||||
|
logger.info('File metadata:', file.metadata);
|
||||||
|
|
||||||
|
// Get performance metrics
|
||||||
|
const metrics = getMetrics();
|
||||||
|
logger.info('Database metrics:', {
|
||||||
|
operations: metrics.operations,
|
||||||
|
performance: {
|
||||||
|
averageOperationTime: `${metrics.performance.averageOperationTime?.toFixed(2)}ms`,
|
||||||
|
},
|
||||||
|
cache: metrics.cacheStats,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unsubscribe from events
|
||||||
|
unsubscribe();
|
||||||
|
logger.info('Unsubscribed from events');
|
||||||
|
|
||||||
|
// Delete a user
|
||||||
|
await remove('users', 'user3');
|
||||||
|
logger.info('Deleted user user3');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error && typeof error === 'object' && 'code' in error) {
|
||||||
|
logger.error(`Database error (${error.code}):`, error.message);
|
||||||
|
} else {
|
||||||
|
logger.error('Error in advanced database example:', error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
advancedDatabaseExample();
|
@ -1,4 +1,6 @@
|
|||||||
import { initIpfs, initOrbitDB, logger, createServiceLogger } from '../src/index';
|
import { init as initIpfs, stop as stopIpfs } from '../src/ipfs/ipfsService';
|
||||||
|
import { init as initOrbitDB } from '../src/orbit/orbitDBService';
|
||||||
|
import { createServiceLogger } from '../src/utils/logger';
|
||||||
|
|
||||||
const appLogger = createServiceLogger('APP');
|
const appLogger = createServiceLogger('APP');
|
||||||
|
|
||||||
@ -11,11 +13,7 @@ async function startNode() {
|
|||||||
appLogger.info('IPFS node initialized');
|
appLogger.info('IPFS node initialized');
|
||||||
|
|
||||||
// Initialize OrbitDB
|
// Initialize OrbitDB
|
||||||
const ipfsService = {
|
const orbitdb = await initOrbitDB();
|
||||||
getHelia: () => ipfs,
|
|
||||||
};
|
|
||||||
|
|
||||||
const orbitdb = await initOrbitDB(ipfsService);
|
|
||||||
appLogger.info('OrbitDB initialized');
|
appLogger.info('OrbitDB initialized');
|
||||||
|
|
||||||
// Create a test database
|
// Create a test database
|
||||||
@ -40,7 +38,7 @@ async function startNode() {
|
|||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
appLogger.info('Shutting down...');
|
appLogger.info('Shutting down...');
|
||||||
await orbitdb.stop();
|
await orbitdb.stop();
|
||||||
await initIpfs.stop();
|
await stopIpfs();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
// Example of how to integrate the new @debros/node-core package into an application
|
|
||||||
|
|
||||||
import express from 'express';
|
|
||||||
import { initIpfs, initOrbitDB, createServiceLogger, getConnectedPeers } from '@debros/node-core'; // This would be the import path when installed from npm
|
|
||||||
|
|
||||||
// Create service-specific loggers
|
|
||||||
const apiLogger = createServiceLogger('API');
|
|
||||||
const networkLogger = createServiceLogger('NETWORK');
|
|
||||||
|
|
||||||
// Initialize Express app
|
|
||||||
const app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Network state
|
|
||||||
let ipfsNode: any;
|
|
||||||
let orbitInstance: any;
|
|
||||||
let messageDB: any;
|
|
||||||
|
|
||||||
// Initialize network components
|
|
||||||
async function initializeNetwork() {
|
|
||||||
try {
|
|
||||||
// Initialize IPFS
|
|
||||||
networkLogger.info('Initializing IPFS node...');
|
|
||||||
ipfsNode = await initIpfs();
|
|
||||||
|
|
||||||
// Initialize OrbitDB
|
|
||||||
networkLogger.info('Initializing OrbitDB...');
|
|
||||||
orbitInstance = await initOrbitDB({
|
|
||||||
getHelia: () => ipfsNode,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open message database
|
|
||||||
messageDB = await orbitInstance.open('messages', {
|
|
||||||
type: 'feed',
|
|
||||||
});
|
|
||||||
|
|
||||||
networkLogger.info('Network components initialized successfully');
|
|
||||||
|
|
||||||
// Log connected peers every minute
|
|
||||||
setInterval(() => {
|
|
||||||
const peers = getConnectedPeers();
|
|
||||||
networkLogger.info(`Connected to ${peers.size} peers`);
|
|
||||||
}, 60000);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
networkLogger.error('Failed to initialize network:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API routes
|
|
||||||
app.get('/api/peers', (req, res) => {
|
|
||||||
const peers = getConnectedPeers();
|
|
||||||
const peerList: any[] = [];
|
|
||||||
|
|
||||||
peers.forEach((data, peerId) => {
|
|
||||||
peerList.push({
|
|
||||||
id: peerId,
|
|
||||||
load: data.load,
|
|
||||||
address: data.publicAddress,
|
|
||||||
lastSeen: data.lastSeen,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ peers: peerList });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/messages', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const messages = messageDB.iterator({ limit: 100 }).collect();
|
|
||||||
res.json({ messages });
|
|
||||||
} catch (error) {
|
|
||||||
apiLogger.error('Error fetching messages:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to fetch messages' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/messages', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { content } = req.body;
|
|
||||||
if (!content) {
|
|
||||||
return res.status(400).json({ error: 'Content is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = await messageDB.add({
|
|
||||||
content,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(201).json({ id: entry });
|
|
||||||
} catch (error) {
|
|
||||||
apiLogger.error('Error creating message:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to create message' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the application
|
|
||||||
async function startApp() {
|
|
||||||
const networkInitialized = await initializeNetwork();
|
|
||||||
|
|
||||||
if (networkInitialized) {
|
|
||||||
const port = config.env.port;
|
|
||||||
app.listen(port, () => {
|
|
||||||
apiLogger.info(`Server listening on port ${port}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
apiLogger.error('Cannot start application: Network initialization failed');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown handler
|
|
||||||
process.on('SIGINT', async () => {
|
|
||||||
networkLogger.info('Application shutting down...');
|
|
||||||
if (orbitInstance) {
|
|
||||||
await orbitInstance.stop();
|
|
||||||
}
|
|
||||||
if (ipfsNode) {
|
|
||||||
await initIpfs.stop();
|
|
||||||
}
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the application
|
|
||||||
startApp();
|
|
2
orbitdb.d.ts
vendored
2
orbitdb.d.ts
vendored
@ -1,7 +1,7 @@
|
|||||||
// custom.d.ts
|
// custom.d.ts
|
||||||
declare module '@orbitdb/core' {
|
declare module '@orbitdb/core' {
|
||||||
// Import the types from @constl/orbit-db-types
|
// Import the types from @constl/orbit-db-types
|
||||||
import { OrbitDBTypes } from '@constl/orbit-db-types';
|
import { OrbitDBTypes } from '@orbitdb/core-types';
|
||||||
|
|
||||||
// Assuming @orbitdb/core exports an interface or type you want to override
|
// Assuming @orbitdb/core exports an interface or type you want to override
|
||||||
// Replace 'OrbitDB' with the actual export name from @orbitdb/core you want to type
|
// Replace 'OrbitDB' with the actual export name from @orbitdb/core you want to type
|
||||||
|
23
package.json
23
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@debros/network",
|
"name": "@debros/network",
|
||||||
"version": "0.0.13-alpha",
|
"version": "0.0.22-alpha",
|
||||||
"description": "Debros network core functionality for IPFS, libp2p and OrbitDB",
|
"description": "Debros network core functionality for IPFS, libp2p and OrbitDB",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
@ -11,12 +11,14 @@
|
|||||||
"types.d.ts"
|
"types.d.ts"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc && tsc-esm-fix --outDir=./dist/esm",
|
||||||
"dev": "tsc -w",
|
"dev": "tsc -w",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"prepublishOnly": "npm run clean && npm run build",
|
"prepublishOnly": "npm run clean && npm run build",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"lint": "npx eslint src"
|
"lint": "npx eslint src",
|
||||||
|
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
|
||||||
|
"lint:fix": "npx eslint src --fix"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ipfs",
|
"ipfs",
|
||||||
@ -32,6 +34,7 @@
|
|||||||
"@chainsafe/libp2p-gossipsub": "^14.1.0",
|
"@chainsafe/libp2p-gossipsub": "^14.1.0",
|
||||||
"@chainsafe/libp2p-noise": "^16.1.0",
|
"@chainsafe/libp2p-noise": "^16.1.0",
|
||||||
"@chainsafe/libp2p-yamux": "^7.0.1",
|
"@chainsafe/libp2p-yamux": "^7.0.1",
|
||||||
|
"@helia/unixfs": "^5.0.0",
|
||||||
"@libp2p/bootstrap": "^11.0.32",
|
"@libp2p/bootstrap": "^11.0.32",
|
||||||
"@libp2p/crypto": "^5.0.15",
|
"@libp2p/crypto": "^5.0.15",
|
||||||
"@libp2p/identify": "^3.0.27",
|
"@libp2p/identify": "^3.0.27",
|
||||||
@ -47,6 +50,8 @@
|
|||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"helia": "^5.3.0",
|
"helia": "^5.3.0",
|
||||||
"libp2p": "^2.8.2",
|
"libp2p": "^2.8.2",
|
||||||
|
"multiformats": "^13.3.2",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"winston": "^3.17.0"
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
@ -54,16 +59,24 @@
|
|||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@constl/orbit-db-types": "^2.0.6",
|
"@eslint/js": "^9.24.0",
|
||||||
"@orbitdb/core-types": "^1.0.14",
|
"@orbitdb/core-types": "^1.0.14",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
"@types/node-forge": "^1.3.11",
|
"@types/node-forge": "^1.3.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||||
|
"@typescript-eslint/parser": "^8.29.0",
|
||||||
|
"eslint": "^9.24.0",
|
||||||
|
"eslint-config-prettier": "^10.1.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.6",
|
||||||
|
"globals": "^16.0.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^15.5.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"typescript": "^5.8.2"
|
"tsc-esm-fix": "^3.1.2",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
"typescript-eslint": "^8.29.0"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
|
1066
pnpm-lock.yaml
generated
1066
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
62
src/db/cache/cacheService.ts
vendored
Normal file
62
src/db/cache/cacheService.ts
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import NodeCache from 'node-cache';
|
||||||
|
import { createServiceLogger } from '../../utils/logger';
|
||||||
|
|
||||||
|
const logger = createServiceLogger('DB_CACHE');
|
||||||
|
|
||||||
|
// Cache for frequently accessed documents
|
||||||
|
const cache = new NodeCache({
|
||||||
|
stdTTL: 300, // 5 minutes default TTL
|
||||||
|
checkperiod: 60, // Check for expired items every 60 seconds
|
||||||
|
useClones: false, // Don't clone objects (for performance)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache statistics
|
||||||
|
export const cacheStats = {
|
||||||
|
hits: 0,
|
||||||
|
misses: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an item from cache
|
||||||
|
*/
|
||||||
|
export const get = <T>(key: string): T | undefined => {
|
||||||
|
const value = cache.get<T>(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
cacheStats.hits++;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
cacheStats.misses++;
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an item in cache
|
||||||
|
*/
|
||||||
|
export const set = <T>(key: string, value: T, ttl?: number): boolean => {
|
||||||
|
if (ttl === undefined) {
|
||||||
|
return cache.set(key, value);
|
||||||
|
}
|
||||||
|
return cache.set(key, value, ttl);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an item from cache
|
||||||
|
*/
|
||||||
|
export const del = (key: string | string[]): number => {
|
||||||
|
return cache.del(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush the entire cache
|
||||||
|
*/
|
||||||
|
export const flushAll = (): void => {
|
||||||
|
cache.flushAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset cache statistics
|
||||||
|
*/
|
||||||
|
export const resetStats = (): void => {
|
||||||
|
cacheStats.hits = 0;
|
||||||
|
cacheStats.misses = 0;
|
||||||
|
};
|
138
src/db/core/connection.ts
Normal file
138
src/db/core/connection.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
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<string, DBConnection>();
|
||||||
|
let defaultConnectionId: string | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the database service
|
||||||
|
* This abstracts away OrbitDB and IPFS from the end user
|
||||||
|
*/
|
||||||
|
export const init = async (connectionId?: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const connId = connectionId || `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
logger.info(`Initializing DB service with connection ID: ${connId}`);
|
||||||
|
|
||||||
|
// Initialize IPFS
|
||||||
|
const ipfsInstance = await initIpfs();
|
||||||
|
|
||||||
|
// Initialize OrbitDB
|
||||||
|
const orbitdbInstance = await initOrbitDB();
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
logger.error('Failed to initialize DB service:', error);
|
||||||
|
throw new DBError(ErrorCode.INITIALIZATION_FAILED, 'Failed to initialize database service', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a specific database connection
|
||||||
|
*/
|
||||||
|
export const closeConnection = async (connectionId: string): Promise<boolean> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void> => {
|
||||||
|
try {
|
||||||
|
// Close all connections
|
||||||
|
for (const [id, connection] of connections.entries()) {
|
||||||
|
if (connection.isActive) {
|
||||||
|
await closeConnection(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop IPFS if needed
|
||||||
|
const ipfs = connections.get(defaultConnectionId || '')?.ipfs;
|
||||||
|
if (ipfs) {
|
||||||
|
await stopIpfs();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConnectionId = null;
|
||||||
|
logger.info('All DB connections stopped successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error stopping DB connections:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
18
src/db/core/error.ts
Normal file
18
src/db/core/error.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
212
src/db/dbService.ts
Normal file
212
src/db/dbService.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import { createServiceLogger } from '../utils/logger';
|
||||||
|
import { init, getConnection, closeConnection, stop } from './core/connection';
|
||||||
|
import { defineSchema, validateDocument } from './schema/validator';
|
||||||
|
import * as cache from './cache/cacheService';
|
||||||
|
import * as events from './events/eventService';
|
||||||
|
import { getMetrics, resetMetrics } from './metrics/metricsService';
|
||||||
|
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, getMetrics, resetMetrics, 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 <T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Omit<T, 'createdAt' | 'updatedAt'>,
|
||||||
|
options?: { connectionId?: string, storeType?: StoreType }
|
||||||
|
): Promise<CreateResult> => {
|
||||||
|
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 <T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: { connectionId?: string; skipCache?: boolean, storeType?: StoreType }
|
||||||
|
): Promise<T | null> => {
|
||||||
|
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 <T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Partial<Omit<T, 'createdAt' | 'updatedAt'>>,
|
||||||
|
options?: { connectionId?: string; upsert?: boolean, storeType?: StoreType }
|
||||||
|
): Promise<UpdateResult> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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 <T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
options?: ListOptions & { storeType?: StoreType }
|
||||||
|
): Promise<PaginatedResult<T>> => {
|
||||||
|
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 <T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
filter: (doc: T) => boolean,
|
||||||
|
options?: QueryOptions & { storeType?: StoreType }
|
||||||
|
): Promise<PaginatedResult<T>> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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,
|
||||||
|
getMetrics,
|
||||||
|
resetMetrics,
|
||||||
|
closeConnection,
|
||||||
|
stop,
|
||||||
|
StoreType
|
||||||
|
};
|
36
src/db/events/eventService.ts
Normal file
36
src/db/events/eventService.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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();
|
||||||
|
};
|
101
src/db/metrics/metricsService.ts
Normal file
101
src/db/metrics/metricsService.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { Metrics, ErrorCode } from '../types';
|
||||||
|
import { DBError } from '../core/error';
|
||||||
|
import * as cacheService from '../cache/cacheService';
|
||||||
|
|
||||||
|
// Metrics tracking
|
||||||
|
const metrics: Metrics = {
|
||||||
|
operations: {
|
||||||
|
creates: 0,
|
||||||
|
reads: 0,
|
||||||
|
updates: 0,
|
||||||
|
deletes: 0,
|
||||||
|
queries: 0,
|
||||||
|
fileUploads: 0,
|
||||||
|
fileDownloads: 0,
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
totalOperationTime: 0,
|
||||||
|
operationCount: 0,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
count: 0,
|
||||||
|
byCode: {},
|
||||||
|
},
|
||||||
|
cacheStats: {
|
||||||
|
hits: 0,
|
||||||
|
misses: 0,
|
||||||
|
},
|
||||||
|
startTime: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measure performance of a database operation
|
||||||
|
*/
|
||||||
|
export async function measurePerformance<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
|
const startTime = performance.now();
|
||||||
|
try {
|
||||||
|
const result = await operation();
|
||||||
|
const endTime = performance.now();
|
||||||
|
metrics.performance.totalOperationTime += (endTime - startTime);
|
||||||
|
metrics.performance.operationCount++;
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const endTime = performance.now();
|
||||||
|
metrics.performance.totalOperationTime += (endTime - startTime);
|
||||||
|
metrics.performance.operationCount++;
|
||||||
|
|
||||||
|
// Track error metrics
|
||||||
|
metrics.errors.count++;
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
metrics.errors.byCode[error.code] = (metrics.errors.byCode[error.code] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database metrics
|
||||||
|
*/
|
||||||
|
export const getMetrics = (): Metrics => {
|
||||||
|
return {
|
||||||
|
...metrics,
|
||||||
|
// Sync cache stats
|
||||||
|
cacheStats: cacheService.cacheStats,
|
||||||
|
// Calculate some derived metrics
|
||||||
|
performance: {
|
||||||
|
...metrics.performance,
|
||||||
|
averageOperationTime: metrics.performance.operationCount > 0 ?
|
||||||
|
metrics.performance.totalOperationTime / metrics.performance.operationCount :
|
||||||
|
0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset metrics (useful for testing)
|
||||||
|
*/
|
||||||
|
export const resetMetrics = (): void => {
|
||||||
|
metrics.operations = {
|
||||||
|
creates: 0,
|
||||||
|
reads: 0,
|
||||||
|
updates: 0,
|
||||||
|
deletes: 0,
|
||||||
|
queries: 0,
|
||||||
|
fileUploads: 0,
|
||||||
|
fileDownloads: 0,
|
||||||
|
};
|
||||||
|
metrics.performance = {
|
||||||
|
totalOperationTime: 0,
|
||||||
|
operationCount: 0,
|
||||||
|
};
|
||||||
|
metrics.errors = {
|
||||||
|
count: 0,
|
||||||
|
byCode: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset cache stats too
|
||||||
|
cacheService.resetStats();
|
||||||
|
|
||||||
|
metrics.startTime = Date.now();
|
||||||
|
};
|
225
src/db/schema/validator.ts
Normal file
225
src/db/schema/validator.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
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<string, CollectionSchema>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
142
src/db/stores/baseStore.ts
Normal file
142
src/db/stores/baseStore.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { createServiceLogger } from '../../utils/logger';
|
||||||
|
import { openDB } from '../../orbit/orbitDBService';
|
||||||
|
import { getConnection } from '../core/connection';
|
||||||
|
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 a new document
|
||||||
|
*/
|
||||||
|
create<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Omit<T, 'createdAt' | 'updatedAt'>,
|
||||||
|
options?: StoreOptions,
|
||||||
|
): Promise<CreateResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a document by ID
|
||||||
|
*/
|
||||||
|
get<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: StoreOptions & { skipCache?: boolean },
|
||||||
|
): Promise<T | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a document
|
||||||
|
*/
|
||||||
|
update<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Partial<Omit<T, 'createdAt' | 'updatedAt'>>,
|
||||||
|
options?: StoreOptions & { upsert?: boolean },
|
||||||
|
): Promise<UpdateResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a document
|
||||||
|
*/
|
||||||
|
remove(collection: string, id: string, options?: StoreOptions): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all documents in a collection with pagination
|
||||||
|
*/
|
||||||
|
list<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
options?: ListOptions,
|
||||||
|
): Promise<PaginatedResult<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query documents in a collection with filtering and pagination
|
||||||
|
*/
|
||||||
|
query<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
filter: (doc: T) => boolean,
|
||||||
|
options?: QueryOptions,
|
||||||
|
): Promise<PaginatedResult<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an index for a collection to speed up queries
|
||||||
|
*/
|
||||||
|
createIndex(collection: string, field: string, options?: StoreOptions): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a store of the specified type
|
||||||
|
*/
|
||||||
|
export async function openStore(
|
||||||
|
collection: string,
|
||||||
|
storeType: StoreType,
|
||||||
|
options?: StoreOptions,
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const connection = getConnection(options?.connectionId);
|
||||||
|
logger.info(`Connection for ${collection}:`, connection);
|
||||||
|
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);
|
||||||
|
throw new DBError(
|
||||||
|
ErrorCode.OPERATION_FAILED,
|
||||||
|
`Failed to open ${storeType} store for collection ${collection}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to prepare a document for storage
|
||||||
|
*/
|
||||||
|
export function prepareDocument<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
data: Omit<T, 'createdAt' | 'updatedAt'>,
|
||||||
|
existingDoc?: T | null,
|
||||||
|
): T {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// Sanitize the input data by replacing undefined with null
|
||||||
|
const sanitizedData = Object.fromEntries(
|
||||||
|
Object.entries(data).map(([key, value]) => [key, value === undefined ? null : value]),
|
||||||
|
) as Omit<T, 'createdAt' | 'updatedAt'>;
|
||||||
|
|
||||||
|
// If it's an update to an existing document
|
||||||
|
if (existingDoc) {
|
||||||
|
const doc = {
|
||||||
|
...existingDoc,
|
||||||
|
...sanitizedData,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
} as T;
|
||||||
|
|
||||||
|
// Validate the document against its schema
|
||||||
|
validateDocument(collection, doc);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it's a new document
|
||||||
|
const doc = {
|
||||||
|
...sanitizedData,
|
||||||
|
createdAt: timestamp,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
} as unknown as T;
|
||||||
|
|
||||||
|
// Validate the document against its schema
|
||||||
|
validateDocument(collection, doc);
|
||||||
|
return doc;
|
||||||
|
}
|
331
src/db/stores/counterStore.ts
Normal file
331
src/db/stores/counterStore.ts
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
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 cache from '../cache/cacheService';
|
||||||
|
import * as events from '../events/eventService';
|
||||||
|
import { measurePerformance } from '../metrics/metricsService';
|
||||||
|
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Omit<T, 'createdAt' | 'updatedAt'>,
|
||||||
|
options?: StoreOptions
|
||||||
|
): Promise<CreateResult> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
|
||||||
|
// Emit change event
|
||||||
|
events.emit('document:created', { collection, id, document });
|
||||||
|
|
||||||
|
logger.info(`Set counter in ${collection} to ${value}`);
|
||||||
|
return { id, hash };
|
||||||
|
} catch (error) {
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: StoreOptions & { skipCache?: boolean }
|
||||||
|
): Promise<T | null> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
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
|
||||||
|
|
||||||
|
// Check cache first if not skipped
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
if (!options?.skipCache) {
|
||||||
|
const cachedDocument = cache.get<T>(cacheKey);
|
||||||
|
if (cachedDocument) {
|
||||||
|
return cachedDocument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
|
||||||
|
return document;
|
||||||
|
} catch (error) {
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Partial<Omit<T, 'createdAt' | 'updatedAt'>>,
|
||||||
|
options?: StoreOptions & { upsert?: boolean }
|
||||||
|
): Promise<UpdateResult> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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<boolean> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Remove from cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.del(cacheKey);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
options?: ListOptions
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
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) {
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
filter: (doc: T) => boolean,
|
||||||
|
options?: QueryOptions
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
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) {
|
||||||
|
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<boolean> {
|
||||||
|
logger.warn(`Index creation not supported for counter collections, ignoring request for ${collection}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
350
src/db/stores/docStore.ts
Normal file
350
src/db/stores/docStore.ts
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
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 cache from '../cache/cacheService';
|
||||||
|
import * as events from '../events/eventService';
|
||||||
|
import { measurePerformance } from '../metrics/metricsService';
|
||||||
|
|
||||||
|
const logger = createServiceLogger('DOCSTORE');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DocStore implementation
|
||||||
|
* Uses OrbitDB's document store which allows for more complex document storage with indices
|
||||||
|
*/
|
||||||
|
export class DocStore implements BaseStore {
|
||||||
|
/**
|
||||||
|
* Create a new document in the specified collection
|
||||||
|
*/
|
||||||
|
async create<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Omit<T, 'createdAt' | 'updatedAt'>,
|
||||||
|
options?: StoreOptions
|
||||||
|
): Promise<CreateResult> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.DOCSTORE, options);
|
||||||
|
|
||||||
|
// Prepare document for storage (including _id which is required for docstore)
|
||||||
|
const document = {
|
||||||
|
_id: id,
|
||||||
|
...prepareDocument<T>(collection, data)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to database
|
||||||
|
const hash = await db.put(document);
|
||||||
|
|
||||||
|
// Add to cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
|
||||||
|
// Emit change event
|
||||||
|
events.emit('document:created', { collection, id, document });
|
||||||
|
|
||||||
|
logger.info(`Created document in ${collection} with id ${id}`);
|
||||||
|
return { id, hash };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error creating document in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to create document in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a document by ID from a collection
|
||||||
|
*/
|
||||||
|
async get<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: StoreOptions & { skipCache?: boolean }
|
||||||
|
): Promise<T | null> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
// Check cache first if not skipped
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
if (!options?.skipCache) {
|
||||||
|
const cachedDocument = cache.get<T>(cacheKey);
|
||||||
|
if (cachedDocument) {
|
||||||
|
return cachedDocument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await openStore(collection, StoreType.DOCSTORE, options);
|
||||||
|
const document = await db.get(id) as T | null;
|
||||||
|
|
||||||
|
// Update cache if document exists
|
||||||
|
if (document) {
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return document;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error getting document ${id} from ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to get document ${id} from ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a document in a collection
|
||||||
|
*/
|
||||||
|
async update<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Partial<Omit<T, 'createdAt' | 'updatedAt'>>,
|
||||||
|
options?: StoreOptions & { upsert?: boolean }
|
||||||
|
): Promise<UpdateResult> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.DOCSTORE, options);
|
||||||
|
const existing = await db.get(id) as T | null;
|
||||||
|
|
||||||
|
if (!existing && !options?.upsert) {
|
||||||
|
throw new DBError(
|
||||||
|
ErrorCode.DOCUMENT_NOT_FOUND,
|
||||||
|
`Document ${id} not found in ${collection}`,
|
||||||
|
{ collection, id }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare document for update
|
||||||
|
const document = {
|
||||||
|
_id: id,
|
||||||
|
...prepareDocument<T>(collection, data as unknown as Omit<T, "createdAt" | "updatedAt">, existing || undefined)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update in database
|
||||||
|
const hash = await db.put(document);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
|
||||||
|
// Emit change event
|
||||||
|
events.emit('document:updated', { collection, id, document, previous: existing });
|
||||||
|
|
||||||
|
logger.info(`Updated document in ${collection} with id ${id}`);
|
||||||
|
return { id, hash };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error updating document in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to update document in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a document from a collection
|
||||||
|
*/
|
||||||
|
async remove(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: StoreOptions
|
||||||
|
): Promise<boolean> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.DOCSTORE, options);
|
||||||
|
|
||||||
|
// Get the document before deleting for the event
|
||||||
|
const document = await db.get(id);
|
||||||
|
|
||||||
|
// Delete from database
|
||||||
|
await db.del(id);
|
||||||
|
|
||||||
|
// Remove from cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.del(cacheKey);
|
||||||
|
|
||||||
|
// Emit change event
|
||||||
|
events.emit('document:deleted', { collection, id, document });
|
||||||
|
|
||||||
|
logger.info(`Deleted document in ${collection} with id ${id}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error deleting document in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to delete document in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all documents in a collection with pagination
|
||||||
|
*/
|
||||||
|
async list<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
options?: ListOptions
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.DOCSTORE, options);
|
||||||
|
const allDocs = await db.query((doc: any) => true);
|
||||||
|
|
||||||
|
let documents = allDocs.map((doc: any) => ({
|
||||||
|
id: doc._id,
|
||||||
|
...doc
|
||||||
|
})) 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = documents.length;
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
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
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error listing documents in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to list documents in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query documents in a collection with filtering and pagination
|
||||||
|
*/
|
||||||
|
async query<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
filter: (doc: T) => boolean,
|
||||||
|
options?: QueryOptions
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.DOCSTORE, 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[];
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = documents.length;
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
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
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error querying documents in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to query 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<boolean> {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.DOCSTORE, options);
|
||||||
|
|
||||||
|
// DocStore supports indexing, so we create the index
|
||||||
|
if (typeof db.createIndex === 'function') {
|
||||||
|
await db.createIndex(field);
|
||||||
|
logger.info(`Index created on ${field} for collection ${collection}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Index creation not supported for this DB instance, but DocStore has built-in indices`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error creating index for ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to create index for ${collection}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
416
src/db/stores/feedStore.ts
Normal file
416
src/db/stores/feedStore.ts
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
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 cache from '../cache/cacheService';
|
||||||
|
import * as events from '../events/eventService';
|
||||||
|
import { measurePerformance } from '../metrics/metricsService';
|
||||||
|
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Omit<T, 'createdAt' | 'updatedAt'>,
|
||||||
|
options?: StoreOptions
|
||||||
|
): Promise<CreateResult> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.FEED, options);
|
||||||
|
|
||||||
|
// Prepare document for storage with ID
|
||||||
|
const document = {
|
||||||
|
id,
|
||||||
|
...prepareDocument<T>(collection, data)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to database
|
||||||
|
const hash = await db.add(document);
|
||||||
|
|
||||||
|
// Feed entries are append-only, so we use a different cache key pattern
|
||||||
|
const cacheKey = `${collection}:entry:${hash}`;
|
||||||
|
cache.set(cacheKey, 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) {
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
hash: string,
|
||||||
|
options?: StoreOptions & { skipCache?: boolean }
|
||||||
|
): Promise<T | null> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
// Check cache first if not skipped
|
||||||
|
const cacheKey = `${collection}:entry:${hash}`;
|
||||||
|
if (!options?.skipCache) {
|
||||||
|
const cachedDocument = cache.get<T>(cacheKey);
|
||||||
|
if (cachedDocument) {
|
||||||
|
return cachedDocument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
|
||||||
|
return document;
|
||||||
|
} catch (error) {
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Partial<Omit<T, 'createdAt' | 'updatedAt'>>,
|
||||||
|
options?: StoreOptions & { upsert?: boolean }
|
||||||
|
): Promise<UpdateResult> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.FEED, options);
|
||||||
|
|
||||||
|
// Find the latest entry with the given id
|
||||||
|
const entries = await db.iterator({ limit: -1 }).collect();
|
||||||
|
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<T>(collection, data as unknown as Omit<T, "createdAt" | "updatedAt">, 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);
|
||||||
|
|
||||||
|
// Cache the new entry
|
||||||
|
const cacheKey = `${collection}:entry:${hash}`;
|
||||||
|
cache.set(cacheKey, 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) {
|
||||||
|
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<boolean> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.FEED, options);
|
||||||
|
|
||||||
|
// Find the entry with the given id
|
||||||
|
const entries = await db.iterator({ limit: -1 }).collect();
|
||||||
|
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) {
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
options?: ListOptions
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.FEED, options);
|
||||||
|
|
||||||
|
// Get all entries
|
||||||
|
const entries = await db.iterator({ limit: -1 }).collect();
|
||||||
|
|
||||||
|
// Group by ID and keep only the latest entry for each ID
|
||||||
|
// Also filter out tombstone entries
|
||||||
|
const latestEntries = new Map<string, any>();
|
||||||
|
for (const entry of entries) {
|
||||||
|
const value = entry.payload.value;
|
||||||
|
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
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = documents.length;
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
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
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
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<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
filter: (doc: T) => boolean,
|
||||||
|
options?: QueryOptions
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.FEED, options);
|
||||||
|
|
||||||
|
// Get all entries
|
||||||
|
const entries = await db.iterator({ limit: -1 }).collect();
|
||||||
|
|
||||||
|
// Group by ID and keep only the latest entry for each ID
|
||||||
|
// Also filter out tombstone entries
|
||||||
|
const latestEntries = new Map<string, any>();
|
||||||
|
for (const entry of entries) {
|
||||||
|
const value = entry.payload.value;
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = filtered.length;
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
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) {
|
||||||
|
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<boolean> {
|
||||||
|
logger.warn(`Index creation not supported for feed collections, ignoring request for ${collection}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
149
src/db/stores/fileStore.ts
Normal file
149
src/db/stores/fileStore.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { createServiceLogger } from '../../utils/logger';
|
||||||
|
import { ErrorCode, StoreType, FileUploadResult, FileResult } from '../types';
|
||||||
|
import { DBError } from '../core/error';
|
||||||
|
import { getConnection } from '../core/connection';
|
||||||
|
import { openStore } from './baseStore';
|
||||||
|
import { getHelia } from '../../ipfs/ipfsService';
|
||||||
|
import { measurePerformance } from '../metrics/metricsService';
|
||||||
|
|
||||||
|
// Helper function to convert AsyncIterable to Buffer
|
||||||
|
async function readAsyncIterableToBuffer(asyncIterable: AsyncIterable<Uint8Array>): Promise<Buffer> {
|
||||||
|
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<string, any>;
|
||||||
|
}
|
||||||
|
): Promise<FileUploadResult> => {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const connection = getConnection(options?.connectionId);
|
||||||
|
const ipfs = getHelia();
|
||||||
|
if (!ipfs) {
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, 'IPFS instance not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to IPFS
|
||||||
|
const blockstore = ipfs.blockstore;
|
||||||
|
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) {
|
||||||
|
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,
|
||||||
|
options?: { connectionId?: string }
|
||||||
|
): Promise<FileResult> => {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const connection = getConnection(options?.connectionId);
|
||||||
|
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) {
|
||||||
|
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,
|
||||||
|
options?: { connectionId?: string }
|
||||||
|
): Promise<boolean> => {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const connection = getConnection(options?.connectionId);
|
||||||
|
|
||||||
|
// Delete metadata
|
||||||
|
try {
|
||||||
|
const filesDb = await openStore('_files', StoreType.KEYVALUE);
|
||||||
|
await filesDb.del(cid);
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore if metadata doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// In IPFS we can't really delete files, but we can remove them from our local blockstore
|
||||||
|
// and they will eventually be garbage collected if no one else has pinned them
|
||||||
|
logger.info(`Deleted file with CID: ${cid}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
335
src/db/stores/keyValueStore.ts
Normal file
335
src/db/stores/keyValueStore.ts
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
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 cache from '../cache/cacheService';
|
||||||
|
import * as events from '../events/eventService';
|
||||||
|
import { measurePerformance } from '../metrics/metricsService';
|
||||||
|
|
||||||
|
const logger = createServiceLogger('KEYVALUE_STORE');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyValue Store implementation
|
||||||
|
*/
|
||||||
|
export class KeyValueStore implements BaseStore {
|
||||||
|
/**
|
||||||
|
* Create a new document in the specified collection
|
||||||
|
*/
|
||||||
|
async create<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Omit<T, 'createdAt' | 'updatedAt'>,
|
||||||
|
options?: StoreOptions
|
||||||
|
): Promise<CreateResult> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.KEYVALUE, options);
|
||||||
|
|
||||||
|
// Prepare document for storage
|
||||||
|
const document = prepareDocument<T>(collection, data);
|
||||||
|
|
||||||
|
// Add to database
|
||||||
|
const hash = await db.put(id, document);
|
||||||
|
|
||||||
|
// Add to cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
|
||||||
|
// Emit change event
|
||||||
|
events.emit('document:created', { collection, id, document });
|
||||||
|
|
||||||
|
logger.info(`Created document in ${collection} with id ${id}`);
|
||||||
|
return { id, hash };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error creating document in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to create document in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a document by ID from a collection
|
||||||
|
*/
|
||||||
|
async get<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: StoreOptions & { skipCache?: boolean }
|
||||||
|
): Promise<T | null> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
// Check cache first if not skipped
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
if (!options?.skipCache) {
|
||||||
|
const cachedDocument = cache.get<T>(cacheKey);
|
||||||
|
if (cachedDocument) {
|
||||||
|
return cachedDocument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await openStore(collection, StoreType.KEYVALUE, options);
|
||||||
|
const document = await db.get(id) as T | null;
|
||||||
|
|
||||||
|
// Update cache if document exists
|
||||||
|
if (document) {
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return document;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error getting document ${id} from ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to get document ${id} from ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a document in a collection
|
||||||
|
*/
|
||||||
|
async update<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Partial<Omit<T, 'createdAt' | 'updatedAt'>>,
|
||||||
|
options?: StoreOptions & { upsert?: boolean }
|
||||||
|
): Promise<UpdateResult> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.KEYVALUE, options);
|
||||||
|
const existing = await db.get(id) as T | null;
|
||||||
|
|
||||||
|
if (!existing && !options?.upsert) {
|
||||||
|
throw new DBError(
|
||||||
|
ErrorCode.DOCUMENT_NOT_FOUND,
|
||||||
|
`Document ${id} not found in ${collection}`,
|
||||||
|
{ collection, id }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare document for update
|
||||||
|
const document = prepareDocument<T>(collection, data as unknown as Omit<T, "createdAt" | "updatedAt">, existing || undefined);
|
||||||
|
|
||||||
|
// Update in database
|
||||||
|
const hash = await db.put(id, document);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.set(cacheKey, document);
|
||||||
|
|
||||||
|
// Emit change event
|
||||||
|
events.emit('document:updated', { collection, id, document, previous: existing });
|
||||||
|
|
||||||
|
logger.info(`Updated document in ${collection} with id ${id}`);
|
||||||
|
return { id, hash };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error updating document in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to update document in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a document from a collection
|
||||||
|
*/
|
||||||
|
async remove(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: StoreOptions
|
||||||
|
): Promise<boolean> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.KEYVALUE, options);
|
||||||
|
|
||||||
|
// Get the document before deleting for the event
|
||||||
|
const document = await db.get(id);
|
||||||
|
|
||||||
|
// Delete from database
|
||||||
|
await db.del(id);
|
||||||
|
|
||||||
|
// Remove from cache
|
||||||
|
const cacheKey = `${collection}:${id}`;
|
||||||
|
cache.del(cacheKey);
|
||||||
|
|
||||||
|
// Emit change event
|
||||||
|
events.emit('document:deleted', { collection, id, document });
|
||||||
|
|
||||||
|
logger.info(`Deleted document in ${collection} with id ${id}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error deleting document in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to delete document in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all documents in a collection with pagination
|
||||||
|
*/
|
||||||
|
async list<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
options?: ListOptions
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.KEYVALUE, options);
|
||||||
|
const all = await db.all();
|
||||||
|
|
||||||
|
let documents = Object.entries(all).map(([key, value]) => ({
|
||||||
|
id: key,
|
||||||
|
...(value as any)
|
||||||
|
})) 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = documents.length;
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
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
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error listing documents in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to list documents in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query documents in a collection with filtering and pagination
|
||||||
|
*/
|
||||||
|
async query<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
filter: (doc: T) => boolean,
|
||||||
|
options?: QueryOptions
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
return measurePerformance(async () => {
|
||||||
|
try {
|
||||||
|
const db = await openStore(collection, StoreType.KEYVALUE, 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[];
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = filtered.length;
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
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) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error querying documents in ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to query documents in ${collection}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an index for a collection to speed up queries
|
||||||
|
*/
|
||||||
|
async createIndex(
|
||||||
|
collection: string,
|
||||||
|
field: string,
|
||||||
|
options?: StoreOptions
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// In a real implementation, this would register the index with OrbitDB
|
||||||
|
// or create a specialized data structure. For now, we'll just log the request.
|
||||||
|
logger.info(`Index created on ${field} for collection ${collection}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DBError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error creating index for ${collection}:`, error);
|
||||||
|
throw new DBError(ErrorCode.OPERATION_FAILED, `Failed to create index for ${collection}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
src/db/stores/storeFactory.ts
Normal file
54
src/db/stores/storeFactory.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
const storeInstances = new Map<StoreType, BaseStore>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a store instance by type
|
||||||
|
*/
|
||||||
|
export function getStore(type: StoreType): BaseStore {
|
||||||
|
// Check if we already have an instance
|
||||||
|
if (storeInstances.has(type)) {
|
||||||
|
return storeInstances.get(type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new instance based on type
|
||||||
|
let store: BaseStore;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case StoreType.KEYVALUE:
|
||||||
|
store = new KeyValueStore();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case StoreType.DOCSTORE:
|
||||||
|
store = new DocStore();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case StoreType.FEED:
|
||||||
|
case StoreType.EVENTLOG: // Alias for feed
|
||||||
|
store = new FeedStore();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case StoreType.COUNTER:
|
||||||
|
store = new CounterStore();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.error(`Unsupported store type: ${type}`);
|
||||||
|
throw new DBError(ErrorCode.STORE_TYPE_ERROR, `Unsupported store type: ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the instance
|
||||||
|
storeInstances.set(type, store);
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
77
src/db/transactions/transactionService.ts
Normal file
77
src/db/transactions/transactionService.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { createServiceLogger } from '../../utils/logger';
|
||||||
|
import { ErrorCode } from '../types';
|
||||||
|
import { DBError } from '../core/error';
|
||||||
|
|
||||||
|
const logger = createServiceLogger('DB_TRANSACTION');
|
||||||
|
|
||||||
|
// 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<T>(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<T>(collection: string, id: string, data: Partial<T>): 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;
|
||||||
|
}
|
||||||
|
}
|
132
src/db/types/index.ts
Normal file
132
src/db/types/index.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// Common types for database operations
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { Transaction } from '../transactions/transactionService';
|
||||||
|
|
||||||
|
export type { Transaction };
|
||||||
|
|
||||||
|
// Database Types
|
||||||
|
export enum StoreType {
|
||||||
|
KEYVALUE = 'keyvalue',
|
||||||
|
DOCSTORE = 'docstore',
|
||||||
|
FEED = 'feed',
|
||||||
|
EVENTLOG = 'eventlog',
|
||||||
|
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<T> {
|
||||||
|
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<string, SchemaDefinition>; // For objects
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollectionSchema {
|
||||||
|
properties: Record<string, SchemaDefinition>;
|
||||||
|
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<string, number>;
|
||||||
|
};
|
||||||
|
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();
|
178
src/index.ts
178
src/index.ts
@ -2,97 +2,149 @@
|
|||||||
import { config, defaultConfig, type DebrosConfig } from './config';
|
import { config, defaultConfig, type DebrosConfig } from './config';
|
||||||
import { validateConfig, type ValidationResult } from './ipfs/config/configValidator';
|
import { validateConfig, type ValidationResult } from './ipfs/config/configValidator';
|
||||||
|
|
||||||
// IPFS exports
|
// Database service exports (new abstracted layer)
|
||||||
import ipfsService, {
|
import {
|
||||||
init as initIpfs,
|
init as initDB,
|
||||||
stop as stopIpfs,
|
create,
|
||||||
getHelia,
|
get,
|
||||||
getProxyAgent,
|
update,
|
||||||
getInstance,
|
remove,
|
||||||
getLibp2p,
|
list,
|
||||||
getConnectedPeers,
|
query,
|
||||||
getOptimalPeer,
|
createIndex,
|
||||||
updateNodeLoad,
|
createTransaction,
|
||||||
logPeersStatus,
|
commitTransaction,
|
||||||
type IPFSModule,
|
subscribe,
|
||||||
} from './ipfs/ipfsService';
|
uploadFile,
|
||||||
|
getFile,
|
||||||
|
deleteFile,
|
||||||
|
defineSchema,
|
||||||
|
getMetrics,
|
||||||
|
resetMetrics,
|
||||||
|
closeConnection,
|
||||||
|
stop as stopDB,
|
||||||
|
} from './db/dbService';
|
||||||
|
import { ErrorCode, StoreType } from './db/types';
|
||||||
|
|
||||||
import { ipfsConfig, getIpfsPort, getBlockstorePath } from './ipfs/config/ipfsConfig';
|
// Import types
|
||||||
|
import type {
|
||||||
|
Transaction,
|
||||||
|
CreateResult,
|
||||||
|
UpdateResult,
|
||||||
|
PaginatedResult,
|
||||||
|
ListOptions,
|
||||||
|
QueryOptions,
|
||||||
|
FileUploadResult,
|
||||||
|
FileResult,
|
||||||
|
CollectionSchema,
|
||||||
|
SchemaDefinition,
|
||||||
|
Metrics,
|
||||||
|
} from './db/types';
|
||||||
|
|
||||||
// OrbitDB exports
|
import { DBError } from './db/core/error';
|
||||||
import orbitDBService, {
|
|
||||||
init as initOrbitDB,
|
|
||||||
openDB,
|
|
||||||
getOrbitDB,
|
|
||||||
db as orbitDB,
|
|
||||||
getOrbitDBDir,
|
|
||||||
getDBAddress,
|
|
||||||
saveDBAddress,
|
|
||||||
} from './orbit/orbitDBService';
|
|
||||||
|
|
||||||
import loadBalancerControllerDefault from './ipfs/loadBalancerController';
|
// Legacy exports (internal use only, not exposed in default export)
|
||||||
export const loadBalancerController = loadBalancerControllerDefault;
|
import { getConnectedPeers, logPeersStatus } from './ipfs/ipfsService';
|
||||||
|
|
||||||
|
// Load balancer exports
|
||||||
|
import loadBalancerController from './ipfs/loadBalancerController';
|
||||||
|
|
||||||
// Logger exports
|
// Logger exports
|
||||||
import logger, { createServiceLogger, createDebrosLogger, type LoggerOptions } from './utils/logger';
|
import logger, {
|
||||||
|
createServiceLogger,
|
||||||
|
createDebrosLogger,
|
||||||
|
type LoggerOptions,
|
||||||
|
} from './utils/logger';
|
||||||
|
|
||||||
// Crypto exports
|
// Export public API
|
||||||
import { getPrivateKey } from './ipfs/utils/crypto';
|
|
||||||
|
|
||||||
// Export everything
|
|
||||||
export {
|
export {
|
||||||
// Config
|
// Configuration
|
||||||
config,
|
config,
|
||||||
defaultConfig,
|
defaultConfig,
|
||||||
validateConfig,
|
validateConfig,
|
||||||
type DebrosConfig,
|
type DebrosConfig,
|
||||||
type ValidationResult,
|
type ValidationResult,
|
||||||
|
|
||||||
// IPFS
|
// Database Service (Main public API)
|
||||||
ipfsService,
|
initDB,
|
||||||
initIpfs,
|
create,
|
||||||
stopIpfs,
|
get,
|
||||||
getHelia,
|
update,
|
||||||
getProxyAgent,
|
remove,
|
||||||
getInstance,
|
list,
|
||||||
getLibp2p,
|
query,
|
||||||
|
createIndex,
|
||||||
|
createTransaction,
|
||||||
|
commitTransaction,
|
||||||
|
subscribe,
|
||||||
|
uploadFile,
|
||||||
|
getFile,
|
||||||
|
deleteFile,
|
||||||
|
defineSchema,
|
||||||
|
getMetrics,
|
||||||
|
resetMetrics,
|
||||||
|
closeConnection,
|
||||||
|
stopDB,
|
||||||
|
ErrorCode,
|
||||||
|
StoreType,
|
||||||
|
|
||||||
|
// Load Balancer
|
||||||
|
loadBalancerController,
|
||||||
getConnectedPeers,
|
getConnectedPeers,
|
||||||
getOptimalPeer,
|
|
||||||
updateNodeLoad,
|
|
||||||
logPeersStatus,
|
logPeersStatus,
|
||||||
type IPFSModule,
|
|
||||||
|
|
||||||
// IPFS Config
|
// Types
|
||||||
ipfsConfig,
|
type Transaction,
|
||||||
getIpfsPort,
|
type DBError,
|
||||||
getBlockstorePath,
|
type CollectionSchema,
|
||||||
|
type SchemaDefinition,
|
||||||
// OrbitDB
|
type CreateResult,
|
||||||
orbitDBService,
|
type UpdateResult,
|
||||||
initOrbitDB,
|
type PaginatedResult,
|
||||||
openDB,
|
type ListOptions,
|
||||||
getOrbitDB,
|
type QueryOptions,
|
||||||
orbitDB,
|
type FileUploadResult,
|
||||||
getOrbitDBDir,
|
type FileResult,
|
||||||
getDBAddress,
|
type Metrics,
|
||||||
saveDBAddress,
|
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
logger,
|
logger,
|
||||||
createServiceLogger,
|
createServiceLogger,
|
||||||
createDebrosLogger,
|
createDebrosLogger,
|
||||||
type LoggerOptions,
|
type LoggerOptions,
|
||||||
|
|
||||||
// Crypto
|
|
||||||
getPrivateKey,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default export for convenience
|
// Default export for convenience
|
||||||
export default {
|
export default {
|
||||||
config,
|
config,
|
||||||
validateConfig,
|
validateConfig,
|
||||||
ipfsService,
|
// Database Service as main interface
|
||||||
orbitDBService,
|
db: {
|
||||||
|
init: initDB,
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
list,
|
||||||
|
query,
|
||||||
|
createIndex,
|
||||||
|
createTransaction,
|
||||||
|
commitTransaction,
|
||||||
|
subscribe,
|
||||||
|
uploadFile,
|
||||||
|
getFile,
|
||||||
|
deleteFile,
|
||||||
|
defineSchema,
|
||||||
|
getMetrics,
|
||||||
|
resetMetrics,
|
||||||
|
closeConnection,
|
||||||
|
stop: stopDB,
|
||||||
|
ErrorCode,
|
||||||
|
StoreType,
|
||||||
|
},
|
||||||
|
loadBalancerController,
|
||||||
|
logPeersStatus,
|
||||||
|
getConnectedPeers,
|
||||||
logger,
|
logger,
|
||||||
createServiceLogger,
|
createServiceLogger,
|
||||||
};
|
};
|
||||||
|
@ -74,7 +74,7 @@ export const openDB = async (name: string, type: string) => {
|
|||||||
const dbOptions = {
|
const dbOptions = {
|
||||||
type,
|
type,
|
||||||
overwrite: false,
|
overwrite: false,
|
||||||
AccessController: IPFSAccessController({ write: ['*'], storage: getHelia() }),
|
AccessController: IPFSAccessController({ write: ['*'] }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingAddress) {
|
if (existingAddress) {
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
// ]
|
// ]
|
||||||
// },
|
// },
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "orbitdb.d.ts"],
|
"include": ["src/**/*", "orbitdb.d.ts", "types.d.ts"],
|
||||||
"exclude": ["coverage", "dist", "eslint.config.js", "node_modules"],
|
"exclude": ["coverage", "dist", "eslint.config.js", "node_modules"],
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"esm": true
|
"esm": true
|
||||||
|
336
types.d.ts
vendored
336
types.d.ts
vendored
@ -2,11 +2,15 @@
|
|||||||
// Project: https://github.com/debros/anchat-relay
|
// Project: https://github.com/debros/anchat-relay
|
||||||
// Definitions by: Debros Team
|
// Definitions by: Debros Team
|
||||||
|
|
||||||
declare module "@debros/network" {
|
declare module '@debros/network' {
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
// Config types
|
// Config types
|
||||||
export interface DebrosConfig {
|
export interface DebrosConfig {
|
||||||
|
env: {
|
||||||
|
fingerprint: string;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
ipfs: {
|
ipfs: {
|
||||||
swarm: {
|
swarm: {
|
||||||
port: number;
|
port: number;
|
||||||
@ -15,9 +19,15 @@ declare module "@debros/network" {
|
|||||||
connectAddresses: string[];
|
connectAddresses: string[];
|
||||||
};
|
};
|
||||||
blockstorePath: string;
|
blockstorePath: string;
|
||||||
orbitdbPath: string;
|
|
||||||
bootstrap: string[];
|
bootstrap: string[];
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
|
serviceDiscovery?: {
|
||||||
|
topic: string;
|
||||||
|
heartbeatInterval: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
orbitdb: {
|
||||||
|
directory: string;
|
||||||
};
|
};
|
||||||
logger: {
|
logger: {
|
||||||
level: string;
|
level: string;
|
||||||
@ -33,66 +43,216 @@ declare module "@debros/network" {
|
|||||||
// Core configuration
|
// Core configuration
|
||||||
export const config: DebrosConfig;
|
export const config: DebrosConfig;
|
||||||
export const defaultConfig: DebrosConfig;
|
export const defaultConfig: DebrosConfig;
|
||||||
export function validateConfig(
|
export function validateConfig(config: Partial<DebrosConfig>): ValidationResult;
|
||||||
config: Partial<DebrosConfig>
|
|
||||||
): ValidationResult;
|
|
||||||
|
|
||||||
// IPFS types
|
// Store types
|
||||||
export interface IPFSModule {
|
export enum StoreType {
|
||||||
helia: any;
|
KEYVALUE = 'keyvalue',
|
||||||
libp2p: any;
|
DOCSTORE = 'docstore',
|
||||||
|
FEED = 'feed',
|
||||||
|
EVENTLOG = 'eventlog',
|
||||||
|
COUNTER = 'counter',
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPFS Service
|
// Error handling
|
||||||
export const ipfsService: {
|
export enum ErrorCode {
|
||||||
init(): Promise<IPFSModule>;
|
NOT_INITIALIZED = 'ERR_NOT_INITIALIZED',
|
||||||
stop(): Promise<void>;
|
INITIALIZATION_FAILED = 'ERR_INIT_FAILED',
|
||||||
};
|
DOCUMENT_NOT_FOUND = 'ERR_DOC_NOT_FOUND',
|
||||||
export function initIpfs(): Promise<IPFSModule>;
|
INVALID_SCHEMA = 'ERR_INVALID_SCHEMA',
|
||||||
export function stopIpfs(): Promise<void>;
|
OPERATION_FAILED = 'ERR_OPERATION_FAILED',
|
||||||
export function getHelia(): any;
|
TRANSACTION_FAILED = 'ERR_TRANSACTION_FAILED',
|
||||||
export function getProxyAgent(): any;
|
FILE_NOT_FOUND = 'ERR_FILE_NOT_FOUND',
|
||||||
export function getInstance(): IPFSModule;
|
INVALID_PARAMETERS = 'ERR_INVALID_PARAMS',
|
||||||
export function getLibp2p(): any;
|
CONNECTION_ERROR = 'ERR_CONNECTION',
|
||||||
export function getConnectedPeers(): any[];
|
STORE_TYPE_ERROR = 'ERR_STORE_TYPE',
|
||||||
export function getOptimalPeer(): any;
|
|
||||||
export function updateNodeLoad(load: number): void;
|
|
||||||
export function logPeersStatus(): void;
|
|
||||||
|
|
||||||
// IPFS Config
|
|
||||||
export const ipfsConfig: any;
|
|
||||||
export function getIpfsPort(): number;
|
|
||||||
export function getBlockstorePath(): string;
|
|
||||||
|
|
||||||
// LoadBalancerController interface and value declaration
|
|
||||||
export interface LoadBalancerController {
|
|
||||||
getNodeInfo: (_req: Request, _res: Response, _next: NextFunction) => void;
|
|
||||||
getOptimalPeer: (
|
|
||||||
_req: Request,
|
|
||||||
_res: Response,
|
|
||||||
_next: NextFunction
|
|
||||||
) => void;
|
|
||||||
getAllPeers: (_req: Request, _res: Response, _next: NextFunction) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declare loadBalancerController as a value
|
export class DBError extends Error {
|
||||||
export const loadBalancerController: LoadBalancerController;
|
code: ErrorCode;
|
||||||
|
details?: any;
|
||||||
|
constructor(code: ErrorCode, message: string, details?: any);
|
||||||
|
}
|
||||||
|
|
||||||
// OrbitDB
|
// Schema validation
|
||||||
export const orbitDBService: {
|
export interface SchemaDefinition {
|
||||||
init(): Promise<any>;
|
type: string;
|
||||||
};
|
required?: boolean;
|
||||||
export function initOrbitDB(): Promise<any>;
|
pattern?: string;
|
||||||
export function openDB(
|
min?: number;
|
||||||
dbName: string,
|
max?: number;
|
||||||
dbType: string,
|
enum?: any[];
|
||||||
options?: any
|
items?: SchemaDefinition; // For arrays
|
||||||
): Promise<any>;
|
properties?: Record<string, SchemaDefinition>; // For objects
|
||||||
export function getOrbitDB(): any;
|
}
|
||||||
export const orbitDB: any;
|
|
||||||
export function getOrbitDBDir(): string;
|
export interface CollectionSchema {
|
||||||
export function getDBAddress(dbName: string): string | null;
|
properties: Record<string, SchemaDefinition>;
|
||||||
export function saveDBAddress(dbName: string, address: string): void;
|
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<T> {
|
||||||
|
documents: T[];
|
||||||
|
total: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction API
|
||||||
|
export class Transaction {
|
||||||
|
create<T>(collection: string, id: string, data: T): Transaction;
|
||||||
|
update<T>(collection: string, id: string, data: Partial<T>): 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<string, number>;
|
||||||
|
};
|
||||||
|
cacheStats: {
|
||||||
|
hits: number;
|
||||||
|
misses: number;
|
||||||
|
};
|
||||||
|
startTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database Operations
|
||||||
|
export function initDB(connectionId?: string): Promise<string>;
|
||||||
|
export function create<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Omit<T, 'createdAt' | 'updatedAt'>,
|
||||||
|
options?: { connectionId?: string; storeType?: StoreType },
|
||||||
|
): Promise<CreateResult>;
|
||||||
|
export function get<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: { connectionId?: string; skipCache?: boolean; storeType?: StoreType },
|
||||||
|
): Promise<T | null>;
|
||||||
|
export function update<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
data: Partial<Omit<T, 'createdAt' | 'updatedAt'>>,
|
||||||
|
options?: { connectionId?: string; upsert?: boolean; storeType?: StoreType },
|
||||||
|
): Promise<UpdateResult>;
|
||||||
|
export function remove(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
options?: { connectionId?: string; storeType?: StoreType },
|
||||||
|
): Promise<boolean>;
|
||||||
|
export function list<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
options?: ListOptions,
|
||||||
|
): Promise<PaginatedResult<T>>;
|
||||||
|
export function query<T extends Record<string, any>>(
|
||||||
|
collection: string,
|
||||||
|
filter: (doc: T) => boolean,
|
||||||
|
options?: QueryOptions,
|
||||||
|
): Promise<PaginatedResult<T>>;
|
||||||
|
|
||||||
|
// 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<boolean>;
|
||||||
|
|
||||||
|
// Subscription API
|
||||||
|
export function subscribe(
|
||||||
|
event: 'document:created' | 'document:updated' | 'document:deleted',
|
||||||
|
callback: (data: any) => void,
|
||||||
|
): () => void;
|
||||||
|
|
||||||
|
// File operations
|
||||||
|
export function uploadFile(
|
||||||
|
fileData: Buffer,
|
||||||
|
options?: { filename?: string; connectionId?: string; metadata?: Record<string, any> },
|
||||||
|
): Promise<FileUploadResult>;
|
||||||
|
export function getFile(cid: string, options?: { connectionId?: string }): Promise<FileResult>;
|
||||||
|
export function deleteFile(cid: string, options?: { connectionId?: string }): Promise<boolean>;
|
||||||
|
|
||||||
|
// Connection management
|
||||||
|
export function closeConnection(connectionId: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
export function getMetrics(): Metrics;
|
||||||
|
export function resetMetrics(): void;
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
export function stopDB(): Promise<void>;
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
export interface LoggerOptions {
|
export interface LoggerOptions {
|
||||||
@ -101,21 +261,67 @@ declare module "@debros/network" {
|
|||||||
service?: string;
|
service?: string;
|
||||||
}
|
}
|
||||||
export const logger: any;
|
export const logger: any;
|
||||||
export function createServiceLogger(
|
export function createServiceLogger(name: string, options?: LoggerOptions): any;
|
||||||
name: string,
|
|
||||||
options?: LoggerOptions
|
|
||||||
): any;
|
|
||||||
export function createDebrosLogger(options?: LoggerOptions): any;
|
export function createDebrosLogger(options?: LoggerOptions): any;
|
||||||
|
|
||||||
// Crypto
|
// Load Balancer
|
||||||
export function getPrivateKey(): Promise<string>;
|
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
|
// Default export
|
||||||
const defaultExport: {
|
const defaultExport: {
|
||||||
config: DebrosConfig;
|
config: DebrosConfig;
|
||||||
validateConfig: typeof validateConfig;
|
validateConfig: typeof validateConfig;
|
||||||
ipfsService: typeof ipfsService;
|
db: {
|
||||||
orbitDBService: typeof orbitDBService;
|
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;
|
||||||
|
getMetrics: typeof getMetrics;
|
||||||
|
resetMetrics: typeof resetMetrics;
|
||||||
|
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;
|
logger: any;
|
||||||
createServiceLogger: typeof createServiceLogger;
|
createServiceLogger: typeof createServiceLogger;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user