Compare commits

..

6 Commits

Author SHA1 Message Date
026f2acc89 v6.0.0
Some checks failed
Default (tags) / security (push) Successful in 45s
Default (tags) / test (push) Failing after 50s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 12:51:45 +00:00
1cd0f09598 BREAKING CHANGE(decorators): Migrate to TC39 Stage 3 decorators and refactor decorator metadata handling; update class initialization, lucene adapter fixes and docs 2025-11-17 12:51:45 +00:00
d254f58a05 v5.16.7
Some checks failed
Default (tags) / security (push) Successful in 47s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 08:05:28 +00:00
c5e7b6f982 fix(classes.collection): Improve Deno and TypeScript compatibility: Collection decorator _svDbOptions forwarding and config cleanup 2025-11-17 08:05:28 +00:00
d30c9619c5 v5.16.6
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 04:16:39 +00:00
7344ae2db3 fix(classes): Add Deno compatibility, prototype-safe decorators and safe collection accessor; bump a few deps 2025-11-17 04:16:39 +00:00
21 changed files with 2418 additions and 2077 deletions

1
.serena/.gitignore vendored
View File

@@ -1 +0,0 @@
/cache

View File

@@ -1,44 +0,0 @@
# Code Style & Conventions
## TypeScript Standards
- **Target**: ES2022
- **Module System**: ESM with NodeNext resolution
- **Decorators**: Experimental decorators enabled
- **Strict Mode**: Implied through TypeScript configuration
## Naming Conventions
- **Interfaces**: Prefix with `I` (e.g., `IUserData`, `IConfig`)
- **Types**: Prefix with `T` (e.g., `TResponseType`, `TQueryResult`)
- **Classes**: PascalCase (e.g., `SmartdataDb`, `SmartDataDbDoc`)
- **Files**: All lowercase (e.g., `classes.doc.ts`, `plugins.ts`)
- **Methods**: camelCase (e.g., `findOne`, `saveToDb`)
## Import Patterns
- All external dependencies imported in `ts/plugins.ts`
- Reference as `plugins.moduleName.method()`
- Use full import paths for internal modules
- Maintain ESM syntax throughout
## Class Structure
- Use decorators for MongoDB document definitions
- Extend base classes (SmartDataDbDoc, SmartDataDbCollection)
- Static methods for factory patterns
- Instance methods for document operations
## Async Patterns
- Preserve Promise-based patterns
- Use async/await for clarity
- Handle errors appropriately
- Return typed Promises
## MongoDB Specifics
- Use `@unify()` decorator for unique fields
- Use `@svDb()` decorator for database fields
- Implement proper serialization/deserialization
- Type-safe query construction with DeepQuery<T>
## Testing Patterns
- Import from `@git.zone/tstest/tapbundle`
- End test files with `export default tap.start()`
- Use descriptive test names
- Cover edge cases and error conditions

View File

