fix(build): update build and test tooling configuration, migrate project config to .smartconfig.json, and align TypeScript typings
This commit is contained in:
216
readme.md
216
readme.md
@@ -52,9 +52,9 @@ const db = new SmartdataDb({
|
||||
mongoDbPass: 'password',
|
||||
|
||||
// Optional: Advanced connection pooling
|
||||
maxPoolSize: 100, // Max connections in pool
|
||||
maxIdleTimeMS: 300000, // Max idle time before connection close
|
||||
serverSelectionTimeoutMS: 30000 // Connection timeout
|
||||
maxPoolSize: 100, // Max connections in pool
|
||||
maxIdleTimeMS: 300000, // Max idle time before connection close
|
||||
serverSelectionTimeoutMS: 30000, // Connection timeout
|
||||
});
|
||||
|
||||
// Initialize with automatic retry and health monitoring
|
||||
@@ -77,25 +77,25 @@ import { ObjectId } from 'mongodb';
|
||||
@Collection(() => db)
|
||||
class User extends SmartDataDbDoc<User, User> {
|
||||
@unI()
|
||||
public id: string; // Unique index with automatic ID generation
|
||||
public id: string; // Unique index with automatic ID generation
|
||||
|
||||
@svDb()
|
||||
@searchable() // Enable Lucene-style searching
|
||||
@searchable() // Enable Lucene-style searching
|
||||
public username: string;
|
||||
|
||||
@svDb()
|
||||
@searchable()
|
||||
@index({ unique: false }) // Performance index
|
||||
@index({ unique: false }) // Performance index
|
||||
public email: string;
|
||||
|
||||
@svDb()
|
||||
public status: 'active' | 'inactive' | 'pending'; // Full union type support
|
||||
public status: 'active' | 'inactive' | 'pending'; // Full union type support
|
||||
|
||||
@svDb()
|
||||
public organizationId: ObjectId; // Native MongoDB types
|
||||
public organizationId: ObjectId; // Native MongoDB types
|
||||
|
||||
@svDb()
|
||||
public profilePicture: Buffer; // Binary data support
|
||||
public profilePicture: Buffer; // Binary data support
|
||||
|
||||
@svDb({
|
||||
// Custom serialization for complex objects
|
||||
@@ -105,7 +105,7 @@ class User extends SmartDataDbDoc<User, User> {
|
||||
public preferences: Record<string, any>;
|
||||
|
||||
@svDb()
|
||||
public tags: string[]; // Array support with operators
|
||||
public tags: string[]; // Array support with operators
|
||||
|
||||
@svDb()
|
||||
public createdAt: Date = new Date();
|
||||
@@ -156,12 +156,12 @@ const john = await User.getInstances({ name: 'John Doe' });
|
||||
// Multiple fields (implicit AND)
|
||||
const activeAdults = await User.getInstances({
|
||||
status: 'active',
|
||||
age: { $gte: 18 }
|
||||
age: { $gte: 18 },
|
||||
});
|
||||
|
||||
// Union types work perfectly
|
||||
const users = await User.getInstances({
|
||||
status: { $in: ['active', 'pending'] } // TypeScript validates these values!
|
||||
status: { $in: ['active', 'pending'] }, // TypeScript validates these values!
|
||||
});
|
||||
```
|
||||
|
||||
@@ -173,19 +173,19 @@ SmartData supports **both** nested object notation and dot notation for querying
|
||||
// Nested object notation - natural TypeScript syntax
|
||||
const users = await User.getInstances({
|
||||
metadata: {
|
||||
loginCount: { $gte: 5 }
|
||||
}
|
||||
loginCount: { $gte: 5 },
|
||||
},
|
||||
});
|
||||
|
||||
// Dot notation - MongoDB style
|
||||
const sameUsers = await User.getInstances({
|
||||
'metadata.loginCount': { $gte: 5 }
|
||||
'metadata.loginCount': { $gte: 5 },
|
||||
});
|
||||
|
||||
// POWERFUL: Combine both notations - operators are merged!
|
||||
const filtered = await User.getInstances({
|
||||
metadata: { loginCount: { $gte: 3 } }, // Object notation
|
||||
'metadata.loginCount': { $lte: 10 } // Dot notation
|
||||
metadata: { loginCount: { $gte: 3 } }, // Object notation
|
||||
'metadata.loginCount': { $lte: 10 }, // Dot notation
|
||||
// Result: metadata.loginCount between 3 and 10
|
||||
});
|
||||
|
||||
@@ -195,10 +195,10 @@ const deepQuery = await User.getInstances({
|
||||
settings: {
|
||||
notifications: {
|
||||
email: true,
|
||||
frequency: { $in: ['daily', 'weekly'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
frequency: { $in: ['daily', 'weekly'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mix styles for complex queries
|
||||
@@ -206,11 +206,11 @@ const advanced = await User.getInstances({
|
||||
// Object notation for structure
|
||||
profile: {
|
||||
age: { $gte: 21 },
|
||||
verified: true
|
||||
verified: true,
|
||||
},
|
||||
// Dot notation for specific overrides
|
||||
'profile.settings.theme': 'dark',
|
||||
'profile.lastSeen': { $gte: new Date('2024-01-01') }
|
||||
'profile.lastSeen': { $gte: new Date('2024-01-01') },
|
||||
});
|
||||
```
|
||||
|
||||
@@ -219,17 +219,17 @@ const advanced = await User.getInstances({
|
||||
```typescript
|
||||
// Numeric comparisons with type checking
|
||||
const adults = await User.getInstances({
|
||||
age: { $gte: 18, $lt: 65 } // Type-safe numeric comparisons
|
||||
age: { $gte: 18, $lt: 65 }, // Type-safe numeric comparisons
|
||||
});
|
||||
|
||||
// Date comparisons
|
||||
const recentUsers = await User.getInstances({
|
||||
createdAt: { $gte: new Date('2024-01-01') }
|
||||
createdAt: { $gte: new Date('2024-01-01') },
|
||||
});
|
||||
|
||||
// Not equal
|
||||
const nonAdmins = await User.getInstances({
|
||||
role: { $ne: 'admin' }
|
||||
role: { $ne: 'admin' },
|
||||
});
|
||||
```
|
||||
|
||||
@@ -238,23 +238,24 @@ const nonAdmins = await User.getInstances({
|
||||
```typescript
|
||||
// Array operations with full type safety
|
||||
const experts = await User.getInstances({
|
||||
tags: { $all: ['typescript', 'mongodb'] }, // Must have all tags
|
||||
skills: { $size: 5 } // Exactly 5 skills
|
||||
tags: { $all: ['typescript', 'mongodb'] }, // Must have all tags
|
||||
skills: { $size: 5 }, // Exactly 5 skills
|
||||
});
|
||||
|
||||
// Array element matching
|
||||
const results = await Order.getInstances({
|
||||
items: {
|
||||
$elemMatch: { // Match array elements
|
||||
$elemMatch: {
|
||||
// Match array elements
|
||||
product: 'laptop',
|
||||
quantity: { $gte: 2 }
|
||||
}
|
||||
}
|
||||
quantity: { $gte: 2 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check if value exists in array field
|
||||
const nodeUsers = await User.getInstances({
|
||||
skills: { $in: ['nodejs'] } // Has nodejs in skills array
|
||||
skills: { $in: ['nodejs'] }, // Has nodejs in skills array
|
||||
});
|
||||
```
|
||||
|
||||
@@ -266,24 +267,18 @@ const results = await Order.getInstances({
|
||||
$and: [
|
||||
{ status: { $in: ['pending', 'processing'] } },
|
||||
{ 'items.price': { $gte: 100 } },
|
||||
{ customer: { verified: true } }
|
||||
]
|
||||
{ customer: { verified: true } },
|
||||
],
|
||||
});
|
||||
|
||||
// $or operator
|
||||
const urgentOrHighValue = await Order.getInstances({
|
||||
$or: [
|
||||
{ priority: 'urgent' },
|
||||
{ totalAmount: { $gte: 1000 } }
|
||||
]
|
||||
$or: [{ priority: 'urgent' }, { totalAmount: { $gte: 1000 } }],
|
||||
});
|
||||
|
||||
// $nor operator - none of the conditions
|
||||
const excluded = await User.getInstances({
|
||||
$nor: [
|
||||
{ status: 'banned' },
|
||||
{ role: 'guest' }
|
||||
]
|
||||
$nor: [{ status: 'banned' }, { role: 'guest' }],
|
||||
});
|
||||
|
||||
// Combine logical operators
|
||||
@@ -291,12 +286,9 @@ const complex = await Order.getInstances({
|
||||
$and: [
|
||||
{ status: 'active' },
|
||||
{
|
||||
$or: [
|
||||
{ priority: 'high' },
|
||||
{ value: { $gte: 1000 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
$or: [{ priority: 'high' }, { value: { $gte: 1000 } }],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -305,12 +297,12 @@ const complex = await Order.getInstances({
|
||||
```typescript
|
||||
// Check field existence
|
||||
const withEmail = await User.getInstances({
|
||||
email: { $exists: true }
|
||||
email: { $exists: true },
|
||||
});
|
||||
|
||||
// Check for null or missing nested fields
|
||||
const noPreferences = await User.getInstances({
|
||||
'profile.preferences': { $exists: false }
|
||||
'profile.preferences': { $exists: false },
|
||||
});
|
||||
```
|
||||
|
||||
@@ -319,12 +311,12 @@ const noPreferences = await User.getInstances({
|
||||
```typescript
|
||||
// Regex patterns
|
||||
const gmailUsers = await User.getInstances({
|
||||
email: { $regex: '@gmail\\.com$', $options: 'i' }
|
||||
email: { $regex: '@gmail\\.com$', $options: 'i' },
|
||||
});
|
||||
|
||||
// Starts with pattern
|
||||
const johnUsers = await User.getInstances({
|
||||
name: { $regex: '^John' }
|
||||
name: { $regex: '^John' },
|
||||
});
|
||||
```
|
||||
|
||||
@@ -349,22 +341,21 @@ const advancedQuery = await User.getInstances({
|
||||
// Nested object with operators
|
||||
profile: {
|
||||
age: { $gte: 18, $lte: 65 },
|
||||
verified: true
|
||||
verified: true,
|
||||
},
|
||||
|
||||
// Dot notation for deep paths
|
||||
'settings.notifications.email': true,
|
||||
'metadata.lastLogin': { $gte: new Date(Date.now() - 30*24*60*60*1000) },
|
||||
'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' },
|
||||
{ 'subscription.trial': true }
|
||||
]
|
||||
$or: [{ 'subscription.plan': 'premium' }, { 'subscription.trial': true }],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -400,7 +391,7 @@ const exact = await Product.search('"MacBook Pro 16"');
|
||||
// Combined with filters for powerful queries
|
||||
const affordable = await Product.search('laptop', {
|
||||
filter: { price: { $lte: 1500 } },
|
||||
validate: async (p) => p.inStock === true
|
||||
validate: async (p) => p.inStock === true,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -427,7 +418,7 @@ const config = await db.createEasyStore<AppConfig>('app-config');
|
||||
// Write with full type checking
|
||||
await config.writeKey('features', {
|
||||
darkMode: true,
|
||||
notifications: false
|
||||
notifications: false,
|
||||
});
|
||||
|
||||
// Read with guaranteed types
|
||||
@@ -437,7 +428,7 @@ const features = await config.readKey('features');
|
||||
// Atomic updates
|
||||
await config.updateKey('limits', (current) => ({
|
||||
...current,
|
||||
maxUsers: current.maxUsers + 100
|
||||
maxUsers: current.maxUsers + 100,
|
||||
}));
|
||||
|
||||
// Delete keys
|
||||
@@ -454,11 +445,11 @@ React to database changes instantly with RxJS integration:
|
||||
```typescript
|
||||
// Watch for changes with automatic reconnection
|
||||
const watcher = await User.watch(
|
||||
{ status: 'active' }, // Filter which documents to watch
|
||||
{ status: 'active' }, // Filter which documents to watch
|
||||
{
|
||||
fullDocument: 'updateLookup', // Get full document on updates
|
||||
bufferTimeMs: 100 // Buffer changes for efficiency
|
||||
}
|
||||
fullDocument: 'updateLookup', // Get full document on updates
|
||||
bufferTimeMs: 100, // Buffer changes for efficiency
|
||||
},
|
||||
);
|
||||
|
||||
// Subscribe to changes with RxJS
|
||||
@@ -466,7 +457,7 @@ watcher.changeSubject.subscribe({
|
||||
next: (change) => {
|
||||
console.log('User changed:', change.fullDocument);
|
||||
|
||||
switch(change.operationType) {
|
||||
switch (change.operationType) {
|
||||
case 'insert':
|
||||
console.log('New user created');
|
||||
break;
|
||||
@@ -478,7 +469,7 @@ watcher.changeSubject.subscribe({
|
||||
break;
|
||||
}
|
||||
},
|
||||
error: (err) => console.error('Watch error:', err)
|
||||
error: (err) => console.error('Watch error:', err),
|
||||
});
|
||||
|
||||
// Advanced: Watch with aggregation pipeline
|
||||
@@ -487,9 +478,9 @@ const complexWatcher = await Order.watch(
|
||||
{
|
||||
pipeline: [
|
||||
{ $match: { 'fullDocument.totalAmount': { $gte: 1000 } } },
|
||||
{ $addFields: { isHighValue: true } }
|
||||
]
|
||||
}
|
||||
{ $addFields: { isHighValue: true } },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
// Clean up when done
|
||||
@@ -522,7 +513,7 @@ const isLeader = coordinator.isLeader;
|
||||
const result = await coordinator.fireDistributedTaskRequest({
|
||||
taskName: 'process-payments',
|
||||
taskExecutionTime: Date.now(),
|
||||
requestResponseId: 'unique-id'
|
||||
requestResponseId: 'unique-id',
|
||||
});
|
||||
|
||||
// Graceful shutdown with leadership handoff
|
||||
@@ -539,11 +530,12 @@ const cursor = await User.getCursor(
|
||||
{ status: 'active' },
|
||||
{
|
||||
// Optional: Use MongoDB native cursor modifiers
|
||||
modifier: (cursor) => cursor
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(10000)
|
||||
.project({ email: 1, username: 1 })
|
||||
}
|
||||
modifier: (cursor) =>
|
||||
cursor
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(10000)
|
||||
.project({ email: 1, username: 1 }),
|
||||
},
|
||||
);
|
||||
|
||||
// Process one at a time
|
||||
@@ -574,16 +566,13 @@ try {
|
||||
// All operations in this block are atomic
|
||||
const sender = await User.getInstance(
|
||||
{ id: 'user-1' },
|
||||
{ session } // Pass session to all operations
|
||||
{ session }, // Pass session to all operations
|
||||
);
|
||||
|
||||
sender.balance -= 100;
|
||||
await sender.save({ session });
|
||||
|
||||
const receiver = await User.getInstance(
|
||||
{ id: 'user-2' },
|
||||
{ session }
|
||||
);
|
||||
const receiver = await User.getInstance({ id: 'user-2' }, { session });
|
||||
|
||||
receiver.balance += 100;
|
||||
await receiver.save({ session });
|
||||
@@ -609,28 +598,28 @@ class Document extends SmartDataDbDoc<Document, Document> {
|
||||
@svDb({
|
||||
// Encrypt sensitive data before storing
|
||||
serialize: async (value) => await encrypt(value),
|
||||
deserialize: async (value) => await decrypt(value)
|
||||
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))
|
||||
deserialize: (value) => JSON.parse(decompress(value)),
|
||||
})
|
||||
public largePayload: any;
|
||||
|
||||
@svDb({
|
||||
// Store Sets as arrays
|
||||
serialize: (set) => Array.from(set),
|
||||
deserialize: (arr) => new Set(arr)
|
||||
deserialize: (arr) => new Set(arr),
|
||||
})
|
||||
public tags: Set<string>;
|
||||
|
||||
@svDb({
|
||||
// Handle custom date formats
|
||||
serialize: (date) => date?.toISOString(),
|
||||
deserialize: (str) => str ? new Date(str) : null
|
||||
deserialize: (str) => (str ? new Date(str) : null),
|
||||
})
|
||||
public scheduledAt: Date | null;
|
||||
}
|
||||
@@ -651,8 +640,9 @@ class Order extends SmartDataDbDoc<Order, Order> {
|
||||
// Called before saving (create or update)
|
||||
async beforeSave() {
|
||||
// Recalculate total
|
||||
this.totalAmount = this.items.reduce((sum, item) =>
|
||||
sum + (item.price * item.quantity), 0
|
||||
this.totalAmount = this.items.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity,
|
||||
0,
|
||||
);
|
||||
|
||||
// Validate
|
||||
@@ -698,34 +688,37 @@ class Order extends SmartDataDbDoc<Order, Order> {
|
||||
|
||||
```typescript
|
||||
@Collection(() => db)
|
||||
class HighPerformanceDoc extends SmartDataDbDoc<HighPerformanceDoc, HighPerformanceDoc> {
|
||||
@unI() // Unique index
|
||||
class HighPerformanceDoc extends SmartDataDbDoc<
|
||||
HighPerformanceDoc,
|
||||
HighPerformanceDoc
|
||||
> {
|
||||
@unI() // Unique index
|
||||
public id: string;
|
||||
|
||||
@index() // Single field index
|
||||
@index() // Single field index
|
||||
public userId: string;
|
||||
|
||||
@index({ sparse: true }) // Sparse index for optional fields
|
||||
@index({ sparse: true }) // Sparse index for optional fields
|
||||
public deletedAt?: Date;
|
||||
|
||||
@index({
|
||||
unique: false,
|
||||
background: true, // Non-blocking index creation
|
||||
expireAfterSeconds: 86400 // TTL index
|
||||
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' }
|
||||
{ userId: 1, createdAt: -1 }, // Compound index
|
||||
{ name: 'user_activity_idx' },
|
||||
);
|
||||
|
||||
// Text index for search
|
||||
await this.collection.createIndex(
|
||||
{ title: 'text', content: 'text' },
|
||||
{ weights: { title: 10, content: 5 } }
|
||||
{ weights: { title: 10, content: 5 } },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -739,18 +732,18 @@ const db = new SmartdataDb({
|
||||
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
|
||||
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
|
||||
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
|
||||
socketTimeoutMS: 360000, // Socket timeout (6 minutes)
|
||||
family: 4, // Force IPv4
|
||||
});
|
||||
```
|
||||
|
||||
@@ -759,6 +752,7 @@ const db = new SmartdataDb({
|
||||
### 1. Always Use TypeScript
|
||||
|
||||
SmartData is built for TypeScript. Using JavaScript means missing out on:
|
||||
|
||||
- Compile-time query validation
|
||||
- IntelliSense for MongoDB operators
|
||||
- Type-safe document updates
|
||||
@@ -785,7 +779,7 @@ await session.withTransaction(async () => {
|
||||
});
|
||||
|
||||
// ❌ Bad: Multiple operations without transactions
|
||||
await debitAccount(fromAccount, amount); // What if this fails?
|
||||
await debitAccount(fromAccount, amount); // What if this fails?
|
||||
await creditAccount(toAccount, amount);
|
||||
```
|
||||
|
||||
@@ -799,7 +793,7 @@ await cursor.forEach(async (doc) => {
|
||||
});
|
||||
|
||||
// ❌ Bad: Loading everything into memory
|
||||
const allDocs = await LargeCollection.getInstances({}); // Could OOM!
|
||||
const allDocs = await LargeCollection.getInstances({}); // Could OOM!
|
||||
```
|
||||
|
||||
### 5. Implement Proper Error Handling
|
||||
@@ -819,7 +813,7 @@ try {
|
||||
|
||||
// ❌ Bad: Ignoring errors
|
||||
const user = await User.getInstance({ id: userId });
|
||||
await processUser(user); // What if user is null?
|
||||
await processUser(user); // What if user is null?
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
@@ -854,17 +848,17 @@ Always clean up resources:
|
||||
// Watchers
|
||||
const watcher = await User.watch({});
|
||||
// ... use watcher
|
||||
await watcher.close(); // Always close!
|
||||
await watcher.close(); // Always close!
|
||||
|
||||
// Cursors
|
||||
const cursor = await User.getCursor({});
|
||||
// ... use cursor
|
||||
await cursor.close(); // Always close!
|
||||
await cursor.close(); // Always close!
|
||||
|
||||
// Sessions
|
||||
const session = db.startSession();
|
||||
// ... use session
|
||||
await session.endSession(); // Always end!
|
||||
await session.endSession(); // Always end!
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
Reference in New Issue
Block a user