BREAKING CHANGE(decorators): Migrate to TC39 Stage 3 decorators and refactor decorator metadata handling; update class initialization, lucene adapter fixes and docs

This commit is contained in:
2025-11-17 12:51:45 +00:00
parent d254f58a05
commit 1cd0f09598
19 changed files with 451 additions and 364 deletions

105
readme.md
View File

@@ -8,7 +8,7 @@
SmartData isn't just another MongoDB wrapper - it's a complete paradigm shift in how you work with databases:
- 🔒 **100% Type-Safe**: Full TypeScript with decorators, generics, and compile-time query validation
- 🔒 **100% Type-Safe**: Full TypeScript with TC39 Stage 3 decorators, generics, and compile-time query validation
-**Lightning Fast**: Connection pooling, cursor streaming, and intelligent indexing
- 🔄 **Real-time Ready**: MongoDB Change Streams with RxJS for reactive applications
- 🌍 **Distributed Systems**: Built-in leader election and task coordination
@@ -31,8 +31,11 @@ yarn add @push.rocks/smartdata
## 🚦 Requirements
- **Node.js** >= 16.x
- **Deno** >= 1.40 (for Deno projects)
- **MongoDB** >= 4.4
- **TypeScript** >= 4.x (for development)
- **TypeScript** >= 5.2 (for TC39 decorator support)
> **Note**: SmartData v6.0+ uses TC39 Stage 3 decorators (the new standard). If you're migrating from v5.x, you'll need to remove `experimentalDecorators` from your tsconfig.json. Bun is not currently supported as it doesn't implement TC39 decorators yet.
## 🎯 Quick Start
@@ -47,7 +50,7 @@ const db = new SmartdataDb({
mongoDbName: 'myapp',
mongoDbUser: 'username',
mongoDbPass: 'password',
// Optional: Advanced connection pooling
maxPoolSize: 100, // Max connections in pool
maxIdleTimeMS: 300000, // Max idle time before connection close
@@ -75,38 +78,38 @@ import { ObjectId } from 'mongodb';
class User extends SmartDataDbDoc<User, User> {
@unI()
public id: string; // Unique index with automatic ID generation
@svDb()
@searchable() // Enable Lucene-style searching
public username: string;
@svDb()
@searchable()
@index({ unique: false }) // Performance index
public email: string;
@svDb()
public status: 'active' | 'inactive' | 'pending'; // Full union type support
@svDb()
public organizationId: ObjectId; // Native MongoDB types
@svDb()
public profilePicture: Buffer; // Binary data support
@svDb({
// Custom serialization for complex objects
serialize: (data) => JSON.stringify(data),
deserialize: (data) => JSON.parse(data),
})
public preferences: Record<string, any>;
@svDb()
public tags: string[]; // Array support with operators
@svDb()
public createdAt: Date = new Date();
constructor(username: string, email: string) {
super();
this.username = username;
@@ -241,7 +244,7 @@ const experts = await User.getInstances({
// Array element matching
const results = await Order.getInstances({
items: {
items: {
$elemMatch: { // Match array elements
product: 'laptop',
quantity: { $gte: 2 }
@@ -342,21 +345,21 @@ const johnUsers = await User.getInstances({
const advancedQuery = await User.getInstances({
// Direct field matching
status: 'active',
// Nested object with operators
profile: {
age: { $gte: 18, $lte: 65 },
verified: true
},
// Dot notation for deep paths
'settings.notifications.email': true,
'metadata.lastLogin': { $gte: new Date(Date.now() - 30*24*60*60*1000) },
// Array operations
roles: { $in: ['admin', 'moderator'] },
tags: { $all: ['verified', 'premium'] },
// Logical grouping
$or: [
{ 'subscription.plan': 'premium' },
@@ -452,7 +455,7 @@ React to database changes instantly with RxJS integration:
// Watch for changes with automatic reconnection
const watcher = await User.watch(
{ status: 'active' }, // Filter which documents to watch
{
{
fullDocument: 'updateLookup', // Get full document on updates
bufferTimeMs: 100 // Buffer changes for efficiency
}
@@ -462,7 +465,7 @@ const watcher = await User.watch(
watcher.changeSubject.subscribe({
next: (change) => {
console.log('User changed:', change.fullDocument);
switch(change.operationType) {
case 'insert':
console.log('New user created');
@@ -570,27 +573,27 @@ try {
await session.withTransaction(async () => {
// All operations in this block are atomic
const sender = await User.getInstance(
{ id: 'user-1' },
{ id: 'user-1' },
{ session } // Pass session to all operations
);
sender.balance -= 100;
await sender.save({ session });
const receiver = await User.getInstance(
{ id: 'user-2' },
{ id: 'user-2' },
{ session }
);
receiver.balance += 100;
await receiver.save({ session });
// If anything fails, everything rolls back
if (sender.balance < 0) {
throw new Error('Insufficient funds!');
}
});
console.log('✅ Transaction completed');
} finally {
await session.endSession();
@@ -609,21 +612,21 @@ class Document extends SmartDataDbDoc<Document, Document> {
deserialize: async (value) => await decrypt(value)
})
public sensitiveData: string;
@svDb({
// Compress large JSON objects
serialize: (value) => compress(JSON.stringify(value)),
deserialize: (value) => JSON.parse(decompress(value))
})
public largePayload: any;
@svDb({
// Store Sets as arrays
serialize: (set) => Array.from(set),
deserialize: (arr) => new Set(arr)
})
public tags: Set<string>;
@svDb({
// Handle custom date formats
serialize: (date) => date?.toISOString(),
@@ -644,45 +647,45 @@ class Order extends SmartDataDbDoc<Order, Order> {
@svDb() public items: Array<{ product: string; quantity: number }>;
@svDb() public totalAmount: number;
@svDb() public status: string;
// Called before saving (create or update)
async beforeSave() {
// Recalculate total
this.totalAmount = this.items.reduce((sum, item) =>
this.totalAmount = this.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
// Validate
if (this.totalAmount < 0) {
throw new Error('Invalid order total');
}
}
// Called after successful save
async afterSave() {
// Send notifications
await notificationService.orderUpdated(this.id);
// Update cache
await cache.set(`order:${this.id}`, this);
}
// Called before deletion
async beforeDelete() {
// Archive order
await archive.store(this);
// Check if deletion is allowed
if (this.status === 'completed') {
throw new Error('Cannot delete completed orders');
}
}
// Called after successful deletion
async afterDelete() {
// Clean up related data
await cache.delete(`order:${this.id}`);
// Log deletion
console.log(`Order ${this.id} deleted at ${new Date()}`);
}
@@ -698,27 +701,27 @@ class Order extends SmartDataDbDoc<Order, Order> {
class HighPerformanceDoc extends SmartDataDbDoc<HighPerformanceDoc, HighPerformanceDoc> {
@unI() // Unique index
public id: string;
@index() // Single field index
public userId: string;
@index({ sparse: true }) // Sparse index for optional fields
public deletedAt?: Date;
@index({
@index({
unique: false,
background: true, // Non-blocking index creation
expireAfterSeconds: 86400 // TTL index
})
public sessionToken: string;
// Compound indexes for complex queries
static async createIndexes() {
await this.collection.createIndex(
{ userId: 1, createdAt: -1 }, // Compound index
{ name: 'user_activity_idx' }
);
// Text index for search
await this.collection.createIndex(
{ title: 'text', content: 'text' },
@@ -734,17 +737,17 @@ class HighPerformanceDoc extends SmartDataDbDoc<HighPerformanceDoc, HighPerforma
const db = new SmartdataDb({
mongoDbUrl: 'mongodb://localhost:27017',
mongoDbName: 'myapp',
// Connection pool optimization
maxPoolSize: 100, // Maximum connections
minPoolSize: 10, // Minimum connections to maintain
maxIdleTimeMS: 300000, // Close idle connections after 5 minutes
waitQueueTimeoutMS: 5000, // Max time to wait for available connection
// Server selection
serverSelectionTimeoutMS: 30000, // Timeout for selecting a server
heartbeatFrequencyMS: 10000, // How often to check server status
// Socket settings
socketTimeoutMS: 360000, // Socket timeout (6 minutes)
family: 4, // Force IPv4
@@ -874,7 +877,7 @@ await db.mongoDb.setProfilingLevel('all');
const stats = await User.collection.mongoDbCollection
.find({ complex: 'query' })
.explain('executionStats');
console.log('Query execution time:', stats.executionTimeMillis);
console.log('Documents examined:', stats.totalDocsExamined);
console.log('Index used:', stats.executionStats.executionStages.indexName);
@@ -918,7 +921,7 @@ console.log('Index used:', stats.executionStats.executionStages.indexName);
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
@@ -928,9 +931,9 @@ This project is owned and maintained by Task Venture Capital GmbH. The names and
### Company Information
Task Venture Capital GmbH
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.