diff --git a/.serena/.gitignore b/.serena/.gitignore deleted file mode 100644 index 14d86ad..0000000 --- a/.serena/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/cache diff --git a/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl b/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl deleted file mode 100644 index 0f64843..0000000 Binary files a/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl and /dev/null differ diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md deleted file mode 100644 index f31caa7..0000000 --- a/.serena/memories/code_style_conventions.md +++ /dev/null @@ -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 - -## 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 \ No newline at end of file diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md deleted file mode 100644 index 4598be9..0000000 --- a/.serena/memories/project_overview.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md deleted file mode 100644 index af3de68..0000000 --- a/.serena/memories/suggested_commands.md +++ /dev/null @@ -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 ` - Add dev dependency -- `pnpm add ` - Add production dependency - -## Version Control -- `git status` - Check current changes -- `git diff` - View uncommitted changes -- `git log --oneline -10` - View recent commits -- `git mv ` - 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 ` - Run TypeScript file directly -- Store debug scripts in `.nogit/debug/` -- Curl endpoints for API testing \ No newline at end of file diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md deleted file mode 100644 index 2bfa1e2..0000000 --- a/.serena/memories/task_completion_checklist.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index 7ae4441..0000000 --- a/.serena/project.yml +++ /dev/null @@ -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" diff --git a/changelog.md b/changelog.md index 18c6138..e23d517 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # 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 diff --git a/deno.json b/deno.json deleted file mode 100644 index 96b474c..0000000 --- a/deno.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "lib": [ - "ES2022", - "DOM" - ] - }, - "nodeModulesDir": "auto", - "version": "5.16.7" -} diff --git a/deno.lock b/deno.lock index b583732..80be041 100644 --- a/deno.lock +++ b/deno.lock @@ -1,8 +1,8 @@ { "version": "5", "specifiers": { - "npm:@git.zone/tsbuild@^2.7.1": "2.7.1", - "npm:@git.zone/tsrun@^1.6.2": "1.6.2", + "npm:@git.zone/tsbuild@^3.1.0": "3.1.0", + "npm:@git.zone/tsrun@2": "2.0.0", "npm:@git.zone/tstest@^2.8.1": "2.8.1", "npm:@push.rocks/lik@^6.2.2": "6.2.2", "npm:@push.rocks/qenv@^6.1.3": "6.1.3", @@ -940,8 +940,8 @@ ], "tarball": "https://verdaccio.lossless.digital/@esm-bundle/chai/-/chai-4.3.4-fix.0.tgz" }, - "@git.zone/tsbuild@2.7.1": { - "integrity": "sha512-O8TTc+LBp8hYy5+zA6AdoqdQQtVXTAd1L0gS/Ihz+QXgXvMdQKVINwpDFu6LS5NdVrGXzxnB63NQpfg5COIPnQ==", + "@git.zone/tsbuild@3.1.0": { + "integrity": "sha512-j8lMd84pmzWiU6NG3e+pyu0o41oo6mQVfcZv8kDsCrQwZMhoQV9Jp87MlU0i/XI5IZkqDjelG8Kx1QhOmbK+iQ==", "dependencies": [ "@git.zone/tspublish", "@push.rocks/early", @@ -954,10 +954,10 @@ "typescript@5.9.3" ], "bin": true, - "tarball": "https://verdaccio.lossless.digital/@git.zone/tsbuild/-/tsbuild-2.7.1.tgz" + "tarball": "https://verdaccio.lossless.digital/@git.zone/tsbuild/-/tsbuild-3.1.0.tgz" }, - "@git.zone/tsbundle@2.5.1": { - "integrity": "sha512-gBskgM3ECy9FEmhCWnQahDyFCAjjw/7emjx/KYM/FOlPqGV+hmYzt368zwSlkzOGgYF8k9OZ+mp6vexDL/+f2w==", + "@git.zone/tsbundle@2.5.2": { + "integrity": "sha512-EYTCfunqoxhxkowREZ+cJnww6eDh9cL18HJbHbSZ+vxzNeyS9x8mT9aqRlWkI7zgpvgDlGIYlyRUlUISXkQO6Q==", "dependencies": [ "@push.rocks/early", "@push.rocks/smartcli", @@ -976,7 +976,7 @@ "typescript@5.8.3" ], "bin": true, - "tarball": "https://verdaccio.lossless.digital/@git.zone/tsbundle/-/tsbundle-2.5.1.tgz" + "tarball": "https://verdaccio.lossless.digital/@git.zone/tsbundle/-/tsbundle-2.5.2.tgz" }, "@git.zone/tspublish@1.10.3": { "integrity": "sha512-o2/jvNsdLC8SRdH1kQ7JjNOQNu9el0FpJ/QOW3mgiC5C9reuTp18iU4kijsVVLgvw4KZv6Z289SoKPh3HPsS0g==", @@ -1004,12 +1004,22 @@ "bin": true, "tarball": "https://verdaccio.lossless.digital/@git.zone/tsrun/-/tsrun-1.6.2.tgz" }, + "@git.zone/tsrun@2.0.0": { + "integrity": "sha512-yA6zCjL+kn7xfZe6sL/m4K+zYqgkznG/pF6++i/E17iwzpG6dHmW+VZmYldHe86sW4DcLMvqM6CxM+KlgaEpKw==", + "dependencies": [ + "@push.rocks/smartfile", + "@push.rocks/smartshell", + "tsx" + ], + "bin": true, + "tarball": "https://verdaccio.lossless.digital/@git.zone/tsrun/-/tsrun-2.0.0.tgz" + }, "@git.zone/tstest@2.8.1": { "integrity": "sha512-0Sct9XsbrmAQgKNoW/jBNPMLllKVI+W6/aVkj9DEguiEnysmxLb3xRyoay06lxTGSBe5dA5uNULrdycdQ9slgQ==", "dependencies": [ "@api.global/typedserver", "@git.zone/tsbundle", - "@git.zone/tsrun", + "@git.zone/tsrun@1.6.2", "@push.rocks/consolecolor", "@push.rocks/qenv", "@push.rocks/smartbrowser", @@ -1638,8 +1648,8 @@ ], "tarball": "https://verdaccio.lossless.digital/@push.rocks/smartcrypto/-/smartcrypto-2.0.4.tgz" }, - "@push.rocks/smartdata@5.16.4": { - "integrity": "sha512-COiKw8yk9iAcLN44WmZHG8Gi0v+HGkgM8Osoq7Cns+UsOA+grPepqbN2r0XPG1fm5vOdJcaydi2ZU0xrnbGVvQ==", + "@push.rocks/smartdata@5.16.7": { + "integrity": "sha512-bu/YSIjQcwxWXkAsuhqE6zs7eT+bTIKV8+/H7TbbjpzeioLCyB3dZ/41cLZk37c/EYt4d4GHgZ0ww80OiKOUMg==", "dependencies": [ "@push.rocks/lik", "@push.rocks/smartdelay", @@ -1654,7 +1664,7 @@ "@tsclass/tsclass@9.3.0", "mongodb" ], - "tarball": "https://verdaccio.lossless.digital/@push.rocks/smartdata/-/smartdata-5.16.4.tgz" + "tarball": "https://verdaccio.lossless.digital/@push.rocks/smartdata/-/smartdata-5.16.7.tgz" }, "@push.rocks/smartdelay@3.0.5": { "integrity": "sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==", @@ -3399,12 +3409,12 @@ ], "tarball": "https://verdaccio.lossless.digital/@types/sinon-chai/-/sinon-chai-3.2.12.tgz" }, - "@types/sinon@20.0.0": { - "integrity": "sha512-etYGUC6IEevDGSWvR9WrECRA01ucR2/Oi9XMBUAdV0g4bLkNf4HlZWGiGlDOq5lgwXRwcV+PSeKgFcW4QzzYOg==", + "@types/sinon@21.0.0": { + "integrity": "sha512-+oHKZ0lTI+WVLxx1IbJDNmReQaIsQJjN2e7UUrJHEeByG7bFeKJYsv1E75JxTQ9QKJDp21bAa/0W2Xo4srsDnw==", "dependencies": [ "@types/sinonjs__fake-timers" ], - "tarball": "https://verdaccio.lossless.digital/@types/sinon/-/sinon-20.0.0.tgz" + "tarball": "https://verdaccio.lossless.digital/@types/sinon/-/sinon-21.0.0.tgz" }, "@types/sinonjs__fake-timers@15.0.1": { "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", @@ -4878,8 +4888,8 @@ "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", "tarball": "https://verdaccio.lossless.digital/form-data-encoder/-/form-data-encoder-2.1.4.tgz" }, - "form-data@4.0.4": { - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "form-data@4.0.5": { + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dependencies": [ "asynckit", "combined-stream", @@ -4887,7 +4897,7 @@ "hasown", "mime-types@2.1.35" ], - "tarball": "https://verdaccio.lossless.digital/form-data/-/form-data-4.0.4.tgz" + "tarball": "https://verdaccio.lossless.digital/form-data/-/form-data-4.0.5.tgz" }, "format@0.2.2": { "integrity": "d6170107e9efdc4ed30c9dc39016df942b5cb58b", @@ -8109,8 +8119,8 @@ "workspace": { "packageJson": { "dependencies": [ - "npm:@git.zone/tsbuild@^2.7.1", - "npm:@git.zone/tsrun@^1.6.2", + "npm:@git.zone/tsbuild@^3.1.0", + "npm:@git.zone/tsrun@2", "npm:@git.zone/tstest@^2.8.1", "npm:@push.rocks/lik@^6.2.2", "npm:@push.rocks/qenv@^6.1.3", diff --git a/package.json b/package.json index b138596..9273a30 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "mongodb": "^6.20.0" }, "devDependencies": { - "@git.zone/tsbuild": "^2.7.1", - "@git.zone/tsrun": "^1.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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8fd845..2493c0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,11 +46,11 @@ importers: version: 6.20.0(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7) devDependencies: '@git.zone/tsbuild': - specifier: ^2.7.1 - version: 2.7.1 + specifier: ^3.1.0 + version: 3.1.0 '@git.zone/tsrun': - specifier: ^1.6.2 - version: 1.6.2 + specifier: ^2.0.0 + version: 2.0.0 '@git.zone/tstest': specifier: ^2.8.1 version: 2.8.1(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.3) @@ -827,8 +827,8 @@ packages: '@esm-bundle/chai@4.3.4-fix.0': resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==} - '@git.zone/tsbuild@2.7.1': - resolution: {integrity: sha512-O8TTc+LBp8hYy5+zA6AdoqdQQtVXTAd1L0gS/Ihz+QXgXvMdQKVINwpDFu6LS5NdVrGXzxnB63NQpfg5COIPnQ==} + '@git.zone/tsbuild@3.1.0': + resolution: {integrity: sha512-j8lMd84pmzWiU6NG3e+pyu0o41oo6mQVfcZv8kDsCrQwZMhoQV9Jp87MlU0i/XI5IZkqDjelG8Kx1QhOmbK+iQ==} hasBin: true '@git.zone/tsbundle@2.5.1': @@ -843,6 +843,10 @@ packages: resolution: {integrity: sha512-SOHbQqBg3/769/jPQcdpPCmugdEtIJINiG0O6aWx+su91GvGhheha5dAhccsCutJYErr+aJcBqBYuUYfhOfkFQ==} hasBin: true + '@git.zone/tsrun@2.0.0': + resolution: {integrity: sha512-yA6zCjL+kn7xfZe6sL/m4K+zYqgkznG/pF6++i/E17iwzpG6dHmW+VZmYldHe86sW4DcLMvqM6CxM+KlgaEpKw==} + hasBin: true + '@git.zone/tstest@2.8.1': resolution: {integrity: sha512-0Sct9XsbrmAQgKNoW/jBNPMLllKVI+W6/aVkj9DEguiEnysmxLb3xRyoay06lxTGSBe5dA5uNULrdycdQ9slgQ==} hasBin: true @@ -6580,7 +6584,7 @@ snapshots: dependencies: '@types/chai': 4.3.20 - '@git.zone/tsbuild@2.7.1': + '@git.zone/tsbuild@3.1.0': dependencies: '@git.zone/tspublish': 1.10.3 '@push.rocks/early': 4.0.4 @@ -6643,6 +6647,12 @@ snapshots: '@push.rocks/smartshell': 3.3.0 tsx: 4.20.6 + '@git.zone/tsrun@2.0.0': + dependencies: + '@push.rocks/smartfile': 11.2.7 + '@push.rocks/smartshell': 3.3.0 + tsx: 4.20.6 + '@git.zone/tstest@2.8.1(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.3)': dependencies: '@api.global/typedserver': 3.0.79 diff --git a/readme.hints.md b/readme.hints.md index e69de29..db0e1ff 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -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 diff --git a/readme.md b/readme.md index 770a694..f5e2e89 100644 --- a/readme.md +++ b/readme.md @@ -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 { @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; - + @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 { 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; - + @svDb({ // Handle custom date formats serialize: (date) => date?.toISOString(), @@ -644,45 +647,45 @@ class Order extends SmartDataDbDoc { @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 { class HighPerformanceDoc extends SmartDataDbDoc { @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) { - return function classDecorator(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 = constructor as any; + const originalConstructor = value as any; + const constructor = value as { new (...args: any[]): any }; const getCollection = () => { if (!(dbArg instanceof SmartdataDb)) { @@ -71,7 +76,44 @@ export function Collection(dbArg: SmartdataDb | TDelayed) { configurable: true }); - return decoratedClass; + // 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; }; } @@ -89,7 +131,13 @@ export const setDefaultManagerForDoc = (managerArg: IManager, dbDocArg: T): * @param dbArg */ export function managed(managerArg?: TManager | TDelayed) { - return function classDecorator(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() { @@ -139,7 +187,46 @@ export function managed(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; }; } diff --git a/ts/classes.doc.ts b/ts/classes.doc.ts index 1385874..886d678 100644 --- a/ts/classes.doc.ts +++ b/ts/classes.doc.ts @@ -28,15 +28,42 @@ export interface SearchOptions { 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; +} export function globalSvDb() { - return (target: SmartDataDbDoc, 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, 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, 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, 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, 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]; + } + }); }; }