@@ -1,37 +0,0 @@
# Project Overview: @push.rocks/smartdata
## Purpose
An advanced TypeScript-first MongoDB wrapper library providing enterprise-grade features for distributed systems, real-time data synchronization, and easy data management.
## Tech Stack
- **Language**: TypeScript (ES2022 target)
- **Runtime**: Node.js >= 16.x
- **Database**: MongoDB >= 4.4
- **Build System**: tsbuild
- **Test Framework**: tstest with tapbundle
- **Package Manager**: pnpm (v10.7.0)
- **Module System**: ESM (ES Modules)
## Key Features
- Type-safe MongoDB integration with decorators
- Document management with automatic timestamps
- EasyStore for key-value storage
- Distributed coordination with leader election
- Real-time data sync with RxJS watchers
- Deep query type safety
- Enhanced cursor API
- Powerful search capabilities
## Project Structure
- **ts/**: Main TypeScript source code
- Core classes for DB, Collections, Documents, Cursors
- Distributed coordinator, EasyStore, Watchers
- Lucene adapter for search functionality
- **test/**: Test files using tstest framework
- **dist_ts/**: Compiled JavaScript output
## Key Dependencies
- MongoDB driver (v6.18.0)
- @push.rocks ecosystem packages
- @tsclass/tsclass for decorators
- RxJS for reactive programming

View File

@@ -1,35 +0,0 @@
# Suggested Commands for @push.rocks/smartdata
## Build & Development
- `pnpm build` - Build the TypeScript project with web support
- `pnpm buildDocs` - Generate documentation using tsdoc
- `tsbuild --web --allowimplicitany` - Direct build command
## Testing
- `pnpm test` - Run all tests in test/ directory
- `pnpm testSearch` - Run specific search test
- `tstest test/test.specific.ts --verbose` - Run specific test with verbose output
- `tsbuild check test/**/* --skiplibcheck` - Type check test files
## Package Management
- `pnpm install` - Install dependencies
- `pnpm install --save-dev <package>` - Add dev dependency
- `pnpm add <package>` - Add production dependency
## Version Control
- `git status` - Check current changes
- `git diff` - View uncommitted changes
- `git log --oneline -10` - View recent commits
- `git mv <old> <new>` - Move/rename files preserving history
## System Utilities (Linux)
- `ls -la` - List all files with details
- `grep -r "pattern" .` - Search for pattern in files
- `find . -name "*.ts"` - Find TypeScript files
- `ps aux | grep node` - Find Node.js processes
- `lsof -i :80` - Check process on port 80
## Debug & Development
- `tsx <script.ts>` - Run TypeScript file directly
- Store debug scripts in `.nogit/debug/`
- Curl endpoints for API testing

View File

@@ -1,33 +0,0 @@
# Task Completion Checklist
When completing any coding task in this project, always:
## Before Committing
1. **Build the project**: Run `pnpm build` to ensure TypeScript compiles
2. **Run tests**: Execute `pnpm test` to verify nothing is broken
3. **Type check**: Verify types compile correctly
4. **Check for lint issues**: Look for any code style violations
## Code Quality Checks
- Verify all imports are in `ts/plugins.ts` for external dependencies
- Check that interfaces are prefixed with `I`
- Check that types are prefixed with `T`
- Ensure filenames are lowercase
- Verify async patterns are preserved where needed
- Check that decorators are properly used for MongoDB documents
## Documentation
- Update relevant comments if functionality changed
- Ensure new exports are properly documented
- Update readme.md if new features added (only if explicitly requested)
## Git Hygiene
- Make small, focused commits
- Write clear commit messages
- Use `git mv` for file operations
- Never commit sensitive data or keys
## Final Verification
- Test the specific functionality that was changed
- Ensure no unintended side effects
- Verify the change solves the original problem completely

View File

@@ -1,68 +0,0 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed)on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "smartdata"

View File

@@ -22,5 +22,6 @@
}
}
}
]
],
"deno.enable": false
}

View File

@@ -1,5 +1,34 @@
# Changelog
## 2025-11-17 - 6.0.0 - BREAKING CHANGE(decorators)
Migrate to TC39 Stage 3 decorators and refactor decorator metadata handling; update class initialization, lucene adapter fixes and docs
- Switch all decorators to TC39 Stage 3 signatures and metadata usage (use context.metadata and context.addInitializer) — affects svDb, globalSvDb, searchable, unI, index, Collection and managed.
- Refactor Collection/managed decorators to read and initialize prototype/constructor properties from context.metadata to ensure prototype properties are available before instance creation (ts/classes.collection.ts).
- Improve search implementation: add a Lucene parser and transformer with safer MongoDB query generation, wildcard/fuzzy handling and properly structured boolean operators (ts/classes.lucene.adapter.ts).
- Search integration updated to use the new adapter and handle advanced Lucene syntax and edge cases more robustly.
- Bump dev tooling versions: @git.zone/tsbuild -> ^3.1.0 and @git.zone/tsrun -> ^2.0.0.
- Documentation: update README and add readme.hints.md describing the TC39 decorator migration, minimum TypeScript (>=5.2) and Deno notes; tests adjusted accordingly.
- Clean up project memory/config files related to the previous decorator approach and Deno configuration adjustments.
## 2025-11-17 - 5.16.7 - fix(classes.collection)
Improve Deno and TypeScript compatibility: Collection decorator _svDbOptions forwarding and config cleanup
- Collection decorator: capture original constructor and forward _svDbOptions to ensure property decorator options (serialize/deserialize) remain accessible in Deno environments.
- Collection decorator: keep instance getter defined on prototype for Deno compatibility (no behavior change, clarifies forwarding logic).
- Build/config: removed experimentalDecorators and useDefineForClassFields from deno.json and tsconfig.json to avoid Deno/TS build issues and rely on default compilation settings.
## 2025-11-17 - 5.16.6 - fix(classes)
Add Deno compatibility, prototype-safe decorators and safe collection accessor; bump a few deps
- Add deno.json to enable experimentalDecorators and target ES2022/DOM for Deno builds.
- Introduce getCollectionSafe() on SmartDataDbDoc and use it for save/update/delete/findOne to avoid runtime errors when instance 'collection' is not present.
- Change several instance properties (globalSaveableProperties, uniqueIndexes, regularIndexes, saveableProperties) to 'declare' so decorator-set prototype properties are not shadowed (Deno compatibility).
- Enhance @Collection decorator: capture original constructor/prototype for Deno, define prototype getter for collection on decorated class, attach docCtor for searchableFields, and forward _svDbOptions to the original constructor to preserve serializer metadata.
- Improve text/search index handling by relying on docCtor.searchableFields and guarding text index creation.
- Bump dependencies/devDependencies: @push.rocks/smartmongo -> ^2.0.14, @git.zone/tsbuild -> ^2.7.1, @git.zone/tstest -> ^2.8.1.
- These are non-breaking runtime compatibility and developer-experience fixes; intended as a patch release.
## 2025-11-16 - 5.16.5 - fix(watcher)
Update dependencies, tooling and watcher import; add .serena cache ignore

426
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartdata",
"version": "5.16.5",
"version": "6.0.0",
"private": false,
"description": "An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.",
"main": "dist_ts/index.js",
@@ -26,7 +26,7 @@
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdelay": "^3.0.1",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartmongo": "^2.0.12",
"@push.rocks/smartmongo": "^2.0.14",
"@push.rocks/smartpromise": "^4.0.2",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstring": "^4.1.0",
@@ -37,9 +37,9 @@
"mongodb": "^6.20.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsrun": "^1.6.2",
"@git.zone/tstest": "^2.6.2",
"@git.zone/tsbuild": "^3.1.0",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^2.8.1",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.2"

2974
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
# Project Memory - Smartdata
## TC39 Decorator Migration (v6.0.0) - ✅ COMPLETED
### Final Status: All Tests Passing (157/157)
Migration successfully completed on 2025-11-17.
### What Changed:
- ✅ Removed `experimentalDecorators` from tsconfig.json
- ✅ Refactored all 7 decorators to TC39 Stage 3 syntax
- 5 property decorators: @globalSvDb, @svDb, @unI, @index, @searchable
- 2 class decorators: @Collection, @managed
- ✅ Implemented context.metadata pattern for shared decorator state
- ✅ All tests passing across Node.js and Deno runtimes
### Critical Discovery: TC39 Metadata Access Pattern
**THE KEY INSIGHT**: In TC39 decorators, metadata is NOT accessed via `constructor[Symbol.metadata]`. Instead:
- **Field decorators**: Write to `context.metadata`
- **Class decorators**: Read from `context.metadata` (same shared object!)
- The `context.metadata` object is shared between all decorators on the same class
- Attempting to write to `constructor[Symbol.metadata]` throws: "Cannot assign to read only property"
### Implementation Pattern:
```typescript
// Field decorator - stores metadata
export function svDb() {
return (value: undefined, context: ClassFieldDecoratorContext) => {
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
metadata.saveableProperties.push(String(context.name));
};
}
// Class decorator - reads metadata and initializes prototype
export function Collection(dbArg: SmartdataDb) {
return function(value: Function, context: ClassDecoratorContext) => {
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (metadata?.saveableProperties) {
decoratedClass.prototype.saveableProperties = [...metadata.saveableProperties];
}
return decoratedClass;
};
}
```
### Runtime Compatibility:
-**Node.js v23.8.0**: Full TC39 support
-**Deno v2.5.4**: Full TC39 support
-**Bun v1.3.0**: No TC39 support (uses legacy decorators only)
- Removed "+bun" from test filenames to skip Bun tests
### Key Technical Notes:
1. **Metadata Initialization Timing**: Class decorators run AFTER field decorators, allowing them to read accumulated metadata and initialize prototypes before any instances are created
2. **Prototype vs Instance Properties**: Properties set on prototype are accessible via `this.propertyName` in instances
3. **TypeScript Lib Support**: TypeScript 5.9.3 includes built-in decorator types (no custom lib configuration needed)
4. **Interface Naming**: Used `ISmartdataDecoratorMetadata` extending `DecoratorMetadataObject` for type safety
### Files Modified:
- ts/classes.doc.ts (property decorators + metadata interface)
- ts/classes.collection.ts (class decorators + prototype initialization)
- tsconfig.json (removed experimentalDecorators flag)
- test/*.ts (renamed files to remove "+bun" suffix)
### Test Results:
All 157 tests passing across 10 test files:
- test.cursor.ts: 7/7
- test.deno.ts: 11/11 (queries working correctly!)
- test.search.advanced.ts: 41/41
- test.typescript.ts: 4/4
- test.watch.ts: 5/5
- And 5 more test files
### Migration Learnings for Future Reference:
1. `context.metadata` is the ONLY way to share state between decorators
2. Class decorators must initialize prototypes from metadata immediately
3. `Symbol.metadata` on constructors is read-only (managed by runtime)
4. Field decorators run before class decorators (guaranteed order)
5. TypeScript 5.2+ has built-in TC39 decorator support

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.

255
test/test.deno.ts Normal file
View File

@@ -0,0 +1,255 @@
// TODO: Decorator support during testing for bun and deno in @git.zone/tstest
import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as smartmongo from '@push.rocks/smartmongo';
import { smartunique } from '../ts/plugins.js';
import * as mongodb from 'mongodb';
const testQenv = new Qenv(process.cwd(), process.cwd() + '/.nogit/');
console.log(process.memoryUsage());
// the tested module
import * as smartdata from '../ts/index.js';
// =======================================
// Connecting to the database server
// =======================================
let smartmongoInstance: smartmongo.SmartMongo;
let testDb: smartdata.SmartdataDb;
const totalCars = 2000;
tap.test('should create a testinstance as database', async () => {
const databaseName = `test-smartdata-deno-${smartunique.shortId()}`;
testDb = new smartdata.SmartdataDb({
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGODB_URL'),
mongoDbName: databaseName,
});
await testDb.init();
});
// =======================================
// The actual tests
// =======================================
// ------
// Collections
// ------
@smartdata.Collection(() => {
return testDb;
})
class Car extends smartdata.SmartDataDbDoc<Car, Car> {
@smartdata.unI()
public index: string = smartunique.shortId();
@smartdata.svDb()
public color: string;
@smartdata.svDb()
public brand: string;
@smartdata.svDb()
public testBuffer = Buffer.from('hello');
@smartdata.svDb()
deepData = {
sodeep: 'yes',
};
constructor(colorArg: string, brandArg: string) {
super();
this.color = colorArg;
this.brand = brandArg;
}
}
tap.test('should create a new id', async () => {
const newid = await Car.getNewId();
console.log(newid);
});
tap.test('should save the car to the db', async (toolsArg) => {
const myCar = new Car('red', 'Volvo');
console.log('Car.collection.smartdataDb:', (Car.collection as any).smartdataDb?.mongoDb?.databaseName);
console.log('Car.collection.collectionName:', (Car.collection as any).collectionName);
console.log('testDb.mongoDb.databaseName:', testDb.mongoDb.databaseName);
await myCar.save();
const myCar2 = new Car('red', 'Volvo');
await myCar2.save();
let counter = 0;
const gottenCarInstance = await Car.getInstance({});
console.log(gottenCarInstance.testBuffer instanceof mongodb.Binary);
process.memoryUsage();
do {
const myCar3 = new Car('red', 'Renault');
await myCar3.save();
counter++;
if (counter % 100 === 0) {
console.log(
`Filled database with ${counter} of ${totalCars} Cars and memory usage ${
process.memoryUsage().rss / 1e6
} MB`,
);
}
} while (counter < totalCars);
console.log(process.memoryUsage());
// DEBUG: Check what's actually in the database
const savedCount = await Car.getCount({});
console.log('Total cars saved in DB:', savedCount);
const renaultCount = await Car.getCount({ brand: 'Renault' });
console.log('Renault cars in DB:', renaultCount);
// Check what's actually in the first saved car
const firstCar = await Car.getInstance({});
console.log('First car data:', JSON.stringify({
color: firstCar?.color,
brand: firstCar?.brand,
index: firstCar?.index
}));
});
tap.test('expect to get instance of Car with shallow match', async () => {
console.log('Before query - testDb.mongoDb.databaseName:', testDb.mongoDb.databaseName);
console.log('Before query - Car.collection.smartdataDb:', (Car.collection as any).smartdataDb?.mongoDb?.databaseName);
console.log('Before query - Car.collection.collectionName:', (Car.collection as any).collectionName);
const totalQueryCycles = totalCars / 2;
let counter = 0;
do {
const timeStart = Date.now();
const myCars = await Car.getInstances({
brand: 'Renault',
});
if (counter % 10 === 0) {
console.log(
`performed ${counter} of ${totalQueryCycles} total query cycles: took ${
Date.now() - timeStart
}ms to query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`,
);
console.log('myCars.length:', myCars.length);
console.log('myCars[0]:', myCars[0]);
}
expect(myCars[0].deepData.sodeep).toEqual('yes');
expect(myCars[0].brand).toEqual('Renault');
counter++;
} while (counter < totalQueryCycles);
});
tap.test('expect to get instance of Car with deep match', async () => {
const totalQueryCycles = totalCars / 6;
let counter = 0;
do {
const timeStart = Date.now();
const myCars2 = await Car.getInstances({
deepData: {
sodeep: 'yes',
},
});
if (counter % 10 === 0) {
console.log(
`performed ${counter} of ${totalQueryCycles} total query cycles: took ${
Date.now() - timeStart
}ms to deep query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`,
);
}
expect(myCars2[0].deepData.sodeep).toEqual('yes');
expect(myCars2[0].brand).toEqual('Volvo');
counter++;
} while (counter < totalQueryCycles);
});
tap.test('expect to get instance of Car and update it', async () => {
const myCar = await Car.getInstance<Car>({
brand: 'Volvo',
});
expect(myCar.color).toEqual('red');
myCar.color = 'blue';
await myCar.save();
});
tap.test('should be able to delete an instance of car', async () => {
const myCars = await Car.getInstances({
brand: 'Volvo',
color: 'blue',
});
console.log(myCars);
expect(myCars[0].color).toEqual('blue');
for (const myCar of myCars) {
await myCar.delete();
}
const myCar2 = await Car.getInstance<Car>({
brand: 'Volvo',
});
expect(myCar2.color).toEqual('red');
});
// tslint:disable-next-line: max-classes-per-file
@smartdata.Collection(() => {
return testDb;
})
class Truck extends smartdata.SmartDataDbDoc<Car, Car> {
@smartdata.unI()
public id: string = smartunique.shortId();
@smartdata.svDb()
public color: string;
@smartdata.svDb()
public brand: string;
constructor(colorArg: string, brandArg: string) {
super();
this.color = colorArg;
this.brand = brandArg;
}
}
tap.test('should store a new Truck', async () => {
const truck = new Truck('blue', 'MAN');
await truck.save();
const myTruck2 = await Truck.getInstance({ color: 'blue' });
expect(myTruck2.color).toEqual('blue');
myTruck2.color = 'red';
await myTruck2.save();
const myTruck3 = await Truck.getInstance({ color: 'blue' });
expect(myTruck3).toBeNull();
});
tap.test('should return a count', async () => {
const truckCount = await Truck.getCount();
expect(truckCount).toEqual(1);
});
tap.test('should use a cursor', async () => {
const cursor = await Car.getCursor({});
let counter = 0;
await cursor.forEach(async (carArg) => {
counter++;
counter % 50 === 0 ? console.log(`50 more of ${carArg.color}`) : null;
});
});
// =======================================
// close the database connection
// =======================================
tap.test('close', async () => {
if (smartmongoInstance) {
await smartmongoInstance.stopAndDumpToDir('./.nogit/dbdump/test.ts');
} else {
await testDb.mongoDb.dropDatabase();
await testDb.close();
}
setTimeout(() => process.exit(), 2000);
});
tap.start({ throwOnError: true });

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartdata',
version: '5.16.5',
version: '6.0.0',
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
}

View File

@@ -26,32 +26,94 @@ const collectionFactory = new CollectionFactory();
* @param dbArg
*/
export function Collection(dbArg: SmartdataDb | TDelayed<SmartdataDb>) {
return function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return function classDecorator(value: Function, context: ClassDecoratorContext) {
if (context.kind !== 'class') {
throw new Error('Collection can only decorate classes');
}
// Capture original constructor for _svDbOptions forwarding
const originalConstructor = value as any;
const constructor = value as { new (...args: any[]): any };
const getCollection = () => {
if (!(dbArg instanceof SmartdataDb)) {
dbArg = dbArg();
}
const coll = collectionFactory.getCollection(constructor.name, dbArg);
// Attach document constructor for searchableFields lookup
if (!(coll as any).docCtor) {
(coll as any).docCtor = decoratedClass;
}
return coll;
};
const decoratedClass = class extends constructor {
public static className = constructor.name;
public static get collection() {
if (!(dbArg instanceof SmartdataDb)) {
dbArg = dbArg();
}
const coll = collectionFactory.getCollection(constructor.name, dbArg);
// Attach document constructor for searchableFields lookup
if (!(coll as any).docCtor) {
(coll as any).docCtor = decoratedClass;
}
return coll;
return getCollection();
}
public get collection() {
if (!(dbArg instanceof SmartdataDb)) {
dbArg = dbArg();
}
const coll = collectionFactory.getCollection(constructor.name, dbArg);
if (!(coll as any).docCtor) {
(coll as any).docCtor = decoratedClass;
}
return coll;
return getCollection();
}
};
return decoratedClass;
// Ensure instance getter works in Deno by defining it on the prototype
Object.defineProperty(decoratedClass.prototype, 'collection', {
get: getCollection,
enumerable: false,
configurable: true
});
// Deno compatibility note: Property decorators set properties on the prototype.
// Since we removed instance property declarations from SmartDataDbDoc,
// the decorator-set prototype properties are now accessible without shadowing.
// No manual forwarding needed - natural prototype inheritance works!
// Point to original constructor's _svDbOptions
Object.defineProperty(decoratedClass, '_svDbOptions', {
get() { return originalConstructor._svDbOptions; },
set(value) { originalConstructor._svDbOptions = value; },
configurable: true
});
// Initialize prototype properties from context.metadata (TC39 decorator metadata)
// This ensures prototype properties are available before any instance is created
const metadata = context.metadata as any;
if (metadata) {
const proto = decoratedClass.prototype;
// Initialize globalSaveableProperties
if (metadata.globalSaveableProperties && !proto.globalSaveableProperties) {
proto.globalSaveableProperties = [...metadata.globalSaveableProperties];
}
// Initialize saveableProperties
if (metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
// Initialize uniqueIndexes
if (metadata.uniqueIndexes && !proto.uniqueIndexes) {
proto.uniqueIndexes = [...metadata.uniqueIndexes];
}
// Initialize regularIndexes
if (metadata.regularIndexes && !proto.regularIndexes) {
proto.regularIndexes = [...metadata.regularIndexes];
}
// Initialize searchableFields on constructor (not prototype)
if (metadata.searchableFields && !Array.isArray((decoratedClass as any).searchableFields)) {
(decoratedClass as any).searchableFields = [...metadata.searchableFields];
}
// Initialize _svDbOptions from metadata
if (metadata._svDbOptions && !originalConstructor._svDbOptions) {
originalConstructor._svDbOptions = { ...metadata._svDbOptions };
}
}
return decoratedClass as any;
};
}
@@ -69,7 +131,13 @@ export const setDefaultManagerForDoc = <T,>(managerArg: IManager, dbDocArg: T):
* @param dbArg
*/
export function managed<TManager extends IManager>(managerArg?: TManager | TDelayed<TManager>) {
return function classDecorator<T extends { new (...args: any[]): any }>(constructor: T) {
return function classDecorator(value: Function, context: ClassDecoratorContext) {
if (context.kind !== 'class') {
throw new Error('managed can only decorate classes');
}
const constructor = value as { new (...args: any[]): any };
const decoratedClass = class extends constructor {
public static className = constructor.name;
public static get collection() {
@@ -119,7 +187,46 @@ export function managed<TManager extends IManager>(managerArg?: TManager | TDela
return manager;
}
};
return decoratedClass;
// Initialize prototype properties from context.metadata (TC39 decorator metadata)
// This ensures prototype properties are available before any instance is created
const originalConstructor = value as any;
const metadata = context.metadata as any;
if (metadata) {
const proto = decoratedClass.prototype;
// Initialize globalSaveableProperties
if (metadata.globalSaveableProperties && !proto.globalSaveableProperties) {
proto.globalSaveableProperties = [...metadata.globalSaveableProperties];
}
// Initialize saveableProperties
if (metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
// Initialize uniqueIndexes
if (metadata.uniqueIndexes && !proto.uniqueIndexes) {
proto.uniqueIndexes = [...metadata.uniqueIndexes];
}
// Initialize regularIndexes
if (metadata.regularIndexes && !proto.regularIndexes) {
proto.regularIndexes = [...metadata.regularIndexes];
}
// Initialize searchableFields on constructor (not prototype)
if (metadata.searchableFields && !Array.isArray((decoratedClass as any).searchableFields)) {
(decoratedClass as any).searchableFields = [...metadata.searchableFields];
}
// Initialize _svDbOptions from metadata
if (metadata._svDbOptions && !originalConstructor._svDbOptions) {
originalConstructor._svDbOptions = { ...metadata._svDbOptions };
}
}
return decoratedClass as any;
};
}

View File

@@ -28,15 +28,42 @@ export interface SearchOptions<T> {
export type TDocCreation = 'db' | 'new' | 'mixed';
// Type for decorator metadata - extends TypeScript's built-in DecoratorMetadataObject
interface ISmartdataDecoratorMetadata extends DecoratorMetadataObject {
globalSaveableProperties?: string[];
saveableProperties?: string[];
uniqueIndexes?: string[];
regularIndexes?: Array<{field: string, options: IIndexOptions}>;
searchableFields?: string[];
_svDbOptions?: Record<string, SvDbOptions>;
}
export function globalSvDb() {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
logger.log('debug', `called svDb() on >${target.constructor.name}.${key}<`);
if (!target.globalSaveableProperties) {
target.globalSaveableProperties = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('globalSvDb can only decorate fields');
}
target.globalSaveableProperties.push(key);
// Store metadata at class level using Symbol.metadata
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.globalSaveableProperties) {
metadata.globalSaveableProperties = [];
}
metadata.globalSaveableProperties.push(String(context.name));
logger.log('debug', `called globalSvDb() on metadata for property ${String(context.name)}`);
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function(this: any) {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.globalSaveableProperties && !proto.globalSaveableProperties) {
// Initialize prototype array from metadata (runs once per class)
proto.globalSaveableProperties = [...metadata.globalSaveableProperties];
logger.log('debug', `initialized globalSaveableProperties with ${proto.globalSaveableProperties.length} properties`);
}
});
};
}
@@ -54,20 +81,47 @@ export interface SvDbOptions {
* saveable - saveable decorator to be used on class properties
*/
export function svDb(options?: SvDbOptions) {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
logger.log('debug', `called svDb() on >${target.constructor.name}.${key}<`);
if (!target.saveableProperties) {
target.saveableProperties = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('svDb can only decorate fields');
}
target.saveableProperties.push(key);
// attach custom serializer/deserializer options to the class constructor
const ctor = target.constructor as any;
if (!ctor._svDbOptions) {
ctor._svDbOptions = {};
const propName = String(context.name);
// Store metadata at class level using Symbol.metadata
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
metadata.saveableProperties.push(propName);
// Store options in metadata
if (options) {
ctor._svDbOptions[key] = options;
if (!metadata._svDbOptions) {
metadata._svDbOptions = {};
}
metadata._svDbOptions[propName] = options;
}
logger.log('debug', `called svDb() on metadata for property ${propName}`);
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function(this: any) {
const proto = this.constructor.prototype;
const ctor = this.constructor;
const metadata = ctor[Symbol.metadata];
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
// Initialize prototype array from metadata (runs once per class)
proto.saveableProperties = [...metadata.saveableProperties];
logger.log('debug', `initialized saveableProperties with ${proto.saveableProperties.length} properties`);
}
// Initialize svDbOptions from metadata
if (metadata && metadata._svDbOptions && !ctor._svDbOptions) {
ctor._svDbOptions = { ...metadata._svDbOptions };
}
});
};
}
@@ -75,13 +129,30 @@ export function svDb(options?: SvDbOptions) {
* searchable - marks a property as searchable with Lucene query syntax
*/
export function searchable() {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
// Attach to class constructor for direct access
const ctor = target.constructor as any;
if (!Array.isArray(ctor.searchableFields)) {
ctor.searchableFields = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('searchable can only decorate fields');
}
ctor.searchableFields.push(key);
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.searchableFields) {
metadata.searchableFields = [];
}
metadata.searchableFields.push(propName);
// Use addInitializer to set up constructor property once
context.addInitializer(function(this: any) {
const ctor = this.constructor as any;
const metadata = ctor[Symbol.metadata];
if (metadata && metadata.searchableFields && !Array.isArray(ctor.searchableFields)) {
// Initialize from metadata (runs once per class)
ctor.searchableFields = [...metadata.searchableFields];
}
});
};
}
@@ -94,20 +165,44 @@ function escapeForRegex(input: string): string {
* unique index - decorator to mark a unique index
*/
export function unI() {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
logger.log('debug', `called unI on >>${target.constructor.name}.${key}<<`);
// mark the index as unique
if (!target.uniqueIndexes) {
target.uniqueIndexes = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('unI can only decorate fields');
}
target.uniqueIndexes.push(key);
// and also save it
if (!target.saveableProperties) {
target.saveableProperties = [];
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.uniqueIndexes) {
metadata.uniqueIndexes = [];
}
target.saveableProperties.push(key);
metadata.uniqueIndexes.push(propName);
// Also mark as saveable
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
if (!metadata.saveableProperties.includes(propName)) {
metadata.saveableProperties.push(propName);
}
logger.log('debug', `called unI on metadata for property ${propName}`);
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function(this: any) {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.uniqueIndexes && !proto.uniqueIndexes) {
proto.uniqueIndexes = [...metadata.uniqueIndexes];
logger.log('debug', `initialized uniqueIndexes with ${proto.uniqueIndexes.length} properties`);
}
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
});
};
}
@@ -126,28 +221,47 @@ export interface IIndexOptions {
* index - decorator to mark a field for regular indexing
*/
export function index(options?: IIndexOptions) {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
logger.log('debug', `called index() on >${target.constructor.name}.${key}<`);
// Initialize regular indexes array if it doesn't exist
if (!target.regularIndexes) {
target.regularIndexes = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('index can only decorate fields');
}
// Add this field to regularIndexes with its options
target.regularIndexes.push({
field: key,
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.regularIndexes) {
metadata.regularIndexes = [];
}
metadata.regularIndexes.push({
field: propName,
options: options || {}
});
// Also ensure it's marked as saveable
if (!target.saveableProperties) {
target.saveableProperties = [];
// Also mark as saveable
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
if (!target.saveableProperties.includes(key)) {
target.saveableProperties.push(key);
if (!metadata.saveableProperties.includes(propName)) {
metadata.saveableProperties.push(propName);
}
logger.log('debug', `called index() on metadata for property ${propName}`);
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function(this: any) {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.regularIndexes && !proto.regularIndexes) {
proto.regularIndexes = [...metadata.regularIndexes];
logger.log('debug', `initialized regularIndexes with ${proto.regularIndexes.length} indexes`);
}
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
});
};
}
@@ -339,6 +453,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
public static manager;
public manager: TManager;
/**
* Helper to get collection with fallback to static for Deno compatibility
*/
private getCollectionSafe(): SmartdataCollection<any> {
return this.collection || (this.constructor as any).collection;
}
// STATIC
public static createInstanceFromMongoDbNativeDoc<T>(
this: plugins.tsclass.typeFest.Class<T>,
@@ -698,23 +819,28 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
/**
* an array of saveable properties of ALL doc
* Note: Set by decorators on prototype - NOT declared as instance property to avoid shadowing in Deno
* Declared with definite assignment assertion to satisfy TypeScript without creating instance property
*/
public globalSaveableProperties: string[];
declare globalSaveableProperties: string[];
/**
* unique indexes
* Note: Set by decorators on prototype - NOT declared as instance property to avoid shadowing in Deno
*/
public uniqueIndexes: string[];
declare uniqueIndexes: string[];
/**
* regular indexes with their options
* Note: Set by decorators on prototype - NOT declared as instance property to avoid shadowing in Deno
*/
public regularIndexes: Array<{field: string, options: IIndexOptions}> = [];
declare regularIndexes: Array<{field: string, options: IIndexOptions}>;
/**
* an array of saveable properties of a specific doc
* Note: Set by decorators on prototype - NOT declared as instance property to avoid shadowing in Deno
*/
public saveableProperties: string[];
declare saveableProperties: string[];
/**
* name
@@ -747,10 +873,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
// perform insert or update
switch (this.creationStatus) {
case 'db':
dbResult = await this.collection.update(self, { session: opts?.session });
dbResult = await this.getCollectionSafe().update(self, { session: opts?.session });
break;
case 'new':
dbResult = await this.collection.insert(self, { session: opts?.session });
dbResult = await this.getCollectionSafe().insert(self, { session: opts?.session });
this.creationStatus = 'db';
break;
default:
@@ -772,7 +898,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
await (this as any).beforeDelete();
}
// perform deletion
const result = await this.collection.delete(this, { session: opts?.session });
const result = await this.getCollectionSafe().delete(this, { session: opts?.session });
// allow hook after delete
if (typeof (this as any).afterDelete === 'function') {
await (this as any).afterDelete();
@@ -802,7 +928,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
* updates an object from db
*/
public async updateFromDb(): Promise<boolean> {
const mongoDbNativeDoc = await this.collection.findOne(await this.createIdentifiableObject());
const mongoDbNativeDoc = await this.getCollectionSafe().findOne(await this.createIdentifiableObject());
if (!mongoDbNativeDoc) {
return false; // Document not found in database
}

View File

@@ -1,7 +1,5 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",