diff --git a/.azure-pipelines/compliance/CredScanSuppressions.json b/.azure-pipelines/compliance/CredScanSuppressions.json index 4408537b4..ca021f1e5 100644 --- a/.azure-pipelines/compliance/CredScanSuppressions.json +++ b/.azure-pipelines/compliance/CredScanSuppressions.json @@ -6,11 +6,7 @@ "_justification": "No need to scan external node modules." }, { - "file": "test\\mongoGetCommand.test.ts", - "_justification": "Fake credentials used for unit tests." - }, - { - "file": "src\\documentdb\\scrapbook\\mongoConnectionStrings.test.ts", + "file": "src\\documentdb\\mongoConnectionStrings.test.ts", "_justification": "Fake credentials used for unit tests." }, { diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e5a438ee1..aeb7ed884 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,6 @@ # GitHub Copilot Instructions for vscode-documentdb -VS Code Extension for Azure Cosmos DB and MongoDB. TypeScript (strict mode), React webviews, Jest testing. +VS Code Extension for Azure Cosmos DB and the MongoDB API. TypeScript (strict mode), React webviews, Jest testing. ## Critical Build Commands @@ -32,6 +32,12 @@ Before finishing work on a PR, agents **must** run the following steps in order: > โš ๏ธ **An agent must not finish or terminate until all three steps above have been run and pass successfully.** Skipping these steps leads to CI failures. +## Git Safety + +- **Never use `git add -f`** to force-add files. If `git add` refuses a file, it is likely in `.gitignore` for a reason (e.g., `docs/plan/`, `docs/analysis/`, build outputs). Do NOT override this with `-f`. +- When `git add` warns that a path is ignored, **stop and inform the user** instead of force-adding. +- Files in `docs/plan/` and `docs/analysis/` are **local planning documents** that must not be committed to the repository. + ## Project Structure | Folder | Purpose | @@ -178,6 +184,32 @@ For Discovery View, both `treeId` and `clusterId` are sanitized (all `/` replace See `src/tree/models/BaseClusterModel.ts` and `docs/analysis/08-cluster-model-simplification-plan.md` for details. +## Terminology + +This is a **DocumentDB** extension that uses the **MongoDB-compatible wire protocol**. + +- Use **"DocumentDB"** when referring to the database service itself. +- Use **"MongoDB API"** or **"DocumentDB API"** when referring to the wire protocol, query language, or API compatibility layer. +- **Never use "MongoDB" alone** as a product name in code, comments, docs, or user-facing strings. + +| โœ… Do | โŒ Don't | +| ---------------------------------------------------- | -------------------------------- | +| `// Query operators supported by the DocumentDB API` | `// MongoDB query operators` | +| `// BSON types per the MongoDB API spec` | `// Uses MongoDB's $match stage` | +| `documentdbQuery` (variable name) | `mongoQuery` | + +This applies to: code comments, JSDoc/TSDoc, naming (prefer `documentdb` prefix), user-facing strings, docs, and test descriptions. + +## TDD Contract Tests + +Test suites prefixed with `TDD:` (e.g., `describe('TDD: Completion Behavior', ...)`) are **behavior contracts** written before the implementation. If a `TDD:` test fails after a code change: + +1. **Do NOT automatically fix the test.** +2. **Stop and ask the user** whether the behavior change is intentional. +3. The user decides: update the contract (test) or fix the implementation. + +This applies to any test whose name starts with `TDD:`, regardless of folder location. + ## Additional Patterns For detailed patterns, see: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac02668fb..7ea04b6ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,6 +55,9 @@ jobs: - name: ๐Ÿ“ฆ Install Dependencies (npm ci) run: npm ci --prefer-offline --no-audit --no-fund --progress=false --verbose + - name: ๐Ÿ”จ Build Workspace Packages + run: npm run build --workspaces --if-present + - name: ๐ŸŒ Check Localization Files run: npm run l10n:check diff --git a/.gitignore b/.gitignore index e0bf99748..eddc516fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +/docs/analysis/ +/docs/plan/ + # User-specific files *.suo *.user @@ -157,6 +160,9 @@ PublishScripts/ **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ +# Include our monorepo packages at the root +!/packages/ +!/packages/** # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files @@ -268,6 +274,7 @@ dist stats.json *.tgz *.zip +*.tsbuildinfo # Scrapbooks *.mongo diff --git a/.vscode/settings.json b/.vscode/settings.json index 9bcd06e4a..a6a027735 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,33 +1,35 @@ { - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit", - "source.organizeImports": "explicit" - }, - "editor.detectIndentation": false, - "editor.formatOnSave": true, - "editor.formatOnPaste": false, - "editor.insertSpaces": true, - "editor.tabSize": 4, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "files.insertFinalNewline": true, - "files.trimTrailingWhitespace": true, - "search.exclude": { - "out": true, - "**/node_modules": true, - ".vscode-test": true - }, - "typescript.preferences.importModuleSpecifier": "relative", - "typescript.tsdk": "node_modules/typescript/lib", - "antlr4.generation": { - // Settings for "ANTLR4 grammar syntax support" extension - "mode": "internal", - "listeners": true, - "visitors": false - }, - "vscode-nmake-tools.workspaceBuildDirectories": ["."], - "vscode-nmake-tools.installOsRepoRustHelperExtension": false, - "sarif-viewer.connectToGithubCodeScanning": "off" - // "eslint.workingDirectories": [ - // ".", "src" - // ] + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" + }, + "editor.detectIndentation": false, + "editor.formatOnSave": true, + "editor.formatOnPaste": false, + "editor.insertSpaces": true, + "editor.tabSize": 4, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "search.exclude": { + "out": true, + "**/node_modules": true, + ".vscode-test": true + }, + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.tsdk": "node_modules/typescript/lib", + "antlr4.generation": { + // Settings for "ANTLR4 grammar syntax support" extension + "mode": "internal", + "listeners": true, + "visitors": false + }, + "vscode-nmake-tools.workspaceBuildDirectories": ["."], + "vscode-nmake-tools.installOsRepoRustHelperExtension": false, + "sarif-viewer.connectToGithubCodeScanning": "off", + "jest.runMode": "deferred", + "testing.automaticallyOpenTestResults": "neverOpen" + // "eslint.workingDirectories": [ + // ".", "src" + // ] } diff --git a/README.md b/README.md index a3210a85d..69fdfc50f 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,12 @@ Your feedback, contributions, and ideas shape the future of the extension. # Prerequisites -- **Mongo Shell Requirement (Optional)**: Some advanced commands in the Mongo [scrapbook](#mongo-scrapbooks), as well as use of the MongoDB shell, require installing [MongoDB shell](https://docs.mongodb.com/manual/installation/). +- **Shell Requirement (Optional)**: Use of the DocumentDB shell requires installing [mongosh](https://www.mongodb.com/docs/mongodb-shell/install/). ## Known Issues Here are some known issues and limitations to be aware of when using the DocumentDB VS Code extension: -- **Escaped Characters in Scrapbooks**: Scrapbook support for escaped characters is preliminary. Use double escaping for newlines (`\\n` instead of `\n`). - #### References diff --git a/extension.bundle.ts b/extension.bundle.ts index 547308d19..bae945823 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -22,16 +22,12 @@ export { AzureAccountTreeItemBase, createAzureClient } from '@microsoft/vscode-a // eslint-disable-next-line no-restricted-imports -- bundle intentionally re-exports many helpers for tests; nonNull helpers are provided locally in this repo export * from '@microsoft/vscode-azext-utils'; export { isWindows, wellKnownEmulatorPassword } from './src/constants'; -export { connectToClient, isCosmosEmulatorConnectionString } from './src/documentdb/scrapbook/connectToClient'; -export { MongoCommand } from './src/documentdb/scrapbook/MongoCommand'; +export { connectToClient, isCosmosEmulatorConnectionString } from './src/documentdb/connectToClient'; export { addDatabaseToAccountConnectionString, encodeMongoConnectionString, getDatabaseNameFromConnectionString, -} from './src/documentdb/scrapbook/mongoConnectionStrings'; -export * from './src/documentdb/scrapbook/registerScrapbookCommands'; -export { findCommandAtPosition, getAllCommandsFromText } from './src/documentdb/scrapbook/ScrapbookHelpers'; -export { ShellScriptRunner as MongoShell } from './src/documentdb/scrapbook/ShellScriptRunner'; +} from './src/documentdb/mongoConnectionStrings'; export { activateInternal, deactivateInternal } from './src/extension'; export { ext } from './src/extensionVariables'; export { SettingUtils } from './src/services/SettingsService'; diff --git a/grammar/JavaScript.tmLanguage.json b/grammar/JavaScript.tmLanguage.json deleted file mode 100644 index 0191da450..000000000 --- a/grammar/JavaScript.tmLanguage.json +++ /dev/null @@ -1,3711 +0,0 @@ -{ - "name.$comment": "** Changed **", - "name": "DocumentDB & MongoDB Scrapbooks (JavaScript)", - "scopeName.$comment": "** Changed **", - "scopeName": "source.mongo.js", - "fileTypes.$comment": "** Changed from .js/.jsx **", - "fileTypes": [".vscode-documentdb-scrapbook"], - "uuid.$comment": "** Changed **", - "uuid": "311c363f-7b1d-4dc4-b237-da46f717cca4", - "patterns": [ - { - "include": "#directives" - }, - { - "include": "#statements" - }, - { - "name": "comment.line.shebang.ts", - "match": "\\A(#!).*(?=$)", - "captures": { - "1": { - "name": "punctuation.definition.comment.ts" - } - } - } - ], - "repository": { - "statements": { - "patterns": [ - { - "include": "#string" - }, - { - "include": "#template" - }, - { - "include": "#comment" - }, - { - "include": "#declaration" - }, - { - "include": "#switch-statement" - }, - { - "include": "#for-loop" - }, - { - "include": "#after-operator-block" - }, - { - "include": "#decl-block" - }, - { - "include": "#control-statement" - }, - { - "include": "#expression" - }, - { - "include": "#punctuation-semicolon" - } - ] - }, - "var-expr": { - "name": "meta.var.expr.js", - "begin": "(?) |\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>]|\\<[^<>]+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>))\n ) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n )))\n )\n)", - "beginCaptures": { - "1": { - "name": "meta.definition.variable.js entity.name.function.js" - } - }, - "end": "(?=$|[;,=}]|(\\s+(of|in)\\s+))", - "patterns": [ - { - "include": "#var-single-variable-type-annotation" - } - ] - }, - { - "name": "meta.var-single-variable.expr.js", - "begin": "([[:upper:]][_$[:digit:][:upper:]]*)(?![_$[:alnum:]])", - "beginCaptures": { - "1": { - "name": "meta.definition.variable.js variable.other.constant.js" - } - }, - "end": "(?=$|[;,=}]|(\\s+(of|in)\\s+))", - "patterns": [ - { - "include": "#var-single-variable-type-annotation" - } - ] - }, - { - "name": "meta.var-single-variable.expr.js", - "begin": "([_$[:alpha:]][_$[:alnum:]]*)", - "beginCaptures": { - "1": { - "name": "meta.definition.variable.js variable.other.readwrite.js" - } - }, - "end": "(?=$|[;,=}]|(\\s+(of|in)\\s+))", - "patterns": [ - { - "include": "#var-single-variable-type-annotation" - } - ] - } - ] - }, - "var-single-variable-type-annotation": { - "patterns": [ - { - "include": "#type-annotation" - }, - { - "include": "#string" - }, - { - "include": "#comment" - } - ] - }, - "destructuring-variable": { - "patterns": [ - { - "name": "meta.object-binding-pattern-variable.js", - "begin": "(?) |\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>]|\\<[^<>]+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>))\n ) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n )))\n )\n)" - }, - { - "name": "meta.definition.property.js variable.object.property.js", - "match": "[_$[:alpha:]][_$[:alnum:]]*" - }, - { - "name": "keyword.operator.optional.js", - "match": "\\?" - } - ] - } - ] - }, - "method-declaration": { - "name": "meta.method.declaration.js", - "begin": "(?) |\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>]|\\<[^<>]+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>))\n ) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n )))\n )\n)", - "captures": { - "1": { - "name": "storage.modifier.js" - }, - "2": { - "name": "keyword.operator.rest.js" - }, - "3": { - "name": "entity.name.function.js" - }, - "4": { - "name": "keyword.operator.optional.js" - } - } - }, - { - "match": "(?:\\s*\\b(public|private|protected|readonly)\\s+)?(\\.\\.\\.)?\\s*(?])|(?<=[\\}>\\]\\)]|[_$[:alpha:]])\\s*(?=\\{)", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#type" - } - ] - }, - "type": { - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#string" - }, - { - "include": "#numeric-literal" - }, - { - "include": "#type-primitive" - }, - { - "include": "#type-builtin-literals" - }, - { - "include": "#type-parameters" - }, - { - "include": "#type-tuple" - }, - { - "include": "#type-object" - }, - { - "include": "#type-operators" - }, - { - "include": "#type-fn-type-parameters" - }, - { - "include": "#type-paren-or-function-parameters" - }, - { - "include": "#type-function-return-type" - }, - { - "include": "#type-name" - } - ] - }, - "function-parameters": { - "name": "meta.parameters.js", - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.parameters.begin.js" - } - }, - "end": "\\)", - "endCaptures": { - "0": { - "name": "punctuation.definition.parameters.end.js" - } - }, - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#decorator" - }, - { - "include": "#destructuring-parameter" - }, - { - "include": "#parameter-name" - }, - { - "include": "#type-annotation" - }, - { - "include": "#variable-initializer" - }, - { - "name": "punctuation.separator.parameter.js", - "match": "," - } - ] - }, - "type-primitive": { - "name": "support.type.primitive.js", - "match": "(?)\n ))\n )\n )\n)", - "end": "(?<=\\))", - "patterns": [ - { - "include": "#function-parameters" - } - ] - } - ] - }, - "type-operators": { - "patterns": [ - { - "include": "#typeof-operator" - }, - { - "name": "keyword.operator.type.js", - "match": "[&|]" - }, - { - "name": "keyword.operator.expression.keyof.js", - "match": "(?", - "beginCaptures": { - "0": { - "name": "storage.type.function.arrow.js" - } - }, - "end": "(?)(?=[,\\]\\)\\{\\}=;>]|//|$)", - "patterns": [ - { - "include": "#comment" - }, - { - "name": "meta.object.type.js", - "begin": "(?<==>)\\s*(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.block.js" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.definition.block.js" - } - }, - "patterns": [ - { - "include": "#type-object-members" - } - ] - }, - { - "include": "#type-predicate-operator" - }, - { - "include": "#type" - } - ] - }, - "type-tuple": { - "name": "meta.type.tuple.js", - "begin": "\\[", - "beginCaptures": { - "0": { - "name": "meta.brace.square.js" - } - }, - "end": "\\]", - "endCaptures": { - "0": { - "name": "meta.brace.square.js" - } - }, - "patterns": [ - { - "include": "#type" - }, - { - "include": "#punctuation-comma" - } - ] - }, - "type-name": { - "patterns": [ - { - "match": "([_$[:alpha:]][_$[:alnum:]]*)\\s*(\\.)", - "captures": { - "1": { - "name": "entity.name.type.module.js" - }, - "2": { - "name": "punctuation.accessor.js" - } - } - }, - { - "name": "entity.name.type.js", - "match": "[_$[:alpha:]][_$[:alnum:]]*" - } - ] - }, - "type-parameters": { - "name": "meta.type.parameters.js", - "begin": "(<)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.typeparameters.begin.js" - } - }, - "end": "(?=$)|(>)", - "endCaptures": { - "1": { - "name": "punctuation.definition.typeparameters.end.js" - } - }, - "patterns": [ - { - "include": "#comment" - }, - { - "name": "storage.modifier.js", - "match": "(?)" - }, - { - "include": "#type" - }, - { - "include": "#punctuation-comma" - } - ] - }, - "variable-initializer": { - "patterns": [ - { - "begin": "(?]|\\<[^<>]+\\>)+>\\s*)?\\()", - "captures": { - "1": { - "name": "punctuation.accessor.js" - }, - "2": { - "name": "support.constant.dom.js" - }, - "3": { - "name": "support.variable.property.dom.js" - } - } - }, - { - "name": "support.class.node.js", - "match": "(?x)(?]|\\<[^<>]+\\>)+>\\s*)?\\()", - "end": "(?<=\\))(?!(([_$[:alpha:]][_$[:alnum:]]*\\s*\\.\\s*)*|(\\.\\s*)?)([_$[:alpha:]][_$[:alnum:]]*)\\s*(<([^<>]|\\<[^<>]+\\>)+>\\s*)?\\()", - "patterns": [ - { - "include": "#literal" - }, - { - "include": "#support-objects" - }, - { - "include": "#object-identifiers" - }, - { - "include": "#punctuation-accessor" - }, - { - "name": "entity.name.function.js", - "match": "([_$[:alpha:]][_$[:alnum:]]*)" - }, - { - "include": "#comment" - }, - { - "name": "meta.type.parameters.js", - "begin": "\\<", - "beginCaptures": { - "0": { - "name": "punctuation.definition.typeparameters.begin.js" - } - }, - "end": "\\>", - "endCaptures": { - "0": { - "name": "punctuation.definition.typeparameters.end.js" - } - }, - "patterns": [ - { - "include": "#type" - }, - { - "include": "#punctuation-comma" - } - ] - }, - { - "include": "#paren-expression" - } - ] - }, - "identifiers": { - "patterns": [ - { - "include": "#object-identifiers" - }, - { - "match": "(?x)(?:(\\.)\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n (async\\s+)|(function\\s*[(<])|(function\\s+)|\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)|\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>]|\\<[^<>]+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)))", - "captures": { - "1": { - "name": "punctuation.accessor.js" - }, - "2": { - "name": "entity.name.function.js" - } - } - }, - { - "match": "(\\.)\\s*([[:upper:]][_$[:digit:][:upper:]]*)(?![_$[:alnum:]])", - "captures": { - "1": { - "name": "punctuation.accessor.js" - }, - "2": { - "name": "variable.other.constant.property.js" - } - } - }, - { - "match": "(\\.)\\s*([_$[:alpha:]][_$[:alnum:]]*)", - "captures": { - "1": { - "name": "punctuation.accessor.js" - }, - "2": { - "name": "variable.other.property.js" - } - } - }, - { - "name": "variable.other.constant.js", - "match": "([[:upper:]][_$[:digit:][:upper:]]*)(?![_$[:alnum:]])" - }, - { - "name": "variable.other.readwrite.js", - "match": "[_$[:alpha:]][_$[:alnum:]]*" - } - ] - }, - "object-identifiers": { - "patterns": [ - { - "name": "support.class.js", - "match": "([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\\.\\s*prototype\\b(?!\\$))" - }, - { - "match": "(?x)(\\.)\\s*(?:\n ([[:upper:]][_$[:digit:][:upper:]]*) |\n ([_$[:alpha:]][_$[:alnum:]]*)\n)(?=\\s*\\.\\s*[_$[:alpha:]][_$[:alnum:]]*)", - "captures": { - "1": { - "name": "punctuation.accessor.js" - }, - "2": { - "name": "variable.other.constant.object.property.js" - }, - "3": { - "name": "variable.other.object.property.js" - } - } - }, - { - "match": "(?x)(?:\n ([[:upper:]][_$[:digit:][:upper:]]*) |\n ([_$[:alpha:]][_$[:alnum:]]*)\n)(?=\\s*\\.\\s*[_$[:alpha:]][_$[:alnum:]]*)", - "captures": { - "1": { - "name": "variable.other.constant.object.js" - }, - "2": { - "name": "variable.other.object.js" - } - } - } - ] - }, - "cast": { - "patterns": [ - { - "include": "#jsx" - } - ] - }, - "new-expr": { - "name": "new.expr.js", - "begin": "(?)|\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>]|\\<[^<>]+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>))))", - "beginCaptures": { - "0": { - "name": "meta.object-literal.key.js" - }, - "1": { - "name": "entity.name.function.js" - }, - "2": { - "name": "punctuation.separator.key-value.js" - } - }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] - }, - { - "name": "meta.object.member.js", - "begin": "(?:[_$[:alpha:]][_$[:alnum:]]*)\\s*(:)", - "beginCaptures": { - "0": { - "name": "meta.object-literal.key.js" - }, - "1": { - "name": "punctuation.separator.key-value.js" - } - }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] - }, - { - "name": "meta.object.member.js", - "begin": "\\.\\.\\.", - "beginCaptures": { - "0": { - "name": "keyword.operator.spread.js" - } - }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] - }, - { - "name": "meta.object.member.js", - "match": "([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=,|\\}|$)", - "captures": { - "1": { - "name": "variable.other.readwrite.js" - } - } - }, - { - "include": "#punctuation-comma" - } - ] - }, - "expression-operators": { - "patterns": [ - { - "name": "keyword.control.flow.js", - "match": "(?>=|>>>=|\\|=" - }, - { - "name": "keyword.operator.bitwise.shift.js", - "match": "<<|>>>|>>" - }, - { - "name": "keyword.operator.comparison.js", - "match": "===|!==|==|!=" - }, - { - "name": "keyword.operator.relational.js", - "match": "<=|>=|<>|<|>" - }, - { - "name": "keyword.operator.logical.js", - "match": "\\!|&&|\\|\\|" - }, - { - "name": "keyword.operator.bitwise.js", - "match": "\\&|~|\\^|\\|" - }, - { - "name": "keyword.operator.assignment.js", - "match": "\\=" - }, - { - "name": "keyword.operator.decrement.js", - "match": "--" - }, - { - "name": "keyword.operator.increment.js", - "match": "\\+\\+" - }, - { - "name": "keyword.operator.arithmetic.js", - "match": "%|\\*|/|-|\\+" - }, - { - "match": "(?<=[_$[:alnum:])])\\s*(/)(?![/*])", - "captures": { - "1": { - "name": "keyword.operator.arithmetic.js" - } - } - } - ] - }, - "typeof-operator": { - "name": "keyword.operator.expression.typeof.js", - "match": "(?)", - "captures": { - "1": { - "name": "storage.modifier.async.js" - }, - "2": { - "name": "variable.parameter.js" - } - } - }, - { - "name": "meta.arrow.js", - "begin": "(?x) (?:\n (? is on new line\n (\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ([_$[:alpha:]][_$[:alnum:]]*\\s*:) | # [(]param:\n (\\.\\.\\.) # [(]...\n )\n ) |\n (\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends \n ) |\n # arrow function possible to detect only with => on same line\n (\n (<([^<>]|\\<[^<>]+\\>)+>\\s*)? # typeparameters\n \\(([^()]|\\([^()]*\\))*\\) # parameteres\n (\\s*:\\s*(.)*)? # return type\n \\s*=> # arrow operator\n )\n )\n)", - "beginCaptures": { - "1": { - "name": "storage.modifier.async.js" - } - }, - "end": "(?==>|\\{)", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#type-parameters" - }, - { - "include": "#function-parameters" - }, - { - "include": "#arrow-return-type" - } - ] - }, - { - "name": "meta.arrow.js", - "begin": "=>", - "beginCaptures": { - "0": { - "name": "storage.type.function.arrow.js" - } - }, - "end": "(?<=\\})|((?!\\{)(?=\\S))", - "patterns": [ - { - "include": "#decl-block" - }, - { - "include": "#expression" - } - ] - } - ] - }, - "arrow-return-type": { - "name": "meta.return.type.arrow.js", - "begin": "(?<=\\))\\s*(:)", - "beginCaptures": { - "1": { - "name": "keyword.operator.type.annotation.js" - } - }, - "end": "(?==>|\\{)", - "patterns": [ - { - "name": "meta.object.type.js", - "begin": "(?<=:)\\s*(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.block.js" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.definition.block.js" - } - }, - "patterns": [ - { - "include": "#type-object-members" - } - ] - }, - { - "include": "#type-predicate-operator" - }, - { - "include": "#type" - } - ] - }, - "punctuation-comma": { - "name": "punctuation.separator.comma.js", - "match": "," - }, - "punctuation-semicolon": { - "name": "punctuation.terminator.statement.js", - "match": ";" - }, - "punctuation-accessor": { - "name": "punctuation.accessor.js", - "match": "\\." - }, - "paren-expression": { - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "meta.brace.round.js" - } - }, - "end": "\\)", - "endCaptures": { - "0": { - "name": "meta.brace.round.js" - } - }, - "patterns": [ - { - "include": "#expression" - }, - { - "include": "#punctuation-comma" - } - ] - }, - "qstring-double": { - "name": "string.quoted.double.js", - "begin": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.js" - } - }, - "end": "(\")|((?:[^\\\\\\n])$)", - "endCaptures": { - "1": { - "name": "punctuation.definition.string.end.js" - }, - "2": { - "name": "invalid.illegal.newline.js" - } - }, - "patterns": [ - { - "include": "#string-character-escape" - } - ] - }, - "qstring-single": { - "name": "string.quoted.single.js", - "begin": "'", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.js" - } - }, - "end": "(\\')|((?:[^\\\\\\n])$)", - "endCaptures": { - "1": { - "name": "punctuation.definition.string.end.js" - }, - "2": { - "name": "invalid.illegal.newline.js" - } - }, - "patterns": [ - { - "include": "#string-character-escape" - } - ] - }, - "regex": { - "patterns": [ - { - "name": "string.regexp.js", - "begin": "(?<=[=(:,\\[?+!]|return|case|=>|&&|\\|\\||\\*\\/)\\s*(/)(?![/*])(?=(?:[^/\\\\\\[]|\\\\.|\\[([^\\]\\\\]|\\\\.)+\\])+/(?![/*])[gimy]*(?!\\s*[a-zA-Z0-9_$]))", - "beginCaptures": { - "1": { - "name": "punctuation.definition.string.begin.js" - } - }, - "end": "(/)([gimuy]*)", - "endCaptures": { - "1": { - "name": "punctuation.definition.string.end.js" - }, - "2": { - "name": "keyword.other.js" - } - }, - "patterns": [ - { - "include": "#regexp" - } - ] - }, - { - "name": "string.regexp.js", - "begin": "(?\\s*$)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.comment.js" - } - }, - "end": "(?=$)", - "patterns": [ - { - "name": "meta.tag.js", - "begin": "(<)(reference|amd-dependency|amd-module)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.tag.directive.js" - }, - "2": { - "name": "entity.name.tag.directive.js" - } - }, - "end": "/>", - "endCaptures": { - "0": { - "name": "punctuation.definition.tag.directive.js" - } - }, - "patterns": [ - { - "name": "entity.other.attribute-name.directive.js", - "match": "path|types|no-default-lib|name" - }, - { - "name": "keyword.operator.assignment.js", - "match": "=" - }, - { - "include": "#string" - } - ] - } - ] - }, - "docblock": { - "patterns": [ - { - "name": "storage.type.class.jsdoc", - "match": "(?x)(? # {Array} or {Object} type application (optional .)\n )\n (?:\n [\\.|~] # {Foo.bar} namespaced, {string|number} multiple, {Foo~bar} class-specific callback\n [a-zA-Z_$]+\n (?:\n (?:\n [\\w$]*\n (?:\\[\\])? # {(string|number[])} type application, a string or an array of numbers\n ) |\n \\.?<[\\w$]+(?:,\\s+[\\w$]+)*> # {Array} or {Object} type application (optional .)\n )\n )*\n \\) |\n [a-zA-Z_$]+\n (?:\n (?:\n [\\w$]*\n (?:\\[\\])? # {(string|number[])} type application, a string or an array of numbers\n ) |\n \\.?<[\\w$]+(?:,\\s+[\\w$]+)*> # {Array} or {Object} type application (optional .)\n )\n (?:\n [\\.|~] # {Foo.bar} namespaced, {string|number} multiple, {Foo~bar} class-specific callback\n [a-zA-Z_$]+\n (?:\n [\\w$]* |\n \\.?<[\\w$]+(?:,\\s+[\\w$]+)*> # {Array} or {Object} type application (optional .)\n )\n )*\n )\n )\n # Check for suffix\n (?:\\[\\])? # {string[]} type application, an array of strings\n =? # {string=} optional parameter\n )\n)})\n\n\\s+\n\n(\n \\[ # [foo] optional parameter\n \\s*\n (?:\n [a-zA-Z_$][\\w$]*\n (?:\n (?:\\[\\])? # Foo[].bar properties within an array\n \\. # Foo.Bar namespaced parameter\n [a-zA-Z_$][\\w$]*\n )*\n (?:\n \\s*\n = # [foo=bar] Default parameter value\n \\s*\n [\\w$\\s]*\n )?\n )\n \\s*\n \\] |\n (?:\n [a-zA-Z_$][\\w$]*\n (?:\n (?:\\[\\])? # Foo[].bar properties within an array\n \\. # Foo.Bar namespaced parameter\n [a-zA-Z_$][\\w$]*\n )*\n )?\n)\n\n\\s+\n\n(?:-\\s+)? # optional hyphen before the description\n\n((?:(?!\\*\\/).)*) # The type description", - "captures": { - "0": { - "name": "other.meta.jsdoc" - }, - "1": { - "name": "entity.name.type.instance.jsdoc" - }, - "2": { - "name": "variable.other.jsdoc" - }, - "3": { - "name": "other.description.jsdoc" - } - } - }, - { - "match": "(?x)\n\n({(?:\n \\* | # {*} any type\n \\? | # {?} unknown type\n\n (?:\n (?: # Check for a prefix\n \\? | # {?string} nullable type\n ! | # {!string} non-nullable type\n \\.{3} # {...string} variable number of parameters\n )?\n\n (?:\n (?:\n function # {function(string, number)} function type\n \\s*\n \\(\n \\s*\n (?:\n [a-zA-Z_$][\\w$]*\n (?:\n \\s*,\\s*\n [a-zA-Z_$][\\w$]*\n )*\n )?\n \\s*\n \\)\n (?: # {function(): string} function return type\n \\s*:\\s*\n [a-zA-Z_$][\\w$]*\n )?\n )?\n |\n (?:\n \\( # Opening bracket of multiple types with parenthesis {(string|number)}\n [a-zA-Z_$]+\n (?:\n [\\w$]* |\n \\.?<[\\w$]+(?:,\\s+[\\w$]+)*> # {Array} or {Object} type application (optional .)\n )\n (?:\n [\\.|~] # {Foo.bar} namespaced, {string|number} multiple, {Foo~bar} class-specific callback\n [a-zA-Z_$]+\n (?:\n [\\w$]* |\n \\.?<[\\w$]+(?:,\\s+[\\w$]+)*> # {Array} or {Object} type application (optional .)\n )\n )*\n \\) |\n [a-zA-Z_$]+\n (?:\n [\\w$]* |\n \\.?<[\\w$]+(?:,\\s+[\\w$]+)*> # {Array} or {Object} type application (optional .)\n )\n (?:\n [\\.|~] # {Foo.bar} namespaced, {string|number} multiple, {Foo~bar} class-specific callback\n [a-zA-Z_$]+\n (?:\n [\\w$]* |\n \\.?<[\\w$]+(?:,\\s+[\\w$]+)*> # {Array} or {Object} type application (optional .)\n )\n )*\n )\n )\n # Check for suffix\n (?:\\[\\])? # {string[]} type application, an array of strings\n =? # {string=} optional parameter\n )\n)})\n\n\\s+\n\n(?:-\\s+)? # optional hyphen before the description\n\n((?:(?!\\*\\/).)*) # The type description", - "captures": { - "0": { - "name": "other.meta.jsdoc" - }, - "1": { - "name": "entity.name.type.instance.jsdoc" - }, - "2": { - "name": "other.description.jsdoc" - } - } - } - ] - }, - "jsx-tag-attributes": { - "patterns": [ - { - "include": "#jsx-tag-attribute-name" - }, - { - "include": "#jsx-tag-attribute-assignment" - }, - { - "include": "#jsx-string-double-quoted" - }, - { - "include": "#jsx-string-single-quoted" - }, - { - "include": "#jsx-evaluated-code" - } - ] - }, - "jsx-tag-attribute-name": { - "match": "(?x)\n \\s*\n ([_$a-zA-Z][-$\\w]*)\n (?=\\s|=|/?>|/\\*|//)", - "captures": { - "1": { - "name": "entity.other.attribute-name.js" - } - } - }, - "jsx-tag-attribute-assignment": { - "name": "keyword.operator.assignment.js", - "match": "=(?=\\s*(?:'|\"|{|/\\*|//|\\n))" - }, - "jsx-string-double-quoted": { - "name": "string.quoted.double.js", - "begin": "\"", - "end": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.js" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.js" - } - }, - "patterns": [ - { - "include": "#jsx-entities" - } - ] - }, - "jsx-string-single-quoted": { - "name": "string.quoted.single.js", - "begin": "'", - "end": "'", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.js" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.js" - } - }, - "patterns": [ - { - "include": "#jsx-entities" - } - ] - }, - "jsx-entities": { - "patterns": [ - { - "name": "constant.character.entity.js", - "match": "(&)([a-zA-Z0-9]+|#[0-9]+|#x[0-9a-fA-F]+)(;)", - "captures": { - "1": { - "name": "punctuation.definition.entity.js" - }, - "3": { - "name": "punctuation.definition.entity.js" - } - } - }, - { - "name": "invalid.illegal.bad-ampersand.js", - "match": "&" - } - ] - }, - "jsx-evaluated-code": { - "name": "meta.embedded.expression.js", - "begin": "\\{", - "end": "\\}", - "beginCaptures": { - "0": { - "name": "punctuation.section.embedded.begin.js" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.embedded.end.js" - } - }, - "patterns": [ - { - "include": "#expression" - } - ] - }, - "jsx-tag-attributes-illegal": { - "name": "invalid.illegal.attribute.js", - "match": "\\S+" - }, - "jsx-tag-without-attributes": { - "name": "meta.tag.without-attributes.js", - "begin": "(<)\\s*([_$a-zA-Z][-$\\w.]*(?)", - "end": "()", - "beginCaptures": { - "1": { - "name": "punctuation.definition.tag.begin.js" - }, - "2": { - "name": "entity.name.tag.js" - }, - "3": { - "name": "punctuation.definition.tag.end.js" - } - }, - "endCaptures": { - "1": { - "name": "punctuation.definition.tag.begin.js" - }, - "2": { - "name": "entity.name.tag.js" - }, - "3": { - "name": "punctuation.definition.tag.end.js" - } - }, - "contentName": "meta.jsx.children.tsx", - "patterns": [ - { - "include": "#jsx-children" - } - ] - }, - "jsx-tag-in-expression": { - "begin": "(?x)\n (?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\Wreturn|^return|\\Wdefault|^)\\s*\n (?!(<)\\s*([_$a-zA-Z][-$\\w.]*(?)) #look ahead is not start of tag without attributes\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*\n ([_$a-zA-Z][-$\\w.]*(?))", - "end": "(/>)|(?:())", - "endCaptures": { - "0": { - "name": "meta.tag.js" - }, - "1": { - "name": "punctuation.definition.tag.end.js" - }, - "2": { - "name": "punctuation.definition.tag.begin.js" - }, - "3": { - "name": "entity.name.tag.js" - }, - "4": { - "name": "punctuation.definition.tag.end.js" - } - }, - "patterns": [ - { - "include": "#jsx-tag" - } - ] - }, - "jsx-child-tag": { - "begin": "(?x)\n (?=(<)\\s*\n ([_$a-zA-Z][-$\\w.]*(?))", - "end": "(/>)|(?:())", - "endCaptures": { - "0": { - "name": "meta.tag.js" - }, - "1": { - "name": "punctuation.definition.tag.end.js" - }, - "2": { - "name": "punctuation.definition.tag.begin.js" - }, - "3": { - "name": "entity.name.tag.js" - }, - "4": { - "name": "punctuation.definition.tag.end.js" - } - }, - "patterns": [ - { - "include": "#jsx-tag" - } - ] - }, - "jsx-tag": { - "name": "meta.tag.js", - "begin": "(?x)\n (?=(<)\\s*\n ([_$a-zA-Z][-$\\w.]*(?))", - "end": "(?=(/>)|(?:()))", - "patterns": [ - { - "begin": "(?x)\n (<)\\s*\n ([_$a-zA-Z][-$\\w.]*(?)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.tag.begin.js" - }, - "2": { - "name": "entity.name.tag.js" - } - }, - "end": "(?=[/]?>)", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#jsx-tag-attributes" - }, - { - "include": "#jsx-tag-attributes-illegal" - } - ] - }, - { - "begin": "(>)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.tag.end.js" - } - }, - "end": "(?=" - }, - "jsx-children": { - "patterns": [ - { - "include": "#jsx-tag-without-attributes" - }, - { - "include": "#jsx-child-tag" - }, - { - "include": "#jsx-tag-invalid" - }, - { - "include": "#jsx-evaluated-code" - }, - { - "include": "#jsx-entities" - } - ] - }, - "jsx": { - "patterns": [ - { - "include": "#jsx-tag-without-attributes" - }, - { - "include": "#jsx-tag-in-expression" - }, - { - "include": "#jsx-tag-invalid" - } - ] - } - }, - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/5c16c3ce4ede362f39fca054d7b25d85b25ecc68" -} diff --git a/grammar/Readme.md b/grammar/Readme.md deleted file mode 100644 index 7d4d7ce13..000000000 --- a/grammar/Readme.md +++ /dev/null @@ -1,10 +0,0 @@ -Note: The file `JavaScript.tmLanguage.json` is derived from [TypeScriptReact.tmLanguage](https://github.com/Microsoft/TypeScript-TmLanguage/blob/master/TypeScriptReact.tmLanguage). - -# To update the grammar after making changes: - -1. npm run update-grammar -2. Re-comment imports in mongoParser.ts that are not used and cause compile errors - -# Debugging the grammar - -See instructions in launch.json. Be sure to explicitly save the mongo.g4 file to generate the debug info before trying to launch. diff --git a/grammar/Regular Expressions (JavaScript).tmLanguage b/grammar/Regular Expressions (JavaScript).tmLanguage deleted file mode 100644 index 18addf254..000000000 --- a/grammar/Regular Expressions (JavaScript).tmLanguage +++ /dev/null @@ -1,240 +0,0 @@ - - - - - fileTypes - - hideFromUser - - name - - Mongo Scrapbooks Regular Expressions (JavaScript) - patterns - - - include - #regexp - - - repository - - regex-character-class - - patterns - - - match - \\[wWsSdD]|\. - name - constant.character.character-class.regexp - - - match - \\([0-7]{3}|x\h\h|u\h\h\h\h) - name - constant.character.numeric.regexp - - - match - \\c[A-Z] - name - constant.character.control.regexp - - - match - \\. - name - constant.character.escape.backslash.regexp - - - - regexp - - patterns - - - match - \\[bB]|\^|\$ - name - keyword.control.anchor.regexp - - - match - \\[1-9]\d* - name - keyword.other.back-reference.regexp - - - match - [?+*]|\{(\d+,\d+|\d+,|,\d+|\d+)\}\?? - name - keyword.operator.quantifier.regexp - - - match - \| - name - keyword.operator.or.regexp - - - begin - (\()((\?=)|(\?!)) - beginCaptures - - 1 - - name - punctuation.definition.group.regexp - - 3 - - name - meta.assertion.look-ahead.regexp - - 4 - - name - meta.assertion.negative-look-ahead.regexp - - - end - (\)) - endCaptures - - 1 - - name - punctuation.definition.group.regexp - - - name - meta.group.assertion.regexp - patterns - - - include - #regexp - - - - - begin - \((\?:)? - beginCaptures - - 0 - - name - punctuation.definition.group.regexp - - - end - \) - endCaptures - - 0 - - name - punctuation.definition.group.regexp - - - name - meta.group.regexp - patterns - - - include - #regexp - - - - - begin - (\[)(\^)? - beginCaptures - - 1 - - name - punctuation.definition.character-class.regexp - - 2 - - name - keyword.operator.negation.regexp - - - end - (\]) - endCaptures - - 1 - - name - punctuation.definition.character-class.regexp - - - name - constant.other.character-class.set.regexp - patterns - - - captures - - 1 - - name - constant.character.numeric.regexp - - 2 - - name - constant.character.control.regexp - - 3 - - name - constant.character.escape.backslash.regexp - - 4 - - name - constant.character.numeric.regexp - - 5 - - name - constant.character.control.regexp - - 6 - - name - constant.character.escape.backslash.regexp - - - match - (?:.|(\\(?:[0-7]{3}|x\h\h|u\h\h\h\h))|(\\c[A-Z])|(\\.))\-(?:[^\]\\]|(\\(?:[0-7]{3}|x\h\h|u\h\h\h\h))|(\\c[A-Z])|(\\.)) - name - constant.other.character-class.range.regexp - - - include - #regex-character-class - - - - - include - #regex-character-class - - - - - scopeName - - source.mongo.js.regexp - uuid - - c362a36f-6fd7-49c1-b7fb-90f53cdb7ee1 - - diff --git a/grammar/configuration.json b/grammar/configuration.json deleted file mode 100644 index c70acdfb0..000000000 --- a/grammar/configuration.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "comments": { - "lineComment": "//", - "blockComment": ["/*", "*/"] - }, - "brackets": [ - ["{", "}"], - ["[", "]"] - ], - "autoClosingPairs": [ - { - "open": "{", - "close": "}" - }, - { - "open": "[", - "close": "]" - }, - { - "open": "(", - "close": ")" - }, - { - "open": "'", - "close": "'", - "notIn": ["string", "comment"] - }, - { - "open": "\"", - "close": "\"", - "notIn": ["string"] - }, - { - "open": "/**", - "close": " */", - "notIn": ["string"] - } - ] -} diff --git a/grammar/mongo.g4 b/grammar/mongo.g4 deleted file mode 100644 index f2f0e5ad5..000000000 --- a/grammar/mongo.g4 +++ /dev/null @@ -1,109 +0,0 @@ -grammar mongo; - -@header { -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -} - -@lexer::members { - private isExternalIdentifierText(text) { - return text === 'db'; - } -} - -mongoCommands: commands EOF; - -commands: ( command | emptyCommand | comment)*; - -command: DB (DOT collection)? (DOT functionCall)+ SEMICOLON?; - -emptyCommand: SEMICOLON; - -collection: IDENTIFIER (DOT IDENTIFIER)*; - -functionCall: FUNCTION_NAME = IDENTIFIER arguments; - -arguments: - OPEN_PARENTHESIS = '(' (argument ( ',' argument)*)? CLOSED_PARENTHESIS = ')'; - -argument: literal | objectLiteral | arrayLiteral; - -objectLiteral: '{' propertyNameAndValueList? ','? '}'; - -arrayLiteral: '[' elementList? ']'; - -elementList: propertyValue ( ',' propertyValue)*; - -propertyNameAndValueList: - propertyAssignment (',' propertyAssignment)*; - -propertyAssignment: propertyName ':' propertyValue; - -propertyValue: - literal - | objectLiteral - | arrayLiteral - | functionCall; - -literal: (NullLiteral | BooleanLiteral | StringLiteral) - | RegexLiteral - | NumericLiteral; - -propertyName: StringLiteral | IDENTIFIER; - -comment: SingleLineComment | MultiLineComment; - -RegexLiteral: - '/' (~[/\n\r*] | '\\/') (~[/\n\r] | '\\/')* '/' (RegexFlag)*; -// Disallow '*' to succeed the opening '/'. This ensures we don't wrongly parse multi-line comments. -// Disallow carriage returns too. - -fragment RegexFlag: [gimuy]; - -SingleLineComment: - '//' ~[\r\n\u2028\u2029]* -> channel(HIDDEN); - -MultiLineComment: '/*' .*? '*/' -> channel(HIDDEN); - -StringLiteral: - SINGLE_QUOTED_STRING_LITERAL - | DOUBLE_QUOTED_STRING_LITERAL; - -NullLiteral: 'null'; - -BooleanLiteral: 'true' | 'false'; - -NumericLiteral: '-'? DecimalLiteral; - -DecimalLiteral: - DecimalIntegerLiteral '.' DecimalDigit+ ExponentPart? - | '.' DecimalDigit+ ExponentPart? - | DecimalIntegerLiteral ExponentPart?; - -LineTerminator: [\r\n\u2028\u2029] -> channel(HIDDEN); - -SEMICOLON: ';'; -DOT: '.'; -DB: 'db'; - -// Don't declare LR/CRLF tokens - they'll interfere with matching against LineTerminator LF: '\n'; -// CRLF: '\r\n'; - -IDENTIFIER: ((~[[\]"',\\ \t\n\r:.;(){}\-]) | STRING_ESCAPE)+ {!this.isExternalIdentifierText(this.text) - }?; -DOUBLE_QUOTED_STRING_LITERAL: - '"' ((~["\\]) | STRING_ESCAPE)* '"'; -SINGLE_QUOTED_STRING_LITERAL: - '\'' ((~['\\]) | STRING_ESCAPE)* '\''; - -fragment STRING_ESCAPE: '\\' [\\"\\']; - -fragment DecimalIntegerLiteral: '0' | [1-9] DecimalDigit*; - -fragment ExponentPart: [eE] [+-]? DecimalDigit+; - -fragment DecimalDigit: [0-9]; - -WHITESPACE: [ \t] -> skip; diff --git a/jest.config.js b/jest.config.js index 7ad26361a..ca22ee433 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,11 +1,18 @@ /** @type {import('ts-jest').JestConfigWithTsJest} **/ module.exports = { - testEnvironment: 'node', - testMatch: ['/src/**/*.test.ts'], - transform: { - '^.+.tsx?$': ['ts-jest', {}], - }, // Limit workers to avoid OOM kills on machines with many cores. // Each ts-jest worker loads the TypeScript compiler and consumes ~500MB+. maxWorkers: '50%', + projects: [ + { + displayName: 'extension', + testEnvironment: 'node', + testMatch: ['/src/**/*.test.ts'], + transform: { + '^.+\\.tsx?$': ['ts-jest', {}], + }, + }, + '/packages/schema-analyzer', + '/packages/documentdb-constants', + ], }; diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index a4493f78a..ec52f6241 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -81,6 +81,8 @@ "[Query Insights Stage 3] Using cached execution plan from Stage 2 (requestKey: {key})": "[Query Insights Stage 3] Using cached execution plan from Stage 2 (requestKey: {key})", "[Reader] Counting documents in {0}.{1}": "[Reader] Counting documents in {0}.{1}", "[Reader] Document count result: {0} documents": "[Reader] Document count result: {0} documents", + "[SchemaStore] Clearing schema cache: {0} collections, {1} documents, {2} fields": "[SchemaStore] Clearing schema cache: {0} collections, {1} documents, {2} fields", + "[SchemaStore] Schema cache cleared: {0} collections, {1} documents, {2} fields": "[SchemaStore] Schema cache cleared: {0} collections, {1} documents, {2} fields", "[StreamingWriter] Abort signal received during streaming": "[StreamingWriter] Abort signal received during streaming", "[StreamingWriter] Buffer flush complete ({0} total processed so far)": "[StreamingWriter] Buffer flush complete ({0} total processed so far)", "[StreamingWriter] Fatal error ({0}): {1}": "[StreamingWriter] Fatal error ({0}): {1}", @@ -104,6 +106,8 @@ "{0} task(s) are using connections in this folder. Check the Output panel for details.": "{0} task(s) are using connections in this folder. Check the Output panel for details.", "{0} tenants available ({1} signed in)": "{0} tenants available ({1} signed in)", "{0} was stopped": "{0} was stopped", + "{0}/{1} โ€” Error": "{0}/{1} โ€” Error", + "{0}/{1} โ€” Results": "{0}/{1} โ€” Results", "{0}/{1} documents": "{0}/{1} documents", "{countMany} documents have been deleted.": "{countMany} documents have been deleted.", "{countOne} document has been deleted.": "{countOne} document has been deleted.", @@ -111,11 +115,7 @@ "{experienceName} Emulator": "{experienceName} Emulator", "**No public IP or FQDN available for direct connection.**": "**No public IP or FQDN available for direct connection.**", "/ (Root)": "/ (Root)", - "โฉ Run All": "โฉ Run All", - "โณ Running Allโ€ฆ": "โณ Running Allโ€ฆ", - "โณ Running Commandโ€ฆ": "โณ Running Commandโ€ฆ", "โ–  Task '{taskName}' was stopped. {message}": "โ–  Task '{taskName}' was stopped. {message}", - "โ–ถ๏ธ Run Command": "โ–ถ๏ธ Run Command", "โ–บ Task '{taskName}' starting...": "โ–บ Task '{taskName}' starting...", "โ—‹ Task '{taskName}' initializing...": "โ—‹ Task '{taskName}' initializing...", "โš ๏ธ **Security:** TLS/SSL Disabled": "โš ๏ธ **Security:** TLS/SSL Disabled", @@ -142,10 +142,6 @@ "$(warning) Some storage accounts were filtered because of their network configurations.": "$(warning) Some storage accounts were filtered because of their network configurations.", "1 tenant available (0 signed in)": "1 tenant available (0 signed in)", "1 tenant available (1 signed in)": "1 tenant available (1 signed in)", - "1. Locating the one you'd like from the DocumentDB side panel,": "1. Locating the one you'd like from the DocumentDB side panel,", - "2. Selecting a database or a collection,": "2. Selecting a database or a collection,", - "3. Right-clicking and then choosing the \"Mongo Scrapbook\" submenu,": "3. Right-clicking and then choosing the \"Mongo Scrapbook\" submenu,", - "4. Selecting the \"Connect to this database\" command.": "4. Selecting the \"Connect to this database\" command.", "A collection with the name \"{0}\" already exists": "A collection with the name \"{0}\" already exists", "A connection name is required.": "A connection name is required.", "A connection with the same username and host already exists.": "A connection with the same username and host already exists.", @@ -188,6 +184,7 @@ "Authenticate to Connect with Your DocumentDB Cluster": "Authenticate to Connect with Your DocumentDB Cluster", "Authenticate using a username and password": "Authenticate using a username and password", "Authenticate using Microsoft Entra ID (Azure AD)": "Authenticate using Microsoft Entra ID (Azure AD)", + "Authenticatingโ€ฆ": "Authenticatingโ€ฆ", "Authentication configuration is missing for \"{cluster}\".": "Authentication configuration is missing for \"{cluster}\".", "Authentication data (primary connection string) is missing for \"{cluster}\".": "Authentication data (primary connection string) is missing for \"{cluster}\".", "Authentication data (properties.connectionString) is missing for \"{cluster}\".": "Authentication data (properties.connectionString) is missing for \"{cluster}\".", @@ -217,7 +214,6 @@ "Back": "Back", "Back to account selection": "Back to account selection", "Back to tenant selection": "Back to tenant selection", - "Browse to {mongoExecutableFileName}": "Browse to {mongoExecutableFileName}", "Bulk write error during import into \"{0}.{1}\": {2} document(s) inserted.": "Bulk write error during import into \"{0}.{1}\": {2} document(s) inserted.", "Cancel": "Cancel", "Cancel this operation": "Cancel this operation", @@ -242,6 +238,8 @@ "Clear Query": "Clear Query", "Click here to retry": "Click here to retry", "Click here to update credentials": "Click here to update credentials", + "Click to learn how to connect": "Click to learn how to connect", + "Click to learn how to connect a database for the DocumentDB Scratchpad": "Click to learn how to connect a database for the DocumentDB Scratchpad", "Click to view resource": "Click to view resource", "Close tips": "Close tips", "Cluster metadata not initialized. Client may not be properly connected.": "Cluster metadata not initialized. Client may not be properly connected.", @@ -267,7 +265,8 @@ "Configuring tenant filteringโ€ฆ": "Configuring tenant filteringโ€ฆ", "Conflict Resolution: {strategyName}": "Conflict Resolution: {strategyName}", "Connect to a database": "Connect to a database", - "Connected to \"{name}\"": "Connected to \"{name}\"", + "Connect to a database before running. Right-click a database in the DocumentDB panel.": "Connect to a database before running. Right-click a database in the DocumentDB panel.", + "Connected to {0}": "Connected to {0}", "Connected to the cluster \"{cluster}\".": "Connected to the cluster \"{cluster}\".", "Connecting to \"{cluster}\"โ€ฆ": "Connecting to \"{cluster}\"โ€ฆ", "Connecting to the cluster as \"{username}\"โ€ฆ": "Connecting to the cluster as \"{username}\"โ€ฆ", @@ -368,6 +367,9 @@ "DocumentDB for VS Code is not signed in to Azure": "DocumentDB for VS Code is not signed in to Azure", "DocumentDB Local": "DocumentDB Local", "DocumentDB Performance Tips": "DocumentDB Performance Tips", + "DocumentDB Scratchpad": "DocumentDB Scratchpad", + "DocumentDB Scratchpad connected to {0}": "DocumentDB Scratchpad connected to {0}", + "DocumentDB Scratchpad connected to {0}/{1}": "DocumentDB Scratchpad connected to {0}/{1}", "Documents": "Documents", "Documents Examined": "Documents Examined", "Documents Returned": "Documents Returned", @@ -410,6 +412,7 @@ "Error creating resource: {0}": "Error creating resource: {0}", "Error deleting selected documents": "Error deleting selected documents", "Error dropping index: {error}": "Error dropping index: {error}", + "Error executing query": "Error executing query", "Error exporting documents: {error}": "Error exporting documents: {error}", "Error generating query": "Error generating query", "Error inserting documents into \"{0}.{1}\": {2}": "Error inserting documents into \"{0}.{1}\": {2}", @@ -418,7 +421,6 @@ "Error running process: ": "Error running process: ", "Error saving the document": "Error saving the document", "Error validating collection name availability: {0}": "Error validating collection name availability: {0}", - "Error while loading the autocompletion data": "Error while loading the autocompletion data", "Error while loading the data": "Error while loading the data", "Error while loading the document": "Error while loading the document", "Error while refreshing the document": "Error while refreshing the document", @@ -429,18 +431,15 @@ "Examined-to-Returned Ratio": "Examined-to-Returned Ratio", "Excellent": "Excellent", "Execute the find query": "Execute the find query", - "Executing all commands in shellโ€ฆ": "Executing all commands in shellโ€ฆ", + "Executed in {0}ms": "Executed in {0}ms", "Executing explain(aggregate) for collection: {collection}, pipeline stages: {stageCount}": "Executing explain(aggregate) for collection: {collection}, pipeline stages: {stageCount}", "Executing explain(count) for collection: {collection}": "Executing explain(count) for collection: {collection}", "Executing explain(find) for collection: {collection}": "Executing explain(find) for collection: {collection}", - "Executing the command in shellโ€ฆ": "Executing the command in shellโ€ฆ", "Execution Strategy": "Execution Strategy", "Execution Time": "Execution Time", "Execution timed out": "Execution timed out", "Execution timed out.": "Execution timed out.", "Exit": "Exit", - "Expected a file name \"{0}\", but the selected filename is \"{1}\"": "Expected a file name \"{0}\", but the selected filename is \"{1}\"", - "Expecting parentheses or quotes at \"{text}\"": "Expecting parentheses or quotes at \"{text}\"", "Explain(aggregate) completed [{durationMs}ms]": "Explain(aggregate) completed [{durationMs}ms]", "Explain(count) completed [{durationMs}ms]": "Explain(count) completed [{durationMs}ms]", "Explain(find) completed [{durationMs}ms]": "Explain(find) completed [{durationMs}ms]", @@ -524,7 +523,7 @@ "Failed to validate source collection: {0}": "Failed to validate source collection: {0}", "Failed with code \"{0}\".": "Failed with code \"{0}\".", "Fair": "Fair", - "Filter: Enter the DocumentDB query filter in JSON format": "Filter: Enter the DocumentDB query filter in JSON format", + "Filter: Enter the DocumentDB query filter": "Filter: Enter the DocumentDB query filter", "Find Query": "Find Query", "Finished importing": "Finished importing", "Folder name cannot be empty": "Folder name cannot be empty", @@ -602,6 +601,7 @@ "Info from the webview: ": "Info from the webview: ", "Information was confusing": "Information was confusing", "Initializing task...": "Initializing task...", + "Initializingโ€ฆ": "Initializingโ€ฆ", "Inserted {0} document(s).": "Inserted {0} document(s).", "Install Azure Account Extension...": "Install Azure Account Extension...", "Internal error: connectionString must be defined.": "Internal error: connectionString must be defined.", @@ -619,7 +619,7 @@ "Invalid Connection String: {error}": "Invalid Connection String: {error}", "Invalid connection type selected.": "Invalid connection type selected.", "Invalid document ID: {0}": "Invalid document ID: {0}", - "Invalid filter syntax: {0}. Please use valid JSON, for example: { \"name\": \"value\" }": "Invalid filter syntax: {0}. Please use valid JSON, for example: { \"name\": \"value\" }", + "Invalid filter syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { name: \"value\" }": "Invalid filter syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { name: \"value\" }", "Invalid folder type.": "Invalid folder type.", "Invalid mongoShell command format": "Invalid mongoShell command format", "Invalid node type.": "Invalid node type.", @@ -627,10 +627,10 @@ "Invalid payload for drop index action": "Invalid payload for drop index action", "Invalid payload for modify index action": "Invalid payload for modify index action", "Invalid projection syntax: {0}": "Invalid projection syntax: {0}", - "Invalid projection syntax: {0}. Please use valid JSON, for example: { \"fieldName\": 1 }": "Invalid projection syntax: {0}. Please use valid JSON, for example: { \"fieldName\": 1 }", + "Invalid projection syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { fieldName: 1 }": "Invalid projection syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { fieldName: 1 }", "Invalid semver \"{0}\".": "Invalid semver \"{0}\".", "Invalid sort syntax: {0}": "Invalid sort syntax: {0}", - "Invalid sort syntax: {0}. Please use valid JSON, for example: { \"fieldName\": 1 }": "Invalid sort syntax: {0}. Please use valid JSON, for example: { \"fieldName\": 1 }", + "Invalid sort syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { fieldName: 1 }": "Invalid sort syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { fieldName: 1 }", "It could be better": "It could be better", "It looks like there aren't any other folders to move these items into.\nYou might want to create a new folder first.\n\nNote: You can't move items between 'DocumentDB Local' and regular connections.": "It looks like there aren't any other folders to move these items into.\nYou might want to create a new folder first.\n\nNote: You can't move items between 'DocumentDB Local' and regular connections.", "item": "item", @@ -684,8 +684,6 @@ "Modify index?": "Modify index?", "Modify Indexโ€ฆ": "Modify Indexโ€ฆ", "Modifying index visibility ({action}) for \"{indexName}\" on collection: {collection}": "Modifying index visibility ({action}) for \"{indexName}\" on collection: {collection}", - "Mongo Shell connected.": "Mongo Shell connected.", - "Mongo Shell Error: {error}": "Mongo Shell Error: {error}", "MongoDB Emulator": "MongoDB Emulator", "Monitor Index Usage": "Monitor Index Usage", "Move": "Move", @@ -713,10 +711,11 @@ "No Azure VMs found with tag \"{tagName}\" in subscription \"{subscriptionName}\".": "No Azure VMs found with tag \"{tagName}\" in subscription \"{subscriptionName}\".", "No collection has been marked for copy. Please use \"Copy Collection...\" first to select a source collection.": "No collection has been marked for copy. Please use \"Copy Collection...\" first to select a source collection.", "No collection selected.": "No collection selected.", - "No commands found in this document.": "No commands found in this document.", "No Connectivity": "No Connectivity", + "No credentials found for cluster {0}": "No credentials found for cluster {0}", "No credentials found for id {clusterId}": "No credentials found for id {clusterId}", "No credentials found for the selected cluster.": "No credentials found for the selected cluster.", + "No database connected": "No database connected", "No folder selected.": "No folder selected.", "No index changes needed at this time.": "No index changes needed at this time.", "No index selected.": "No index selected.", @@ -724,9 +723,7 @@ "No matching resources found.": "No matching resources found.", "No node selected.": "No node selected.", "No parent folder selected.": "No parent folder selected.", - "No properties found in the schema at path \"{0}\"": "No properties found in the schema at path \"{0}\"", "No public connectivity": "No public connectivity", - "No result returned from the MongoDB shell.": "No result returned from the MongoDB shell.", "No results found": "No results found", "No scope was provided for the role assignment.": "No scope was provided for the role assignment.", "No session found for id {sessionId}": "No session found for id {sessionId}", @@ -739,7 +736,6 @@ "No tenants selected. Tenant filtering disabled (all tenants will be shown).": "No tenants selected. Tenant filtering disabled (all tenants will be shown).", "No, only copy documents": "No, only copy documents", "None": "None", - "Not connected to any MongoDB database.": "Not connected to any MongoDB database.", "Note: This confirmation type can be configured in the extension settings.": "Note: This confirmation type can be configured in the extension settings.", "Note: You can disable these URL handling confirmations in the extension settings.": "Note: You can disable these URL handling confirmations in the extension settings.", "Number of documents returned by the query": "Number of documents returned by the query", @@ -747,10 +743,10 @@ "Number of index keys scanned during query execution. Lower is better.": "Number of index keys scanned during query execution. Lower is better.", "OK": "OK", "Open Collection": "Open Collection", - "Open installation page": "Open installation page", "Open the VS Code Marketplace to learn more about \"{0}\"": "Open the VS Code Marketplace to learn more about \"{0}\"", "Opening DocumentDB connectionโ€ฆ": "Opening DocumentDB connectionโ€ฆ", "Operation cancelled.": "Operation cancelled.", + "Operation timed out after {0} seconds": "Operation timed out after {0} seconds", "Optimization Opportunities": "Optimization Opportunities", "Optimize Index Strategy": "Optimize Index Strategy", "Optimizing the index on {0} can improve query performance by better matching the query pattern.": "Optimizing the index on {0} can improve query performance by better matching the query pattern.", @@ -766,7 +762,6 @@ "Pick \"{number}\" to confirm and continue.": "Pick \"{number}\" to confirm and continue.", "Please authenticate first by expanding the tree item of the selected cluster.": "Please authenticate first by expanding the tree item of the selected cluster.", "Please confirm by re-entering the previous value.": "Please confirm by re-entering the previous value.", - "Please connect to a MongoDB database before running a Scrapbook command.": "Please connect to a MongoDB database before running a Scrapbook command.", "Please edit the connection string.": "Please edit the connection string.", "Please enter a new connection name.": "Please enter a new connection name.", "Please enter a valid tenant ID in GUID format (e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012)": "Please enter a valid tenant ID in GUID format (e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012)", @@ -831,13 +826,23 @@ "report an issue": "report an issue", "Report an issue": "Report an issue", "Resource group \"{0}\" already exists in subscription \"{1}\".": "Resource group \"{0}\" already exists in subscription \"{1}\".", + "Result: {0}": "Result: {0}", + "Result: Array ({0} elements)": "Result: Array ({0} elements)", + "Result: Cursor ({0} documents)": "Result: Cursor ({0} documents)", "Results found": "Results found", "Retry": "Retry", "Reusing active connection for \"{cluster}\".": "Reusing active connection for \"{cluster}\".", "Revisit connection details and try again.": "Revisit connection details and try again.", + "Right-click a database or collection in the DocumentDB panel and select \"Connect Scratchpad to this database\".": "Right-click a database or collection in the DocumentDB panel and select \"Connect Scratchpad to this database\".", "Role assignment \"{0}\" created for the {2} resource \"{1}\".": "Role assignment \"{0}\" created for the {2} resource \"{1}\".", "Role Assignment {0} created for {1}": "Role Assignment {0} created for {1}", "Role Assignment {0} failed for {1}": "Role Assignment {0} failed for {1}", + "Run": "Run", + "Run All": "Run All", + "Run the entire file ({0}+Shift+Enter)": "Run the entire file ({0}+Shift+Enter)", + "Run this block ({0}+Enter)": "Run this block ({0}+Enter)", + "Running queryโ€ฆ": "Running queryโ€ฆ", + "Runningโ€ฆ": "Runningโ€ฆ", "Save": "Save", "Save credentials for future connections.": "Save credentials for future connections.", "Save credentials for future use?": "Save credentials for future use?", @@ -846,9 +851,9 @@ "Save to the database": "Save to the database", "Saving \"{path}\" will update the entity \"{name}\" to the cloud.": "Saving \"{path}\" will update the entity \"{name}\" to the cloud.", "Saving credentials for \"{clusterName}\"โ€ฆ": "Saving credentials for \"{clusterName}\"โ€ฆ", + "Scratchpad execution failed: {0}": "Scratchpad execution failed: {0}", "See output for more details.": "See output for more details.", "Select {0}": "Select {0}", - "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", "Select a location for new resources.": "Select a location for new resources.", "Select a tenant for Microsoft Entra ID authentication": "Select a tenant for Microsoft Entra ID authentication", "Select a workspace folder": "Select a workspace folder", @@ -984,8 +989,6 @@ "The name can only contain lowercase letters and numbers.": "The name can only contain lowercase letters and numbers.", "The name cannot end in a period.": "The name cannot end in a period.", "The name must be between {0} and {1} characters.": "The name must be between {0} and {1} characters.", - "The output window may contain additional information.": "The output window may contain additional information.", - "The process exited prematurely.": "The process exited prematurely.", "The selected authentication method is not supported.": "The selected authentication method is not supported.", "The selected connection has been removed.": "The selected connection has been removed.", "The selected folder has been removed.": "The selected folder has been removed.", @@ -1000,7 +1003,6 @@ "This cannot be undone.": "This cannot be undone.", "This field is not set": "This field is not set", "This functionality requires installing the Azure Account extension.": "This functionality requires installing the Azure Account extension.", - "This functionality requires the Mongo DB shell, but we could not find it in the path or using the documentDB.mongoShell.path setting.": "This functionality requires the Mongo DB shell, but we could not find it in the path or using the documentDB.mongoShell.path setting.", "This functionality requires updating the Azure Account extension to at least version \"{0}\".": "This functionality requires updating the Azure Account extension to at least version \"{0}\".", "This index on {0} is not being used and adds unnecessary overhead to write operations.": "This index on {0} is not being used and adds unnecessary overhead to write operations.", "This operation is not supported as it would create a circular dependency and never terminate. Please select a different target collection or database.": "This operation is not supported as it would create a circular dependency and never terminate. Please select a different target collection or database.", @@ -1013,19 +1015,14 @@ "This will allow the query planner to use this index again.": "This will allow the query planner to use this index again.", "This will also delete {0}.": "This will also delete {0}.", "This will prevent the query planner from using this index.": "This will prevent the query planner from using this index.", - "Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting.": "Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting.", "To connect to Azure resources, you need to sign in to Azure accounts.": "To connect to Azure resources, you need to sign in to Azure accounts.", "TODO: Share the steps needed to reliably reproduce the problem. Please include actual and expected results.": "TODO: Share the steps needed to reliably reproduce the problem. Please include actual and expected results.", - "Too many arguments. Expecting 0 or 1 argument(s) to {constructorCall}": "Too many arguments. Expecting 0 or 1 argument(s) to {constructorCall}", "Total documents to import: {0}": "Total documents to import: {0}", "Total time taken to execute the query on the server": "Total time taken to execute the query on the server", "Transforming Stage 2 response to UI format": "Transforming Stage 2 response to UI format", "Tree View": "Tree View", - "Try again": "Try again", - "Type \"it\" for more": "Type \"it\" for more", "Unable to connect to the local database instance. Make sure it is started correctly. See {link} for tips.": "Unable to connect to the local database instance. Make sure it is started correctly. See {link} for tips.", "Unable to connect to the local instance. Make sure it is started correctly. See {link} for tips.": "Unable to connect to the local instance. Make sure it is started correctly. See {link} for tips.", - "Unable to parse syntax near line {line}, col {column}: {message}": "Unable to parse syntax near line {line}, col {column}: {message}", "Unable to retrieve credentials for cluster \"{cluster}\".": "Unable to retrieve credentials for cluster \"{cluster}\".", "Unable to retrieve credentials for the selected cluster.": "Unable to retrieve credentials for the selected cluster.", "Understanding Your Query Execution Plan": "Understanding Your Query Execution Plan", @@ -1043,9 +1040,6 @@ "Unknown query generation type: {type}": "Unknown query generation type: {type}", "Unknown strategy": "Unknown strategy", "Unknown tenant": "Unknown tenant", - "Unrecognized node type encountered. Could not parse {constructorCall} as part of {functionCall}": "Unrecognized node type encountered. Could not parse {constructorCall} as part of {functionCall}", - "Unrecognized node type encountered. We could not parse {text}": "Unrecognized node type encountered. We could not parse {text}", - "Unrecognized token. Token text: {text}": "Unrecognized token. Token text: {text}", "Unsupported authentication mechanism. Only \"Username and Password\" (SCRAM-SHA-256) is supported.": "Unsupported authentication mechanism. Only \"Username and Password\" (SCRAM-SHA-256) is supported.", "Unsupported authentication mechanism. Only SCRAM-SHA-256 (username/password) is supported.": "Unsupported authentication mechanism. Only SCRAM-SHA-256 (username/password) is supported.", "Unsupported authentication method: {0}": "Unsupported authentication method: {0}", @@ -1062,7 +1056,6 @@ "Updated entity \"{name}\".": "Updated entity \"{name}\".", "Upload": "Upload", "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.": "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.", - "Use anyway": "Use anyway", "Use projection to return only necessary fields. This reduces network transfer and memory usage, especially important for documents with large embedded arrays or binary data.": "Use projection to return only necessary fields. This reduces network transfer and memory usage, especially important for documents with large embedded arrays or binary data.", "Username and Password": "Username and Password", "Username cannot be empty": "Username cannot be empty", @@ -1092,6 +1085,7 @@ "What's New": "What's New", "Where to save the exported documents?": "Where to save the exported documents?", "with Popover": "with Popover", + "Worker is not running": "Worker is not running", "Working...": "Working...", "Workingโ€ฆ": "Workingโ€ฆ", "Would you like to open the Collection View?": "Would you like to open the Collection View?", @@ -1108,11 +1102,9 @@ "You are already signed in to tenant \"{0}\"": "You are already signed in to tenant \"{0}\"", "You are not signed in to an Azure account. Please sign in.": "You are not signed in to an Azure account. Please sign in.", "You are not signed in to the MongoDB Cluster. Please sign in (by expanding the node \"{0}\") and try again.": "You are not signed in to the MongoDB Cluster. Please sign in (by expanding the node \"{0}\") and try again.", - "You can connect to a different DocumentDB by:": "You can connect to a different DocumentDB by:", "You clicked a link that wants to open a DocumentDB connection in VS Code.": "You clicked a link that wants to open a DocumentDB connection in VS Code.", "You do not have permission to create a resource group in subscription \"{0}\".": "You do not have permission to create a resource group in subscription \"{0}\".", "You might be asked for credentials to establish the connection.\nDo you want to continue?\n\nNote: You can disable these URL handling confirmations in the extension settings.": "You might be asked for credentials to establish the connection.\nDo you want to continue?\n\nNote: You can disable these URL handling confirmations in the extension settings.", - "You must open a *.vscode-documentdb-scrapbook file to run commands.": "You must open a *.vscode-documentdb-scrapbook file to run commands.", "You need to provide the password for \"{username}\" in order to continue. Your password will not be stored.": "You need to provide the password for \"{username}\" in order to continue. Your password will not be stored.", "You're attempting to copy a large number of documents. This process can be slow because it downloads all documents from the source to your computer and then uploads them to the destination, which can take a significant amount of time and bandwidth.\n\nFor larger data migrations, we recommend using a dedicated migration tool for a faster experience.\n\nNote: You can disable this warning or adjust the document count threshold in the extension settings.": "You're attempting to copy a large number of documents. This process can be slow because it downloads all documents from the source to your computer and then uploads them to the destination, which can take a significant amount of time and bandwidth.\n\nFor larger data migrations, we recommend using a dedicated migration tool for a faster experience.\n\nNote: You can disable this warning or adjust the document count threshold in the extension settings.", "Your Cluster": "Your Cluster", diff --git a/package-lock.json b/package-lock.json index 95094b7b5..5e7c6cd3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,16 @@ { "name": "vscode-documentdb", - "version": "0.7.2", + "version": "0.8.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb", - "version": "0.7.2", + "version": "0.8.0-beta", "license": "SEE LICENSE IN LICENSE.md", + "workspaces": [ + "packages/*" + ], "dependencies": { "@azure/arm-compute": "^22.4.0", "@azure/arm-cosmosdb": "~16.4.0", @@ -24,15 +27,24 @@ "@microsoft/vscode-azureresources-api": "~2.5.0", "@monaco-editor/react": "~4.7.0", "@mongodb-js/explain-plan-helper": "1.4.24", + "@mongodb-js/shell-bson-parser": "^1.5.6", + "@mongosh/errors": "^2.4.6", + "@mongosh/service-provider-core": "^5.0.3", + "@mongosh/service-provider-node-driver": "^5.0.6", + "@mongosh/shell-api": "^5.1.4", + "@mongosh/shell-evaluator": "^5.1.4", "@trpc/client": "~11.10.0", "@trpc/server": "~11.10.0", + "@vscode-documentdb/documentdb-constants": "*", + "@vscode-documentdb/schema-analyzer": "*", "@vscode/l10n": "~0.0.18", - "antlr4ts": "^0.5.0-alpha.4", - "bson": "~7.0.0", + "acorn": "^8.16.0", + "acorn-walk": "^8.3.5", + "bson": "^7.2.0", "denque": "~2.1.0", "es-toolkit": "~1.45.1", "monaco-editor": "~0.52.2", - "mongodb": "~7.0.0", + "mongodb": "^7.1.0", "mongodb-connection-string-url": "~3.0.2", "react-hotkeys-hook": "~5.2.1", "react-markdown": "^10.1.0", @@ -113,1550 +125,2569 @@ "vscode": "^1.105.0" } }, - "node_modules/@azu/format-text": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", - "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@azu/style-format": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", - "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", - "dev": true, - "license": "WTFPL", + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", "dependencies": { - "@azu/format-text": "^1.0.1" + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@azure-rest/ai-translation-text": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure-rest/ai-translation-text/-/ai-translation-text-1.0.1.tgz", - "integrity": "sha512-lUs1FfBXjik6EReUEYP1ogkhaSPHZdUV+EB215y7uejuyHgG1RXD2aLsqXQrluZwXcLMdN+bTzxylKBc5xDhgQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", "dependencies": { - "@azure-rest/core-client": "^2.3.1", - "@azure/core-auth": "^1.9.0", - "@azure/core-rest-pipeline": "^1.18.0", - "@azure/logger": "^1.1.4", - "tslib": "^2.8.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@azure-rest/core-client": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", - "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.10.0", - "@azure/core-rest-pipeline": "^1.22.0", - "@azure/core-tracing": "^1.3.0", - "@typespec/ts-http-runtime": "^0.3.0", + "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=14.0.0" } }, - "node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@azure/arm-authorization": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@azure/arm-authorization/-/arm-authorization-9.0.0.tgz", - "integrity": "sha512-GdiCA8IA1gO+qcCbFEPj+iLC4+3ByjfKzmeAnkP7MdlL84Yo30Huo/EwbZzwRjYybXYUBuFxGPBB+yeTT4Ebxg==", - "license": "MIT", + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", "dependencies": { - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.7.0", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.8.0", - "tslib": "^2.2.0" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@azure/arm-authorization-profile-2020-09-01-hybrid": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/arm-authorization-profile-2020-09-01-hybrid/-/arm-authorization-profile-2020-09-01-hybrid-2.1.0.tgz", - "integrity": "sha512-uOXhcj6Dv+TB8Yn2fguQQhoBZhafTf0ir5/QIZ8C7Rb2vS0nLcnoeesWRj9jmS0SerE7y2AF3qEhrowEZotX1Q==", - "license": "MIT", + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", "dependencies": { - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.6.1", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.8.0", - "tslib": "^2.2.0" + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@azure/arm-compute": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/@azure/arm-compute/-/arm-compute-22.4.0.tgz", - "integrity": "sha512-MYwIfwtvN7JvjbKQC9iakOHKMWvdcfZn8agiduNqNY8Rw/MWkGb39kpOLK/SeW36tfaNStuXoW/ijsxAzqQ40A==", - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.3", - "@azure/core-lro": "^2.5.4", - "@azure/core-paging": "^1.6.2", - "@azure/core-rest-pipeline": "^1.19.1", - "tslib": "^2.8.1" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@azure/arm-cosmosdb": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/@azure/arm-cosmosdb/-/arm-cosmosdb-16.4.0.tgz", - "integrity": "sha512-TBEaKFSKXFlmbrt7xeQrx8amfg09pRd3s5bYixSpXZg4zbnKufPMC3/NC2o0Dkd/G1/oyQe65kxgvzosoggS1g==", - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.3", - "@azure/core-lro": "^2.5.4", - "@azure/core-paging": "^1.6.2", - "@azure/core-rest-pipeline": "^1.19.1", - "tslib": "^2.8.1" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=14.0.0" } }, - "node_modules/@azure/arm-mongocluster": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/arm-mongocluster/-/arm-mongocluster-1.1.0.tgz", - "integrity": "sha512-O//J38/uFZr5C+s2W7oLSPowsIPEt2KfHSIb+KtJsEckegfm5z70CWPWJP764rA7bKPwPfugOJwnLQ9Y8SakbQ==", - "license": "MIT", + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.1015.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1015.0.tgz", + "integrity": "sha512-YTxPmgqF0zEVR2AVZVmvrqhHnPSeA1wZk5uNQaTPVPGZBrwxAoEBMEQvneOnRLjpAiP+T4xggsg6F5nBwfoTRQ==", + "license": "Apache-2.0", "dependencies": { - "@azure-rest/core-client": "^2.3.1", - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-lro": "^3.1.0", - "@azure/core-rest-pipeline": "^1.20.0", - "@azure/core-util": "^1.12.0", - "@azure/logger": "^1.2.0", - "tslib": "^2.8.1" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.25", + "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.11", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/arm-mongocluster/node_modules/@azure/core-lro": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-3.3.1.tgz", - "integrity": "sha512-bulm3klLqIAhzI3iQMYQ42i+V9EnevScsHdI9amFfjaw6OJqPBK1038cq5qachoKV3yt/iQQEDittHmZW2aSuA==", - "license": "MIT", + "node_modules/@aws-sdk/core": { + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.24.tgz", + "integrity": "sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-util": "^1.13.0", - "@azure/logger": "^1.3.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.15", + "@smithy/core": "^3.23.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/arm-msi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@azure/arm-msi/-/arm-msi-2.2.0.tgz", - "integrity": "sha512-Wqg9j9qR+k1IxZXwKtegZWj6k1d6UUGOz4uHPmhyJOeiQrtZHXBcaWSZhtqJo5sy888TD6jVIQV/4znhbKYL9g==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.17.tgz", + "integrity": "sha512-rMiW9GkLFBeRNvYdTzIRNP2Gq8vE8lomuqkv0BM7taX80UrN5oAa1wA8dDSWidga15k+0eFLo4RDsBHmeR1TUA==", + "license": "Apache-2.0", "dependencies": { - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.2", - "@azure/core-paging": "^1.6.2", - "@azure/core-rest-pipeline": "^1.19.0", - "tslib": "^2.8.1" + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/arm-network": { - "version": "33.5.0", - "resolved": "https://registry.npmjs.org/@azure/arm-network/-/arm-network-33.5.0.tgz", - "integrity": "sha512-wXD34p4/J2kZrkWylabtxLzrea5TK2m9NRwYtBLCvgQXN5NFTQohHgOFhoi/7DgV6w1qaM+qGz9mo5HRxpU70w==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.22.tgz", + "integrity": "sha512-cXp0VTDWT76p3hyK5D51yIKEfpf6/zsUvMfaB8CkyqadJxMQ8SbEeVroregmDlZbtG31wkj9ei0WnftmieggLg==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.6.0", - "@azure/core-client": "^1.7.0", - "@azure/core-lro": "^2.5.4", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.14.0", - "tslib": "^2.2.0" + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/arm-resources": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-7.0.0.tgz", - "integrity": "sha512-ezC1YLuPp1bh0GQFALcBvBxAB+9H5O0ynS40jp1t6hTlYe2t61cSplM3M4+4+nt9FCFZOjQSgAwj4KWYb8gruA==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.24.tgz", + "integrity": "sha512-h694K7+tRuepSRJr09wTvQfaEnjzsKZ5s7fbESrVds02GT/QzViJ94/HCNwM7bUfFxqpPXHxulZfL6Cou0dwPg==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.2", - "@azure/core-lro": "^2.5.4", - "@azure/core-paging": "^1.6.2", - "@azure/core-rest-pipeline": "^1.19.0", - "tslib": "^2.8.1" + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/arm-resources-profile-2020-09-01-hybrid": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/arm-resources-profile-2020-09-01-hybrid/-/arm-resources-profile-2020-09-01-hybrid-2.1.0.tgz", - "integrity": "sha512-7sNMfaf8agfQmExgtlvXzTooxoXPl2wJelSB2hQCG2BPZ37Oi24kxeECia+lXiZKfXAkiPSHxgdgECGjDQaV+Q==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.24.tgz", + "integrity": "sha512-O46fFmv0RDFWiWEA9/e6oW92BnsyAXuEgTTasxHligjn2RCr9L/DK773m/NoFaL3ZdNAUz8WxgxunleMnHAkeQ==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.6.1", - "@azure/core-lro": "^2.2.0", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.8.0", - "tslib": "^2.2.0" + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/credential-provider-env": "^3.972.22", + "@aws-sdk/credential-provider-http": "^3.972.24", + "@aws-sdk/credential-provider-login": "^3.972.24", + "@aws-sdk/credential-provider-process": "^3.972.22", + "@aws-sdk/credential-provider-sso": "^3.972.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/arm-resources-profile-2020-09-01-hybrid/node_modules/@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.24.tgz", + "integrity": "sha512-sIk8oa6AzDoUhxsR11svZESqvzGuXesw62Rl2oW6wguZx8i9cdGCvkFg+h5K7iucUZP8wyWibUbJMc+J66cu5g==", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.2.0" + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/arm-resources-subscriptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/arm-resources-subscriptions/-/arm-resources-subscriptions-2.1.0.tgz", - "integrity": "sha512-vKiu/3Yh84IV3IuJJ+0Fgs/ZQpvuGzoZ3dAoBksIV++Uu/Qz9RcQVz7pj+APWYIuODuR9I0eGKswZvzynzekug==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.25.tgz", + "integrity": "sha512-m7dR0Dsva2P+VUpL+VkC0WwiDby5pgmWXkRVDB5rlwv0jXJrQJf7YMtCoM8Wjk0H9jPeCYOxOXXcIgp/qp5Alg==", + "license": "Apache-2.0", "dependencies": { - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.7.0", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.8.0", - "tslib": "^2.2.0" + "@aws-sdk/credential-provider-env": "^3.972.22", + "@aws-sdk/credential-provider-http": "^3.972.24", + "@aws-sdk/credential-provider-ini": "^3.972.24", + "@aws-sdk/credential-provider-process": "^3.972.22", + "@aws-sdk/credential-provider-sso": "^3.972.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.24", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/arm-storage": { - "version": "18.6.0", - "resolved": "https://registry.npmjs.org/@azure/arm-storage/-/arm-storage-18.6.0.tgz", - "integrity": "sha512-dyN50fxts2xClCLIQY8qoDepYx2ql/eW5cVOy8XP+5zt9wIr1cgN2Mmv9/so2HDg6M/zOz8LhrvY+bS2blbhDQ==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.22.tgz", + "integrity": "sha512-Os32s8/4gTZjBk5BtoS/cuTILaj+K72d0dVG7TCJX/fC4598cxwLDmf1AEHEpER5oL3K//yETjvFaz0V8oO5Xw==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.3", - "@azure/core-lro": "^2.5.4", - "@azure/core-paging": "^1.6.2", - "@azure/core-rest-pipeline": "^1.19.1", - "tslib": "^2.8.1" + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/arm-storage-profile-2020-09-01-hybrid": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/arm-storage-profile-2020-09-01-hybrid/-/arm-storage-profile-2020-09-01-hybrid-2.1.0.tgz", - "integrity": "sha512-XZYoBWQP9BkQPde5DA7xIiOJVE+6Eeo755VfRqymN42gRn/X6GOcZ0X5x0qvLVxXZcwpFRKblRpkmxGi0FpIxg==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.24.tgz", + "integrity": "sha512-PaFv7snEfypU2yXkpvfyWgddEbDLtgVe51wdZlinhc2doubBjUzJZZpgwuF2Jenl1FBydMhNpMjD6SBUM3qdSA==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.6.1", - "@azure/core-lro": "^2.2.0", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.8.0", - "tslib": "^2.2.0" + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/token-providers": "3.1015.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/arm-storage-profile-2020-09-01-hybrid/node_modules/@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.24.tgz", + "integrity": "sha512-J6H4R1nvr3uBTqD/EeIPAskrBtET4WFfNhpFySr2xW7bVZOXpQfPjrLSIx65jcNjBmLXzWq8QFLdVoGxiGG/SA==", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.2.0" + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-auth": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", - "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", - "license": "MIT", + "node_modules/@aws-sdk/credential-providers": { + "version": "3.1015.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1015.0.tgz", + "integrity": "sha512-CUGBPLpIuTzIF0FhUWOZNaZiOvQEAevQ6wGreC2XADmJ9bg1xVEcPZo8XcQmmsOZqTp3oFsRiFdst/qHKlyEkA==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-util": "^1.13.0", + "@aws-sdk/client-cognito-identity": "3.1015.0", + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.17", + "@aws-sdk/credential-provider-env": "^3.972.22", + "@aws-sdk/credential-provider-http": "^3.972.24", + "@aws-sdk/credential-provider-ini": "^3.972.24", + "@aws-sdk/credential-provider-login": "^3.972.24", + "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/credential-provider-process": "^3.972.22", + "@aws-sdk/credential-provider-sso": "^3.972.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/core-client": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", - "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", - "license": "MIT", + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.10.0", - "@azure/core-rest-pipeline": "^1.22.0", - "@azure/core-tracing": "^1.3.0", - "@azure/core-util": "^1.13.0", - "@azure/logger": "^1.3.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/core-http-compat": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", - "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", - "license": "MIT", + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-client": "^1.10.0", - "@azure/core-rest-pipeline": "^1.22.0" + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/core-lro": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", - "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", - "license": "MIT", + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.2.0", - "@azure/logger": "^1.0.0", + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-paging": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", - "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", - "license": "MIT", + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.25.tgz", + "integrity": "sha512-QxiMPofvOt8SwSynTOmuZfvvPM1S9QfkESBxB22NMHTRXCJhR5BygLl8IXfC4jELiisQgwsgUby21GtXfX3f/g==", + "license": "Apache-2.0", "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-rest-pipeline": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", - "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", - "license": "MIT", + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.14.tgz", + "integrity": "sha512-fSESKvh1VbfjtV3QMnRkCPZWkUbQof6T/DOpiLp33yP2wA+rbwwnZeG3XT3Ekljgw2I8X4XaQPnw+zSR8yxJ5Q==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.10.0", - "@azure/core-tracing": "^1.3.0", - "@azure/core-util": "^1.13.0", - "@azure/logger": "^1.3.0", - "@typespec/ts-http-runtime": "^0.3.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.25", + "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.11", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/core-tracing": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", - "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", - "license": "MIT", + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.9.tgz", + "integrity": "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==", + "license": "Apache-2.0", "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.13", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/core-util": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", - "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", - "license": "MIT", + "node_modules/@aws-sdk/token-providers": { + "version": "3.1015.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1015.0.tgz", + "integrity": "sha512-3OSD4y110nisRhHzFOjoEeHU4GQL4KpzkX9PxzWaiZe0Yg2+thZKM0Pn9DjYwezH5JYfh/K++xK/SE0IHGrmCQ==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@typespec/ts-http-runtime": "^0.3.0", + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/cosmos": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.7.0.tgz", - "integrity": "sha512-a8OV7E41u/ZDaaaDAFdqTTiJ7c82jZc/+ot3XzNCIIilR25NBB+1ixzWQOAgP8SHRUIKfaUl6wAPdTuiG9I66A==", - "license": "MIT", + "node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-rest-pipeline": "^1.19.1", - "@azure/core-tracing": "^1.2.0", - "@azure/core-util": "^1.11.0", - "@azure/keyvault-keys": "^4.9.0", - "@azure/logger": "^1.1.4", - "fast-json-stable-stringify": "^2.1.0", - "priorityqueuejs": "^2.0.0", - "semaphore": "^1.1.0", - "tslib": "^2.8.1" + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/identity": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", - "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", - "license": "MIT", + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.2", - "@azure/core-rest-pipeline": "^1.17.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^4.2.0", - "@azure/msal-node": "^3.5.0", - "open": "^10.1.0", - "tslib": "^2.2.0" + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/keyvault-common": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", - "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", - "license": "MIT", + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.5.0", - "@azure/core-rest-pipeline": "^1.8.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.10.0", - "@azure/logger": "^1.1.4", - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/keyvault-keys": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", - "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", - "license": "MIT", + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "license": "Apache-2.0", "dependencies": { - "@azure-rest/core-client": "^2.3.3", - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-http-compat": "^2.2.0", - "@azure/core-lro": "^2.7.2", - "@azure/core-paging": "^1.6.2", - "@azure/core-rest-pipeline": "^1.19.0", - "@azure/core-tracing": "^1.2.0", - "@azure/core-util": "^1.11.0", - "@azure/keyvault-common": "^2.0.0", - "@azure/logger": "^1.1.4", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=18.0.0" + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" } }, - "node_modules/@azure/logger": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", - "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", - "license": "MIT", + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.11.tgz", + "integrity": "sha512-1qdXbXo2s5MMLpUvw00284LsbhtlQ4ul7Zzdn5n+7p4WVgCMLqhxImpHIrjSoc72E/fyc4Wq8dLtUld2Gsh+lA==", + "license": "Apache-2.0", "dependencies": { - "@typespec/ts-http-runtime": "^0.3.0", + "@aws-sdk/middleware-user-agent": "^3.972.25", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@azure/ms-rest-azure-env": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-azure-env/-/ms-rest-azure-env-2.0.0.tgz", - "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==", - "license": "MIT" - }, - "node_modules/@azure/msal-browser": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.26.2.tgz", - "integrity": "sha512-F2U1mEAFsYGC5xzo1KuWc/Sy3CRglU9Ql46cDUx8x/Y3KnAIr1QAq96cIKCk/ZfnVxlvprXWRjNKoEpgLJXLhg==", - "license": "MIT", + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz", + "integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==", + "license": "Apache-2.0", "dependencies": { - "@azure/msal-common": "15.13.2" + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.5.8", + "tslib": "^2.6.2" }, "engines": { - "node": ">=0.8.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/msal-common": { - "version": "15.13.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.2.tgz", - "integrity": "sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==", - "license": "MIT", + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", "engines": { - "node": ">=0.8.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/msal-node": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.3.tgz", - "integrity": "sha512-Ul7A4gwmaHzYWj2Z5xBDly/W8JSC1vnKgJ898zPMZr0oSf1ah0tiL15sytjycU/PMhDZAlkWtEL1+MzNMU6uww==", - "license": "MIT", + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", "dependencies": { - "@azure/msal-common": "15.13.2", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" - }, - "engines": { - "node": ">=16" + "@azu/format-text": "^1.0.1" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "node_modules/@azure-rest/ai-translation-text": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure-rest/ai-translation-text/-/ai-translation-text-1.0.1.tgz", + "integrity": "sha512-lUs1FfBXjik6EReUEYP1ogkhaSPHZdUV+EB215y7uejuyHgG1RXD2aLsqXQrluZwXcLMdN+bTzxylKBc5xDhgQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "@azure-rest/core-client": "^2.3.1", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.18.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=18.0.0" } }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, + "node_modules/@azure/arm-authorization": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@azure/arm-authorization/-/arm-authorization-9.0.0.tgz", + "integrity": "sha512-GdiCA8IA1gO+qcCbFEPj+iLC4+3ByjfKzmeAnkP7MdlL84Yo30Huo/EwbZzwRjYybXYUBuFxGPBB+yeTT4Ebxg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.7.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, + "node_modules/@azure/arm-authorization-profile-2020-09-01-hybrid": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-authorization-profile-2020-09-01-hybrid/-/arm-authorization-profile-2020-09-01-hybrid-2.1.0.tgz", + "integrity": "sha512-uOXhcj6Dv+TB8Yn2fguQQhoBZhafTf0ir5/QIZ8C7Rb2vS0nLcnoeesWRj9jmS0SerE7y2AF3qEhrowEZotX1Q==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.6.1", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", + "node_modules/@azure/arm-compute": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/@azure/arm-compute/-/arm-compute-22.4.0.tgz", + "integrity": "sha512-MYwIfwtvN7JvjbKQC9iakOHKMWvdcfZn8agiduNqNY8Rw/MWkGb39kpOLK/SeW36tfaNStuXoW/ijsxAzqQ40A==", + "license": "MIT", "dependencies": { - "yallist": "^3.0.2" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-lro": "^2.5.4", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@azure/arm-cosmosdb": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@azure/arm-cosmosdb/-/arm-cosmosdb-16.4.0.tgz", + "integrity": "sha512-TBEaKFSKXFlmbrt7xeQrx8amfg09pRd3s5bYixSpXZg4zbnKufPMC3/NC2o0Dkd/G1/oyQe65kxgvzosoggS1g==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-lro": "^2.5.4", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, + "node_modules/@azure/arm-mongocluster": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-mongocluster/-/arm-mongocluster-1.1.0.tgz", + "integrity": "sha512-O//J38/uFZr5C+s2W7oLSPowsIPEt2KfHSIb+KtJsEckegfm5z70CWPWJP764rA7bKPwPfugOJwnLQ9Y8SakbQ==", "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.1", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-lro": "^3.1.0", + "@azure/core-rest-pipeline": "^1.20.0", + "@azure/core-util": "^1.12.0", + "@azure/logger": "^1.2.0", + "tslib": "^2.8.1" + }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, + "node_modules/@azure/arm-mongocluster/node_modules/@azure/core-lro": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-3.3.1.tgz", + "integrity": "sha512-bulm3klLqIAhzI3iQMYQ42i+V9EnevScsHdI9amFfjaw6OJqPBK1038cq5qachoKV3yt/iQQEDittHmZW2aSuA==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, + "node_modules/@azure/arm-msi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/arm-msi/-/arm-msi-2.2.0.tgz", + "integrity": "sha512-Wqg9j9qR+k1IxZXwKtegZWj6k1d6UUGOz4uHPmhyJOeiQrtZHXBcaWSZhtqJo5sy888TD6jVIQV/4znhbKYL9g==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "tslib": "^2.8.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, + "node_modules/@azure/arm-network": { + "version": "33.5.0", + "resolved": "https://registry.npmjs.org/@azure/arm-network/-/arm-network-33.5.0.tgz", + "integrity": "sha512-wXD34p4/J2kZrkWylabtxLzrea5TK2m9NRwYtBLCvgQXN5NFTQohHgOFhoi/7DgV6w1qaM+qGz9mo5HRxpU70w==", "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.6.0", + "@azure/core-client": "^1.7.0", + "@azure/core-lro": "^2.5.4", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.14.0", + "tslib": "^2.2.0" + }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "node_modules/@azure/arm-resources": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-7.0.0.tgz", + "integrity": "sha512-ezC1YLuPp1bh0GQFALcBvBxAB+9H5O0ynS40jp1t6hTlYe2t61cSplM3M4+4+nt9FCFZOjQSgAwj4KWYb8gruA==", "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-lro": "^2.5.4", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "tslib": "^2.8.1" + }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "node_modules/@azure/arm-resources-profile-2020-09-01-hybrid": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-resources-profile-2020-09-01-hybrid/-/arm-resources-profile-2020-09-01-hybrid-2.1.0.tgz", + "integrity": "sha512-7sNMfaf8agfQmExgtlvXzTooxoXPl2wJelSB2hQCG2BPZ37Oi24kxeECia+lXiZKfXAkiPSHxgdgECGjDQaV+Q==", "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.6.1", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, + "node_modules/@azure/arm-resources-profile-2020-09-01-hybrid/node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "license": "MIT", + "dependencies": { + "tslib": "^2.2.0" + }, "engines": { - "node": ">=6.9.0" + "node": ">=12.0.0" } }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, + "node_modules/@azure/arm-resources-subscriptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-resources-subscriptions/-/arm-resources-subscriptions-2.1.0.tgz", + "integrity": "sha512-vKiu/3Yh84IV3IuJJ+0Fgs/ZQpvuGzoZ3dAoBksIV++Uu/Qz9RcQVz7pj+APWYIuODuR9I0eGKswZvzynzekug==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.7.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, + "node_modules/@azure/arm-storage": { + "version": "18.6.0", + "resolved": "https://registry.npmjs.org/@azure/arm-storage/-/arm-storage-18.6.0.tgz", + "integrity": "sha512-dyN50fxts2xClCLIQY8qoDepYx2ql/eW5cVOy8XP+5zt9wIr1cgN2Mmv9/so2HDg6M/zOz8LhrvY+bS2blbhDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-lro": "^2.5.4", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "tslib": "^2.8.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, + "node_modules/@azure/arm-storage-profile-2020-09-01-hybrid": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-storage-profile-2020-09-01-hybrid/-/arm-storage-profile-2020-09-01-hybrid-2.1.0.tgz", + "integrity": "sha512-XZYoBWQP9BkQPde5DA7xIiOJVE+6Eeo755VfRqymN42gRn/X6GOcZ0X5x0qvLVxXZcwpFRKblRpkmxGi0FpIxg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.6.1", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, + "node_modules/@azure/arm-storage-profile-2020-09-01-hybrid/node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "tslib": "^2.2.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=12.0.0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, + "node_modules/@azure/core-http-compat": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", - "dev": true, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, + "node_modules/@azure/cosmos": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.7.0.tgz", + "integrity": "sha512-a8OV7E41u/ZDaaaDAFdqTTiJ7c82jZc/+ot3XzNCIIilR25NBB+1ixzWQOAgP8SHRUIKfaUl6wAPdTuiG9I66A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-keys": "^4.9.0", + "@azure/logger": "^1.1.4", + "fast-json-stable-stringify": "^2.1.0", + "priorityqueuejs": "^2.0.0", + "semaphore": "^1.1.0", + "tslib": "^2.8.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, + "node_modules/@azure/ms-rest-azure-env": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-azure-env/-/ms-rest-azure-env-2.0.0.tgz", + "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==", + "license": "MIT" + }, + "node_modules/@azure/msal-browser": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.26.2.tgz", + "integrity": "sha512-F2U1mEAFsYGC5xzo1KuWc/Sy3CRglU9Ql46cDUx8x/Y3KnAIr1QAq96cIKCk/ZfnVxlvprXWRjNKoEpgLJXLhg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@azure/msal-common": "15.13.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=0.8.0" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", - "dev": true, + "node_modules/@azure/msal-common": { + "version": "15.13.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.2.tgz", + "integrity": "sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.3.tgz", + "integrity": "sha512-Ul7A4gwmaHzYWj2Z5xBDly/W8JSC1vnKgJ898zPMZr0oSf1ah0tiL15sytjycU/PMhDZAlkWtEL1+MzNMU6uww==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@azure/msal-common": "15.13.2", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=16" } }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { + "node_modules/@babel/core": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", - "debug": "^4.3.1" + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/types": { + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@borewit/text-codec": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", - "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", - "dev": true, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@ctrl/tinycolor": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", - "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "dev": true, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@epic-web/invariant": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "@babel/types": "^7.28.5" }, - "funding": { - "url": "https://opencollective.com/eslint" + "bin": { + "parser": "bin/babel-parser.js" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": "*" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/core": "^0.17.0" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@babel/helper-plugin-utils": "^7.28.6" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, - "license": "Python-2.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "@babel/helper-plugin-utils": "^7.8.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": "*" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://eslint.org/donate" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@excel-builder-vanilla/types": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@excel-builder-vanilla/types/-/types-4.2.1.tgz", - "integrity": "sha512-AtVzHKfH7TtRTH7Yczwu6SMXYhmvO+W6H2L4ktg7JesK4cHSS7FinzFk+zkPWs2ROZEXYHLZxJTGY7OrhiOTjw==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "license": "MIT", "dependencies": { - "fflate": "^0.8.2" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, - "funding": { - "type": "ko_fi", - "url": "https://ko-fi.com/ghiscoding" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.11" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/devtools": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.2.3.tgz", - "integrity": "sha512-ZTcxTvgo9CRlP7vJV62yCxdqmahHTGpSTi5QaTDgGoyQq0OyjaVZhUhXv/qdkQFOI3Sxlfmz0XGG4HaZMsDf8Q==", + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, "peerDependencies": { - "@floating-ui/dom": "^1.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", + "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", + "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@excel-builder-vanilla/types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@excel-builder-vanilla/types/-/types-4.2.1.tgz", + "integrity": "sha512-AtVzHKfH7TtRTH7Yczwu6SMXYhmvO+W6H2L4ktg7JesK4cHSS7FinzFk+zkPWs2ROZEXYHLZxJTGY7OrhiOTjw==", + "license": "MIT", + "dependencies": { + "fflate": "^0.8.2" + }, + "funding": { + "type": "ko_fi", + "url": "https://ko-fi.com/ghiscoding" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/devtools": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.2.3.tgz", + "integrity": "sha512-ZTcxTvgo9CRlP7vJV62yCxdqmahHTGpSTi5QaTDgGoyQq0OyjaVZhUhXv/qdkQFOI3Sxlfmz0XGG4HaZMsDf8Q==", + "license": "MIT", + "peerDependencies": { + "@floating-ui/dom": "^1.0.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@fluentui/keyboard-keys": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.8.tgz", + "integrity": "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/priority-overflow": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.3.0.tgz", + "integrity": "sha512-yaBC0R4e+4ZlCWDulB5S+xBrlnLwfzdg68GaarCqQO8OHjLg7Ah05xTj7PsAYcoHeEg/9vYeBwGXBpRO8+Tjqw==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/react-accordion": { + "version": "9.9.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.9.2.tgz", + "integrity": "sha512-Mmi5nVKfQrBiBiD1JPVtCmIMrR1CpCy8hsWZLwv/pHt+uHHyW9HyrPXwiOitj3ookA5ec1kXyl34BN8RUi7DGQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.13.0", + "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-alert": { + "version": "9.0.0-beta.135", + "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.135.tgz", + "integrity": "sha512-Qkr89e6tl4q0fhzfx9Wzb3ltiqbFtZj7AhT+CHZdW0I6KtpfGmJnvzaqvz0KXMdrKROTgvkA1Ny3Epf9ortc0Q==", + "license": "MIT", + "dependencies": { + "@fluentui/react-avatar": "^9.10.2", + "@fluentui/react-button": "^9.8.2", + "@fluentui/react-icons": "^2.0.239", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-aria": { + "version": "9.17.10", + "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.17.10.tgz", + "integrity": "sha512-KqS2XcdN84XsgVG4fAESyOBfixN7zbObWfQVLNZ2gZrp2b1hPGVYfQ6J4WOO0vXMKYp0rre/QMOgDm6/srL0XQ==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-utilities": "^9.26.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-avatar": { + "version": "9.10.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.10.2.tgz", + "integrity": "sha512-0qy3U1S80c2Z0A8O/3Ko8XmG4d/NCof1XZ1jclbneKLDT0PeoX3BUlDDgCalOEwb0s1x6TjLabam5FtY4E30cg==", + "license": "MIT", + "dependencies": { + "@fluentui/react-badge": "^9.4.15", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-popover": "^9.14.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-tooltip": "^9.9.3", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-badge": { + "version": "9.4.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.4.15.tgz", + "integrity": "sha512-KgFUJHBHP76vE3EDuPg/ml7lGqxs9zJ634e+vtxn8D7ghCZ6h9P6A0WbmgsPcN6MZoBZYLzzYT3OJ6Vmu3BM8g==", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } }, - "node_modules/@fluentui/keyboard-keys": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.8.tgz", - "integrity": "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw==", + "node_modules/@fluentui/react-breadcrumb": { + "version": "9.3.17", + "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.3.17.tgz", + "integrity": "sha512-POnwCFyvXabq7lNtJRslASNkrm0iRoXpnrWwh0LyBTFZRDiGDKaV18Bpk0UiuQNTUurVQiH513164XKHIP+d7Q==", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-button": "^9.8.2", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-link": "^9.7.4", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-button": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.8.2.tgz", + "integrity": "sha512-T2xBn6s6DRNH17Y+kLO+uEOaRe89Q20WP1Rs6OzC45cSpOGc+q9ogbPbYBqU7Tr1fur+Xd8LRHxdQJ3j5ufbdw==", "license": "MIT", "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/priority-overflow": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.3.0.tgz", - "integrity": "sha512-yaBC0R4e+4ZlCWDulB5S+xBrlnLwfzdg68GaarCqQO8OHjLg7Ah05xTj7PsAYcoHeEg/9vYeBwGXBpRO8+Tjqw==", + "node_modules/@fluentui/react-card": { + "version": "9.5.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.5.11.tgz", + "integrity": "sha512-0W3BmDER/aKx+7+ttGy+M6LO09DW7DkJlO8F0x13L1ssOVxJ0OhyhSGiCF0cJliOK1tiGPveYf6+X2xMq2MT6g==", "license": "MIT", "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-text": "^9.6.15", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-accordion": { - "version": "9.9.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.9.2.tgz", - "integrity": "sha512-Mmi5nVKfQrBiBiD1JPVtCmIMrR1CpCy8hsWZLwv/pHt+uHHyW9HyrPXwiOitj3ookA5ec1kXyl34BN8RUi7DGQ==", + "node_modules/@fluentui/react-carousel": { + "version": "9.9.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.9.4.tgz", + "integrity": "sha512-mzGZUOe3tB+86/WPsQTgppYRoqeM1vl8LswISl7FVrxk7PREnzZLW4BEZnFOKuP29dThcjJNzF0mM/5kq1lKug==", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-button": "^9.8.2", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-tooltip": "^9.9.3", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1", + "embla-carousel": "^8.5.1", + "embla-carousel-autoplay": "^8.5.1", + "embla-carousel-fade": "^8.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-checkbox": { + "version": "9.5.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.5.16.tgz", + "integrity": "sha512-jjbj5RTy78OzFT95zj6SI7RMV1JF7FLT1CiYIL13bFTsL9tiPyAqXRcdXGJOnt/EuyD3uKs2nyOu4M3QFVy0ng==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.3.15", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-color-picker": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-color-picker/-/react-color-picker-9.2.15.tgz", + "integrity": "sha512-RMmawl7g4gUYLuTQG2QwCcR9fGC+vDD+snsBlXtObpj/cKpeDmYif46g88pYv86jeIXY1zsjINmLpELmz+uFmw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.3.4", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-combobox": { + "version": "9.16.18", + "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.16.18.tgz", + "integrity": "sha512-nmyleswOSS9O/3gn8AWQ9Uuyis0WTHO1zZnDVapFUdgd2+hAcUSjJXPQv6NGftuUB5bgS2qAx9prRJg17ZrZvA==", "license": "MIT", "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.10", "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-field": "^9.4.16", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-components": { + "version": "9.73.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.73.3.tgz", + "integrity": "sha512-8JqxJuQmcBungWH8KxgBjiNe4sP5UXiiVWTqQGJ8l23gua3SC8uHufoOEKneEDWULR4HHJThscbqDLsGpkcJaw==", + "license": "MIT", + "dependencies": { + "@fluentui/react-accordion": "^9.9.2", + "@fluentui/react-alert": "9.0.0-beta.135", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-avatar": "^9.10.2", + "@fluentui/react-badge": "^9.4.15", + "@fluentui/react-breadcrumb": "^9.3.17", + "@fluentui/react-button": "^9.8.2", + "@fluentui/react-card": "^9.5.11", + "@fluentui/react-carousel": "^9.9.4", + "@fluentui/react-checkbox": "^9.5.16", + "@fluentui/react-color-picker": "^9.2.15", + "@fluentui/react-combobox": "^9.16.18", + "@fluentui/react-dialog": "^9.17.2", + "@fluentui/react-divider": "^9.6.2", + "@fluentui/react-drawer": "^9.11.5", + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-image": "^9.3.15", + "@fluentui/react-infobutton": "9.0.0-beta.112", + "@fluentui/react-infolabel": "^9.4.17", + "@fluentui/react-input": "^9.7.16", + "@fluentui/react-label": "^9.3.15", + "@fluentui/react-link": "^9.7.4", + "@fluentui/react-list": "^9.6.11", + "@fluentui/react-menu": "^9.22.0", + "@fluentui/react-message-bar": "^9.6.21", "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-nav": "^9.3.20", + "@fluentui/react-overflow": "^9.7.1", + "@fluentui/react-persona": "^9.6.2", + "@fluentui/react-popover": "^9.14.0", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-progress": "^9.4.16", + "@fluentui/react-provider": "^9.22.15", + "@fluentui/react-radio": "^9.5.16", + "@fluentui/react-rating": "^9.3.15", + "@fluentui/react-search": "^9.3.16", + "@fluentui/react-select": "^9.4.16", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-skeleton": "^9.5.0", + "@fluentui/react-slider": "^9.5.16", + "@fluentui/react-spinbutton": "^9.5.16", + "@fluentui/react-spinner": "^9.7.15", + "@fluentui/react-swatch-picker": "^9.5.0", + "@fluentui/react-switch": "^9.6.1", + "@fluentui/react-table": "^9.19.11", + "@fluentui/react-tabs": "^9.11.2", "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-tag-picker": "^9.8.2", + "@fluentui/react-tags": "^9.7.17", + "@fluentui/react-teaching-popover": "^9.6.18", + "@fluentui/react-text": "^9.6.15", + "@fluentui/react-textarea": "^9.6.16", "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-toast": "^9.7.15", + "@fluentui/react-toolbar": "^9.7.4", + "@fluentui/react-tooltip": "^9.9.3", + "@fluentui/react-tree": "^9.15.13", "@fluentui/react-utilities": "^9.26.2", + "@fluentui/react-virtualizer": "9.0.0-alpha.111", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, @@ -1667,40 +2698,42 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-alert": { - "version": "9.0.0-beta.135", - "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.135.tgz", - "integrity": "sha512-Qkr89e6tl4q0fhzfx9Wzb3ltiqbFtZj7AhT+CHZdW0I6KtpfGmJnvzaqvz0KXMdrKROTgvkA1Ny3Epf9ortc0Q==", + "node_modules/@fluentui/react-context-selector": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.2.15.tgz", + "integrity": "sha512-QymBntFLJNZ9VfTOaBn2ApUSSSC5UuDW8ZcgPJPA+06XEFH+U9Zny2d9QAg1xYNYwIGWahWGQ+7ATOuLxtB8Jw==", "license": "MIT", "dependencies": { - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-icons": "^2.0.239", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "react-dom": ">=16.14.0 <20.0.0", + "scheduler": ">=0.19.0" } }, - "node_modules/@fluentui/react-aria": { - "version": "9.17.10", - "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.17.10.tgz", - "integrity": "sha512-KqS2XcdN84XsgVG4fAESyOBfixN7zbObWfQVLNZ2gZrp2b1hPGVYfQ6J4WOO0vXMKYp0rre/QMOgDm6/srL0XQ==", + "node_modules/@fluentui/react-dialog": { + "version": "9.17.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.17.2.tgz", + "integrity": "sha512-mZdKylSvh2fRf0e3wMX3ZNccb9DahsOE7A5Y9LG97ghYvndMBVG2YwScIzUFVvLS206ari6HMOl0lC5JRB1bKA==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.13.0", + "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-portal": "^9.8.11", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1710,21 +2743,15 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-avatar": { - "version": "9.10.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.10.2.tgz", - "integrity": "sha512-0qy3U1S80c2Z0A8O/3Ko8XmG4d/NCof1XZ1jclbneKLDT0PeoX3BUlDDgCalOEwb0s1x6TjLabam5FtY4E30cg==", + "node_modules/@fluentui/react-divider": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.6.2.tgz", + "integrity": "sha512-jfHlpSoJys78STe/SSjqdcn+W7QjEO1xCGiedWp/MdTBi3pH5vEeYbt2u8RU+zP32IF0Clta85KsUEEG0DYELQ==", "license": "MIT", "dependencies": { - "@fluentui/react-badge": "^9.4.15", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-popover": "^9.14.0", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-tooltip": "^9.9.3", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" @@ -1736,15 +2763,19 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-badge": { - "version": "9.4.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.4.15.tgz", - "integrity": "sha512-KgFUJHBHP76vE3EDuPg/ml7lGqxs9zJ634e+vtxn8D7ghCZ6h9P6A0WbmgsPcN6MZoBZYLzzYT3OJ6Vmu3BM8g==", + "node_modules/@fluentui/react-drawer": { + "version": "9.11.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.11.5.tgz", + "integrity": "sha512-eoZY+jKZwbJo1PUsb7Ico7u/8aObHL4BhPP6hd+HHNzB7seTpN7rLd0DpASLZsxJUy5yvch4QF2TrjOu6V8kRA==", "license": "MIT", "dependencies": { - "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-dialog": "^9.17.2", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.13.0", + "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-portal": "^9.8.11", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -1757,19 +2788,17 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-breadcrumb": { - "version": "9.3.17", - "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.3.17.tgz", - "integrity": "sha512-POnwCFyvXabq7lNtJRslASNkrm0iRoXpnrWwh0LyBTFZRDiGDKaV18Bpk0UiuQNTUurVQiH513164XKHIP+d7Q==", + "node_modules/@fluentui/react-field": { + "version": "9.4.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.4.16.tgz", + "integrity": "sha512-2mfuYGldeqr9Llt8QSfwdj1hQofScvNQ/1Rns9TE4QUP6cdqs3cPX2+FZNJzpgO9vq5bk0hJpKqo7lvXZdyEzw==", "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-link": "^9.7.4", + "@fluentui/react-label": "^9.3.15", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -1782,41 +2811,27 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-button": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.8.2.tgz", - "integrity": "sha512-T2xBn6s6DRNH17Y+kLO+uEOaRe89Q20WP1Rs6OzC45cSpOGc+q9ogbPbYBqU7Tr1fur+Xd8LRHxdQJ3j5ufbdw==", + "node_modules/@fluentui/react-icons": { + "version": "2.0.320", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.320.tgz", + "integrity": "sha512-NU4gErPeaTD/T6Z9g3Uvp898lIFS6fDLr3++vpT8pcI4Ds0fZqQdrwNi3dF0R/SVws8DXQaRYiGlPHxszo4J4g==", "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" }, "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "react": ">=16.8.0 <20.0.0" } }, - "node_modules/@fluentui/react-card": { - "version": "9.5.11", - "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.5.11.tgz", - "integrity": "sha512-0W3BmDER/aKx+7+ttGy+M6LO09DW7DkJlO8F0x13L1ssOVxJ0OhyhSGiCF0cJliOK1tiGPveYf6+X2xMq2MT6g==", + "node_modules/@fluentui/react-image": { + "version": "9.3.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.3.15.tgz", + "integrity": "sha512-k8ftGUc5G3Hj5W9nOFnWEKZ1oXmoZE3EvAEdyI6Cn9R8E6zW2PZ1+cug0p6rr01JCDG8kbry1LAITcObMrlPdw==", "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-text": "^9.6.15", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -1829,27 +2844,21 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-carousel": { - "version": "9.9.4", - "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.9.4.tgz", - "integrity": "sha512-mzGZUOe3tB+86/WPsQTgppYRoqeM1vl8LswISl7FVrxk7PREnzZLW4BEZnFOKuP29dThcjJNzF0mM/5kq1lKug==", + "node_modules/@fluentui/react-infobutton": { + "version": "9.0.0-beta.112", + "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.112.tgz", + "integrity": "sha512-Fhqoc6b1MQtHW+Mm5sBhfa5ZrRdOV4azuUa5WyBvwD4Ozq/z2pBOC/wi/A/WCjKMnGoMlQ2CggoLaMhQmenzAQ==", "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-icons": "^2.0.237", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-label": "^9.3.15", + "@fluentui/react-popover": "^9.14.0", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-tooltip": "^9.9.3", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1", - "embla-carousel": "^8.5.1", - "embla-carousel-autoplay": "^8.5.1", - "embla-carousel-fade": "^8.5.1" + "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", @@ -1858,16 +2867,16 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-checkbox": { - "version": "9.5.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.5.16.tgz", - "integrity": "sha512-jjbj5RTy78OzFT95zj6SI7RMV1JF7FLT1CiYIL13bFTsL9tiPyAqXRcdXGJOnt/EuyD3uKs2nyOu4M3QFVy0ng==", + "node_modules/@fluentui/react-infolabel": { + "version": "9.4.17", + "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.17.tgz", + "integrity": "sha512-zLw52jn2wAuEKWFzaNj3aKhuB4BAEI8LqblryCg0LKPKHcv/z9d9RllCqcVz+ngdK1tQGtCIPH/wxNlZXx/I3Q==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.16", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-label": "^9.3.15", + "@fluentui/react-popover": "^9.14.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -1876,23 +2885,21 @@ "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "react-dom": ">=16.8.0 <20.0.0" } }, - "node_modules/@fluentui/react-color-picker": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-color-picker/-/react-color-picker-9.2.15.tgz", - "integrity": "sha512-RMmawl7g4gUYLuTQG2QwCcR9fGC+vDD+snsBlXtObpj/cKpeDmYif46g88pYv86jeIXY1zsjINmLpELmz+uFmw==", + "node_modules/@fluentui/react-input": { + "version": "9.7.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.7.16.tgz", + "integrity": "sha512-dr6tBWbyDiP2KR7LDvJlxFwxucWfeFETumFo3fAtUSpjbTHMG0ZShh3cq0/c7Gqvq/ypl12jVB1Tj6E4RimV8g==", "license": "MIT", "dependencies": { - "@ctrl/tinycolor": "^3.3.4", - "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-field": "^9.4.16", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -1905,100 +2912,30 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-combobox": { - "version": "9.16.18", - "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.16.18.tgz", - "integrity": "sha512-nmyleswOSS9O/3gn8AWQ9Uuyis0WTHO1zZnDVapFUdgd2+hAcUSjJXPQv6NGftuUB5bgS2qAx9prRJg17ZrZvA==", + "node_modules/@fluentui/react-jsx-runtime": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.4.1.tgz", + "integrity": "sha512-ZodSm7jRa4kaLKDi+emfHFMP/IDnYwFQQAI2BdtKbVrvfwvzPRprGcnTgivnqKBT1ROvKOCY2ddz7+yZzesnNw==", "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-portal": "^9.8.11", - "@fluentui/react-positioning": "^9.22.0", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "react": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-components": { - "version": "9.73.3", - "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.73.3.tgz", - "integrity": "sha512-8JqxJuQmcBungWH8KxgBjiNe4sP5UXiiVWTqQGJ8l23gua3SC8uHufoOEKneEDWULR4HHJThscbqDLsGpkcJaw==", + "node_modules/@fluentui/react-label": { + "version": "9.3.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.3.15.tgz", + "integrity": "sha512-ycmaQwC4tavA8WeDfgcay1Ywu/4goHq1NOeVxkyzWTPGA7rs+tdCgdZBQZLAsBK2XFaZiHs7l+KG9r1oIRKolA==", "license": "MIT", "dependencies": { - "@fluentui/react-accordion": "^9.9.2", - "@fluentui/react-alert": "9.0.0-beta.135", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-badge": "^9.4.15", - "@fluentui/react-breadcrumb": "^9.3.17", - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-card": "^9.5.11", - "@fluentui/react-carousel": "^9.9.4", - "@fluentui/react-checkbox": "^9.5.16", - "@fluentui/react-color-picker": "^9.2.15", - "@fluentui/react-combobox": "^9.16.18", - "@fluentui/react-dialog": "^9.17.2", - "@fluentui/react-divider": "^9.6.2", - "@fluentui/react-drawer": "^9.11.5", - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-image": "^9.3.15", - "@fluentui/react-infobutton": "9.0.0-beta.112", - "@fluentui/react-infolabel": "^9.4.17", - "@fluentui/react-input": "^9.7.16", - "@fluentui/react-label": "^9.3.15", - "@fluentui/react-link": "^9.7.4", - "@fluentui/react-list": "^9.6.11", - "@fluentui/react-menu": "^9.22.0", - "@fluentui/react-message-bar": "^9.6.21", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-nav": "^9.3.20", - "@fluentui/react-overflow": "^9.7.1", - "@fluentui/react-persona": "^9.6.2", - "@fluentui/react-popover": "^9.14.0", - "@fluentui/react-portal": "^9.8.11", - "@fluentui/react-positioning": "^9.22.0", - "@fluentui/react-progress": "^9.4.16", - "@fluentui/react-provider": "^9.22.15", - "@fluentui/react-radio": "^9.5.16", - "@fluentui/react-rating": "^9.3.15", - "@fluentui/react-search": "^9.3.16", - "@fluentui/react-select": "^9.4.16", + "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-skeleton": "^9.5.0", - "@fluentui/react-slider": "^9.5.16", - "@fluentui/react-spinbutton": "^9.5.16", - "@fluentui/react-spinner": "^9.7.15", - "@fluentui/react-swatch-picker": "^9.5.0", - "@fluentui/react-switch": "^9.6.1", - "@fluentui/react-table": "^9.19.11", - "@fluentui/react-tabs": "^9.11.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-tag-picker": "^9.8.2", - "@fluentui/react-tags": "^9.7.17", - "@fluentui/react-teaching-popover": "^9.6.18", - "@fluentui/react-text": "^9.6.15", - "@fluentui/react-textarea": "^9.6.16", "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-toast": "^9.7.15", - "@fluentui/react-toolbar": "^9.7.4", - "@fluentui/react-tooltip": "^9.9.3", - "@fluentui/react-tree": "^9.15.13", "@fluentui/react-utilities": "^9.26.2", - "@fluentui/react-virtualizer": "9.0.0-alpha.111", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, @@ -2009,37 +2946,38 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-context-selector": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.2.15.tgz", - "integrity": "sha512-QymBntFLJNZ9VfTOaBn2ApUSSSC5UuDW8ZcgPJPA+06XEFH+U9Zny2d9QAg1xYNYwIGWahWGQ+7ATOuLxtB8Jw==", + "node_modules/@fluentui/react-link": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.7.4.tgz", + "integrity": "sha512-ILKFpo/QH1SRsLN9gopAyZT/b/xsGcdO4JxthEeuTRvpLD6gImvRplum8ySIlbTskVVzog6038bHUSYLMdN7OA==", "license": "MIT", "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0", - "scheduler": ">=0.19.0" + "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-dialog": { - "version": "9.17.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.17.2.tgz", - "integrity": "sha512-mZdKylSvh2fRf0e3wMX3ZNccb9DahsOE7A5Y9LG97ghYvndMBVG2YwScIzUFVvLS206ari6HMOl0lC5JRB1bKA==", + "node_modules/@fluentui/react-list": { + "version": "9.6.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.11.tgz", + "integrity": "sha512-ao1WdgWDrz4mTvic3dOD3Jk1V9XcppxX3Y3DI7Emsw2QI9Y2AsZBtiUrqYNEQ0ym3yFobURYJ3ZIhrW11VCKAw==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-checkbox": "^9.5.16", "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", - "@fluentui/react-portal": "^9.8.11", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -2048,20 +2986,29 @@ "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "react-dom": ">=16.8.0 <20.0.0" } }, - "node_modules/@fluentui/react-divider": { - "version": "9.6.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.6.2.tgz", - "integrity": "sha512-jfHlpSoJys78STe/SSjqdcn+W7QjEO1xCGiedWp/MdTBi3pH5vEeYbt2u8RU+zP32IF0Clta85KsUEEG0DYELQ==", + "node_modules/@fluentui/react-menu": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.22.0.tgz", + "integrity": "sha512-RPZvqHsxMDEArsz80mJabs1fVGPlCrhMntzM/wt3Bga+fyPv4yEuDdN5FB8JqUpIAjRZneiW0RLC0Mr3WqmatA==", "license": "MIT", "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.13.0", + "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -2074,24 +3021,58 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-drawer": { - "version": "9.11.5", - "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.11.5.tgz", - "integrity": "sha512-eoZY+jKZwbJo1PUsb7Ico7u/8aObHL4BhPP6hd+HHNzB7seTpN7rLd0DpASLZsxJUy5yvch4QF2TrjOu6V8kRA==", + "node_modules/@fluentui/react-message-bar": { + "version": "9.6.21", + "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.6.21.tgz", + "integrity": "sha512-Vba3+7+TuzH2Ma6YB/Sd5dy+dm4DWwacZc0a78CetVqCzYZ4u/5opdmiBs8JY1Qr8uYW38siHLbY8kLnu6OOjA==", "license": "MIT", "dependencies": { - "@fluentui/react-dialog": "^9.17.2", + "@fluentui/react-button": "^9.8.2", + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-link": "^9.7.4", "@fluentui/react-motion": "^9.13.0", "@fluentui/react-motion-components-preview": "^0.15.2", - "@fluentui/react-portal": "^9.8.11", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-motion": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.13.0.tgz", + "integrity": "sha512-YdOpW6e7qfvzoWKcqh8hReCqwYEoiEmNBcCprGaupKjWOi9jBbF/JESM1AHI9nOjPd8aY90WUG2+ahvrqfL9LA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-utilities": "^9.26.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-motion-components-preview": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.15.2.tgz", + "integrity": "sha512-KqHRV8lLmVwOWiHBdpUFA+TwMbuYu9cyzNvmhbMFLVKzZyr3MPgN+97Tf+6QYPf22o99SMT0BPySDv/HiNYanA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-motion": "*", + "@fluentui/react-utilities": "*", + "@swc/helpers": "^0.5.1" + }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", @@ -2099,18 +3080,25 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-field": { - "version": "9.4.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.4.16.tgz", - "integrity": "sha512-2mfuYGldeqr9Llt8QSfwdj1hQofScvNQ/1Rns9TE4QUP6cdqs3cPX2+FZNJzpgO9vq5bk0hJpKqo7lvXZdyEzw==", + "node_modules/@fluentui/react-nav": { + "version": "9.3.20", + "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.20.tgz", + "integrity": "sha512-YIObOcR92Nz4OUePrDhRdLQ5m9ph0y+U7U9NYgE/XFrLtWl+uqUS7u36m3NJl9QGgZVpUHO4nbNjizGLkncCCA==", "license": "MIT", "dependencies": { + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-button": "^9.8.2", "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-divider": "^9.6.2", + "@fluentui/react-drawer": "^9.11.5", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", + "@fluentui/react-motion": "^9.13.0", + "@fluentui/react-motion-components-preview": "^0.15.2", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-tooltip": "^9.9.3", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" @@ -2122,25 +3110,34 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-icons": { - "version": "2.0.320", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.320.tgz", - "integrity": "sha512-NU4gErPeaTD/T6Z9g3Uvp898lIFS6fDLr3++vpT8pcI4Ds0fZqQdrwNi3dF0R/SVws8DXQaRYiGlPHxszo4J4g==", + "node_modules/@fluentui/react-overflow": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.7.1.tgz", + "integrity": "sha512-Ml1GlcLrAUv31d9WN15WGOZv32gzDtZD5Mp1MOQ3ichDfTtxrswIch7MDzZ8hLMGf/7Y2IzBpV8iFR1XdSrGBA==", "license": "MIT", "dependencies": { - "@griffel/react": "^1.0.0", - "tslib": "^2.1.0" + "@fluentui/priority-overflow": "^9.3.0", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "react": ">=16.8.0 <20.0.0" + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-image": { - "version": "9.3.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.3.15.tgz", - "integrity": "sha512-k8ftGUc5G3Hj5W9nOFnWEKZ1oXmoZE3EvAEdyI6Cn9R8E6zW2PZ1+cug0p6rr01JCDG8kbry1LAITcObMrlPdw==", + "node_modules/@fluentui/react-persona": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.6.2.tgz", + "integrity": "sha512-60kOmljlYjUiySWDN1bZh1FB4C7jbJS2dobtBJQh5agnKg34p3egO+6MwsBHRcwaGhVMh4T8XcbE6t2hw+iqyQ==", "license": "MIT", "dependencies": { + "@fluentui/react-avatar": "^9.10.2", + "@fluentui/react-badge": "^9.4.15", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", @@ -2155,16 +3152,21 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-infobutton": { - "version": "9.0.0-beta.112", - "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.112.tgz", - "integrity": "sha512-Fhqoc6b1MQtHW+Mm5sBhfa5ZrRdOV4azuUa5WyBvwD4Ozq/z2pBOC/wi/A/WCjKMnGoMlQ2CggoLaMhQmenzAQ==", + "node_modules/@fluentui/react-popover": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.14.0.tgz", + "integrity": "sha512-XrZlSfSYhA12j5bna4Sq8N/If2vul7gl8woVrN8U3iQUjdaHB6OAMZ/WMNUdMm35Z+4e4rHClAZxU2dUsbHrmw==", "license": "MIT", "dependencies": { - "@fluentui/react-icons": "^2.0.237", + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", - "@fluentui/react-popover": "^9.14.0", + "@fluentui/react-motion": "^9.13.0", + "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", @@ -2178,43 +3180,39 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-infolabel": { - "version": "9.4.17", - "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.17.tgz", - "integrity": "sha512-zLw52jn2wAuEKWFzaNj3aKhuB4BAEI8LqblryCg0LKPKHcv/z9d9RllCqcVz+ngdK1tQGtCIPH/wxNlZXx/I3Q==", + "node_modules/@fluentui/react-portal": { + "version": "9.8.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.8.11.tgz", + "integrity": "sha512-2eg4MdW7e2UGRYWPg05GCytAjWYNd55YOP9+iUDINoQwwto9oeFTtZRyn08HYw37cSNqoH24qGz/VBctzTkqDA==", "license": "MIT", "dependencies": { - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", - "@fluentui/react-popover": "^9.14.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@types/react": ">=16.8.0 <20.0.0", - "@types/react-dom": ">=16.8.0 <20.0.0", + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.8.0 <20.0.0" + "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-input": { - "version": "9.7.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.7.16.tgz", - "integrity": "sha512-dr6tBWbyDiP2KR7LDvJlxFwxucWfeFETumFo3fAtUSpjbTHMG0ZShh3cq0/c7Gqvq/ypl12jVB1Tj6E4RimV8g==", + "node_modules/@fluentui/react-positioning": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.22.0.tgz", + "integrity": "sha512-i3DLC4jd4MoYSZMYLKQNUTpkjKAJ0snIcihvkrjt2jpvv34CifKJhqVtjFQ470pRW4XNx/pBBX07vdXpA3poxA==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-jsx-runtime": "^9.4.1", + "@floating-ui/devtools": "^0.2.3", + "@floating-ui/dom": "^1.6.12", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "@swc/helpers": "^0.5.1", + "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", @@ -2223,30 +3221,40 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-jsx-runtime": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.4.1.tgz", - "integrity": "sha512-ZodSm7jRa4kaLKDi+emfHFMP/IDnYwFQQAI2BdtKbVrvfwvzPRprGcnTgivnqKBT1ROvKOCY2ddz7+yZzesnNw==", + "node_modules/@fluentui/react-progress": { + "version": "9.4.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.4.16.tgz", + "integrity": "sha512-IWVuD1hQoyIBK+RIGOCTc3HUPkdtOQghJPZ5uGwRrUlxGgpUV1h7rdAApiuQTWitrFfN6bP4PrsJmHT2DM2OFw==", "license": "MIT", "dependencies": { + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", - "react": ">=16.14.0 <20.0.0" + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-label": { - "version": "9.3.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.3.15.tgz", - "integrity": "sha512-ycmaQwC4tavA8WeDfgcay1Ywu/4goHq1NOeVxkyzWTPGA7rs+tdCgdZBQZLAsBK2XFaZiHs7l+KG9r1oIRKolA==", + "node_modules/@fluentui/react-provider": { + "version": "9.22.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.22.15.tgz", + "integrity": "sha512-a+ImgL9DOlylDM4UYPnxQTA3yXxbVj+O0iNEyTZ6fMzdMsHzpALU4GAq6tOyW4L7RaQtRBmNpVfwTCEKpqaTJQ==", "license": "MIT", "dependencies": { + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", + "@griffel/core": "^1.16.0", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, @@ -2257,14 +3265,15 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-link": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.7.4.tgz", - "integrity": "sha512-ILKFpo/QH1SRsLN9gopAyZT/b/xsGcdO4JxthEeuTRvpLD6gImvRplum8ySIlbTskVVzog6038bHUSYLMdN7OA==", + "node_modules/@fluentui/react-radio": { + "version": "9.5.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.5.16.tgz", + "integrity": "sha512-xHRqm+MTkIf6JLEz/dMLlHSL9X+ysXAkig+VOV5QTPZwDIr3SqfJVvBmLNUVmtzf+cmWsRKrrIbVGpFGo/CvxA==", "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-field": "^9.4.16", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.3.15", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -2279,15 +3288,13 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-list": { - "version": "9.6.11", - "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.11.tgz", - "integrity": "sha512-ao1WdgWDrz4mTvic3dOD3Jk1V9XcppxX3Y3DI7Emsw2QI9Y2AsZBtiUrqYNEQ0ym3yFobURYJ3ZIhrW11VCKAw==", + "node_modules/@fluentui/react-rating": { + "version": "9.3.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.3.15.tgz", + "integrity": "sha512-MH/Jgoco8p+haf1d5Gi+d5VCjwd0qE6y/uP0YJsB9m11+DFnDxgKhzJKIiIzs3yzB2M4bMM8z9SqEHzQGCQEPg==", "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-checkbox": "^9.5.16", - "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", @@ -2303,23 +3310,16 @@ "react-dom": ">=16.8.0 <20.0.0" } }, - "node_modules/@fluentui/react-menu": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.22.0.tgz", - "integrity": "sha512-RPZvqHsxMDEArsz80mJabs1fVGPlCrhMntzM/wt3Bga+fyPv4yEuDdN5FB8JqUpIAjRZneiW0RLC0Mr3WqmatA==", + "node_modules/@fluentui/react-search": { + "version": "9.3.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.3.16.tgz", + "integrity": "sha512-7dKzGqIXzfhYxIKI1arGARkUDyQHYfwArlR6jKrhmYppXJh7U174xsjkMH62B78rDdNVer3G38MXXjpQ5MvNAQ==", "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-input": "^9.7.16", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", - "@fluentui/react-portal": "^9.8.11", - "@fluentui/react-positioning": "^9.22.0", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -2332,18 +3332,15 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-message-bar": { - "version": "9.6.21", - "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.6.21.tgz", - "integrity": "sha512-Vba3+7+TuzH2Ma6YB/Sd5dy+dm4DWwacZc0a78CetVqCzYZ4u/5opdmiBs8JY1Qr8uYW38siHLbY8kLnu6OOjA==", + "node_modules/@fluentui/react-select": { + "version": "9.4.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.4.16.tgz", + "integrity": "sha512-YsHMZsiKxH8suBtNTBXhtsvjM0u9UUXH641cEumgtjUz7SzeKNc/cWToLVyNz7GIoANL49rvubkByTeAQVCo2g==", "license": "MIT", "dependencies": { - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-field": "^9.4.16", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-link": "^9.7.4", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", @@ -2351,37 +3348,38 @@ "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@types/react": ">=16.8.0 <20.0.0", - "@types/react-dom": ">=16.8.0 <20.0.0", + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.8.0 <20.0.0" + "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-motion": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.13.0.tgz", - "integrity": "sha512-YdOpW6e7qfvzoWKcqh8hReCqwYEoiEmNBcCprGaupKjWOi9jBbF/JESM1AHI9nOjPd8aY90WUG2+ahvrqfL9LA==", + "node_modules/@fluentui/react-shared-contexts": { + "version": "9.26.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.26.2.tgz", + "integrity": "sha512-upKXkwlIp5oIhELr4clAZXQkuCd4GDXM6GZEz8BOmRO+PnxyqmycCXvxDxsmi6XN+0vkGM4joiIgkB14o/FctQ==", "license": "MIT", "dependencies": { - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-utilities": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@types/react": ">=16.8.0 <20.0.0", - "@types/react-dom": ">=16.8.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.8.0 <20.0.0" + "@types/react": ">=16.14.0 <20.0.0", + "react": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-motion-components-preview": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.15.2.tgz", - "integrity": "sha512-KqHRV8lLmVwOWiHBdpUFA+TwMbuYu9cyzNvmhbMFLVKzZyr3MPgN+97Tf+6QYPf22o99SMT0BPySDv/HiNYanA==", + "node_modules/@fluentui/react-skeleton": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.5.0.tgz", + "integrity": "sha512-hYmtzFV47HezmW+v6EcHJOz560uuBahn3iZQpUrfyOmKFMM5Ou1Hc1lq62vuxuA9pybEqwZsaMRydGP3Ms23YQ==", "license": "MIT", "dependencies": { - "@fluentui/react-motion": "*", - "@fluentui/react-utilities": "*", + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2391,25 +3389,17 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-nav": { - "version": "9.3.20", - "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.20.tgz", - "integrity": "sha512-YIObOcR92Nz4OUePrDhRdLQ5m9ph0y+U7U9NYgE/XFrLtWl+uqUS7u36m3NJl9QGgZVpUHO4nbNjizGLkncCCA==", + "node_modules/@fluentui/react-slider": { + "version": "9.5.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.5.16.tgz", + "integrity": "sha512-IgFdKcnX1KXLpfaB9/CYPgAmC7lfJ0FGEl1Y1uHYiL2YV6Dc+4yoAsCBABC1/KcEeafqCiaFTdNhS62QRK7Tbg==", "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-divider": "^9.6.2", - "@fluentui/react-drawer": "^9.11.5", - "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-field": "^9.4.16", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-tooltip": "^9.9.3", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" @@ -2421,14 +3411,17 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-overflow": { - "version": "9.7.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.7.1.tgz", - "integrity": "sha512-Ml1GlcLrAUv31d9WN15WGOZv32gzDtZD5Mp1MOQ3ichDfTtxrswIch7MDzZ8hLMGf/7Y2IzBpV8iFR1XdSrGBA==", + "node_modules/@fluentui/react-spinbutton": { + "version": "9.5.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.5.16.tgz", + "integrity": "sha512-V4U9PSJM26BXrFqJ9K/VYYQeusBf8ldx5KOlZZ7hRamPsKTS5hyytWrF39lTLqCRlGckXPCLNzJpb1DLB+ID1g==", "license": "MIT", "dependencies": { - "@fluentui/priority-overflow": "^9.3.0", - "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -2441,15 +3434,14 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-persona": { - "version": "9.6.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.6.2.tgz", - "integrity": "sha512-60kOmljlYjUiySWDN1bZh1FB4C7jbJS2dobtBJQh5agnKg34p3egO+6MwsBHRcwaGhVMh4T8XcbE6t2hw+iqyQ==", + "node_modules/@fluentui/react-spinner": { + "version": "9.7.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.7.15.tgz", + "integrity": "sha512-ZMJ7y08yvVXL9HuiMLLCy1cRn8plR9A4mL57CM2/otaXVWQbOwRaFD0/+Dx3u9A8sEtdYLo6O9gJIjU8fZGaYw==", "license": "MIT", "dependencies": { - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-badge": "^9.4.15", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.3.15", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", @@ -2463,20 +3455,40 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-popover": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.14.0.tgz", - "integrity": "sha512-XrZlSfSYhA12j5bna4Sq8N/If2vul7gl8woVrN8U3iQUjdaHB6OAMZ/WMNUdMm35Z+4e4rHClAZxU2dUsbHrmw==", + "node_modules/@fluentui/react-swatch-picker": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.5.0.tgz", + "integrity": "sha512-sl7MifqQGR4QGDhhgBIYc25YgPuFQW7+BOfNRMO5DYPq33lX5xHNcczhXywcBESAVHrjM0MC1lsE7glv6gU8RA==", "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", - "@fluentui/react-portal": "^9.8.11", - "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-switch": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.6.1.tgz", + "integrity": "sha512-UVHJViXSR5jrNyjtU3yqhr1F14TbY8V59wMw9N1vP027ztrLx3Q30sEt0xG1TXv5BoAERnXhHws9HVIxBpRvEA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.3.15", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -2491,14 +3503,23 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-portal": { - "version": "9.8.11", - "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.8.11.tgz", - "integrity": "sha512-2eg4MdW7e2UGRYWPg05GCytAjWYNd55YOP9+iUDINoQwwto9oeFTtZRyn08HYw37cSNqoH24qGz/VBctzTkqDA==", + "node_modules/@fluentui/react-table": { + "version": "9.19.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.11.tgz", + "integrity": "sha512-0ivIFR2JAp3HYlPnDrV5axBaOH06wtsQArBSOw6HXbQEz9JQ8Gi9SqEhQo6DBQ1/pcY3XeZjP+3r2HoFZXGaqA==", "license": "MIT", "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-avatar": "^9.10.2", + "@fluentui/react-checkbox": "^9.5.16", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-radio": "^9.5.16", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" @@ -2510,20 +3531,20 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-positioning": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.22.0.tgz", - "integrity": "sha512-i3DLC4jd4MoYSZMYLKQNUTpkjKAJ0snIcihvkrjt2jpvv34CifKJhqVtjFQ470pRW4XNx/pBBX07vdXpA3poxA==", + "node_modules/@fluentui/react-tabs": { + "version": "9.11.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.11.2.tgz", + "integrity": "sha512-zmWzySlPM9EwHJNW0/JhyxBCqBvmfZIj1OZLdRDpbPDsKjhO0aGZV6WjLHFYJmq58kbN0wHKUbxc7LfafHHUwA==", "license": "MIT", "dependencies": { - "@floating-ui/devtools": "^0.2.3", - "@floating-ui/dom": "^1.6.12", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1", - "use-sync-external-store": "^1.2.0" + "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", @@ -2532,19 +3553,19 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-progress": { - "version": "9.4.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.4.16.tgz", - "integrity": "sha512-IWVuD1hQoyIBK+RIGOCTc3HUPkdtOQghJPZ5uGwRrUlxGgpUV1h7rdAApiuQTWitrFfN6bP4PrsJmHT2DM2OFw==", + "node_modules/@fluentui/react-tabster": { + "version": "9.26.13", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.26.13.tgz", + "integrity": "sha512-uOuJj7jn1ME52Vc685/Ielf6srK/sfFQA5zBIbXIvy2Eisfp7R1RmJe2sXWoszz/Fu/XDkPwdM/GLv23N3vrvQ==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "@swc/helpers": "^0.5.1", + "keyborg": "^2.6.0", + "tabster": "^8.5.5" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", @@ -2553,19 +3574,26 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-provider": { - "version": "9.22.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.22.15.tgz", - "integrity": "sha512-a+ImgL9DOlylDM4UYPnxQTA3yXxbVj+O0iNEyTZ6fMzdMsHzpALU4GAq6tOyW4L7RaQtRBmNpVfwTCEKpqaTJQ==", + "node_modules/@fluentui/react-tag-picker": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.8.2.tgz", + "integrity": "sha512-j8a9X3jychd9KQ7uhzjoyDT8hcAH40d+ZeHXCLQ8PcYfDdoZSDWcmLNc+xCGmlf+UkhWQU1Ks7hdWqBjGpr0MA==", "license": "MIT", "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-combobox": "^9.16.18", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-field": "^9.4.16", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-tags": "^9.7.17", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", - "@griffel/core": "^1.16.0", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, @@ -2576,15 +3604,17 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-radio": { - "version": "9.5.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.5.16.tgz", - "integrity": "sha512-xHRqm+MTkIf6JLEz/dMLlHSL9X+ysXAkig+VOV5QTPZwDIr3SqfJVvBmLNUVmtzf+cmWsRKrrIbVGpFGo/CvxA==", + "node_modules/@fluentui/react-tags": { + "version": "9.7.17", + "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.7.17.tgz", + "integrity": "sha512-LCJJqoXIiN+aNqFHC/5nddsQJqh56xzrywwpMbMrQYI/dbIk5UYlmZ6arIPhQ9HVKat3YzGKAvOGlhFhEHIwDg==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.16", + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-avatar": "^9.10.2", + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -2599,20 +3629,25 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-rating": { - "version": "9.3.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.3.15.tgz", - "integrity": "sha512-MH/Jgoco8p+haf1d5Gi+d5VCjwd0qE6y/uP0YJsB9m11+DFnDxgKhzJKIiIzs3yzB2M4bMM8z9SqEHzQGCQEPg==", + "node_modules/@fluentui/react-teaching-popover": { + "version": "9.6.18", + "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.18.tgz", + "integrity": "sha512-cf76vSRZs40geZEw/RChfQvu6ioMyFKR0qvPc52QstPDC/cgGkOg+45G7SZo11IpYwBdkpUVWasnWUWSxTMiHw==", "license": "MIT", "dependencies": { + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-button": "^9.8.2", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-popover": "^9.14.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "@swc/helpers": "^0.5.1", + "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "@types/react": ">=16.8.0 <20.0.0", @@ -2621,14 +3656,12 @@ "react-dom": ">=16.8.0 <20.0.0" } }, - "node_modules/@fluentui/react-search": { - "version": "9.3.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.3.16.tgz", - "integrity": "sha512-7dKzGqIXzfhYxIKI1arGARkUDyQHYfwArlR6jKrhmYppXJh7U174xsjkMH62B78rDdNVer3G38MXXjpQ5MvNAQ==", + "node_modules/@fluentui/react-text": { + "version": "9.6.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.6.15.tgz", + "integrity": "sha512-YB1azhq8MGfnYTGlEAX1mzcFZ6CvqkkaxaCogU4TM9BtPgQ1YUAxE01RMenl8VVi8W9hNbJKkuc8R8GzYwzT4Q==", "license": "MIT", "dependencies": { - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-input": "^9.7.16", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", @@ -2643,14 +3676,13 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-select": { - "version": "9.4.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.4.16.tgz", - "integrity": "sha512-YsHMZsiKxH8suBtNTBXhtsvjM0u9UUXH641cEumgtjUz7SzeKNc/cWToLVyNz7GIoANL49rvubkByTeAQVCo2g==", + "node_modules/@fluentui/react-textarea": { + "version": "9.6.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.6.16.tgz", + "integrity": "sha512-d72Ufs//9T+X7lIrY1D28/9BiVqtKSjZ5hHVgBnJJwuPSFAKn5b4jlysXkNKHEdMjJz57kYMK4Ieneyz+Xkhrw==", "license": "MIT", "dependencies": { "@fluentui/react-field": "^9.4.16", - "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", @@ -2665,29 +3697,31 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-shared-contexts": { - "version": "9.26.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.26.2.tgz", - "integrity": "sha512-upKXkwlIp5oIhELr4clAZXQkuCd4GDXM6GZEz8BOmRO+PnxyqmycCXvxDxsmi6XN+0vkGM4joiIgkB14o/FctQ==", + "node_modules/@fluentui/react-theme": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.2.1.tgz", + "integrity": "sha512-lJxfz7LmmglFz+c9C41qmMqaRRZZUPtPPl9DWQ79vH+JwZd4dkN7eA78OTRwcGCOTPEKoLTX72R+EFaWEDlX+w==", "license": "MIT", "dependencies": { - "@fluentui/react-theme": "^9.2.1", + "@fluentui/tokens": "1.0.0-alpha.23", "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "react": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-skeleton": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.5.0.tgz", - "integrity": "sha512-hYmtzFV47HezmW+v6EcHJOz560uuBahn3iZQpUrfyOmKFMM5Ou1Hc1lq62vuxuA9pybEqwZsaMRydGP3Ms23YQ==", + "node_modules/@fluentui/react-toast": { + "version": "9.7.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.15.tgz", + "integrity": "sha512-iuk4rf/WumpGrNIpRVLNamlPBY0rT9BhI4qTnVmzXqz5pY+8GmAq/TKUPER9/withtQW8V9srj91FWblxzpHRg==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.16", + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.13.0", + "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-portal": "^9.8.11", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -2700,14 +3734,17 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-slider": { - "version": "9.5.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.5.16.tgz", - "integrity": "sha512-IgFdKcnX1KXLpfaB9/CYPgAmC7lfJ0FGEl1Y1uHYiL2YV6Dc+4yoAsCBABC1/KcEeafqCiaFTdNhS62QRK7Tbg==", + "node_modules/@fluentui/react-toolbar": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.7.4.tgz", + "integrity": "sha512-cpr+vJAzHJckN4S+JFSIeH4cg6q8pQuLVldH3ETrtNnWKERHeiY9ljAq3fbi/fU7ohgDit0DZnWUACrNu0pQQA==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.16", + "@fluentui/react-button": "^9.8.2", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-divider": "^9.6.2", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-radio": "^9.5.16", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -2722,17 +3759,18 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-spinbutton": { - "version": "9.5.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.5.16.tgz", - "integrity": "sha512-V4U9PSJM26BXrFqJ9K/VYYQeusBf8ldx5KOlZZ7hRamPsKTS5hyytWrF39lTLqCRlGckXPCLNzJpb1DLB+ID1g==", + "node_modules/@fluentui/react-tooltip": { + "version": "9.9.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.9.3.tgz", + "integrity": "sha512-a351JFoaBAOn0SnQ76tzuNv2ieHzAS+VO8Ncy4m9/emrIs5lvBBfKX8fvA4/efVxY+683XEQdoL1LuApuJuTWw==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -2745,15 +3783,25 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-spinner": { - "version": "9.7.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.7.15.tgz", - "integrity": "sha512-ZMJ7y08yvVXL9HuiMLLCy1cRn8plR9A4mL57CM2/otaXVWQbOwRaFD0/+Dx3u9A8sEtdYLo6O9gJIjU8fZGaYw==", + "node_modules/@fluentui/react-tree": { + "version": "9.15.13", + "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.15.13.tgz", + "integrity": "sha512-ITT8SlYXfG+Wi0FYPJOqwROTa6Po2VZEtolUq9jPjMy5/q+Vto++fdHyWaVn3a5Joq6w576RDP1ZnlS7qoFPgg==", "license": "MIT", "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-avatar": "^9.10.2", + "@fluentui/react-button": "^9.8.2", + "@fluentui/react-checkbox": "^9.5.16", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", + "@fluentui/react-motion": "^9.13.0", + "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-radio": "^9.5.16", "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -2766,43 +3814,29 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-swatch-picker": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.5.0.tgz", - "integrity": "sha512-sl7MifqQGR4QGDhhgBIYc25YgPuFQW7+BOfNRMO5DYPq33lX5xHNcczhXywcBESAVHrjM0MC1lsE7glv6gU8RA==", + "node_modules/@fluentui/react-utilities": { + "version": "9.26.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.26.2.tgz", + "integrity": "sha512-Yp2GGNoWifj8Z/VVir4HyRumRsqXnLJd4IP/Y70vEm9ruAvyqUvfn+1lQUuA+k/Reqw8GI+Ix7FTo3rogixZBg==", "license": "MIT", "dependencies": { - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@types/react": ">=16.8.0 <20.0.0", - "@types/react-dom": ">=16.8.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.8.0 <20.0.0" + "@types/react": ">=16.14.0 <20.0.0", + "react": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-switch": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.6.1.tgz", - "integrity": "sha512-UVHJViXSR5jrNyjtU3yqhr1F14TbY8V59wMw9N1vP027ztrLx3Q30sEt0xG1TXv5BoAERnXhHws9HVIxBpRvEA==", + "node_modules/@fluentui/react-virtualizer": { + "version": "9.0.0-alpha.111", + "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.111.tgz", + "integrity": "sha512-yku++0779Ve1RNz6y/HWjlXKd2x1wCSbWMydT2IdCICBVwolXjPYMpkqqZUSjbJ0N9gl6BfsCBpU9Dfe2bR8Zg==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" @@ -2814,609 +3848,678 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui/react-table": { - "version": "9.19.11", - "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.11.tgz", - "integrity": "sha512-0ivIFR2JAp3HYlPnDrV5axBaOH06wtsQArBSOw6HXbQEz9JQ8Gi9SqEhQo6DBQ1/pcY3XeZjP+3r2HoFZXGaqA==", + "node_modules/@fluentui/tokens": { + "version": "1.0.0-alpha.23", + "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", + "integrity": "sha512-uxrzF9Z+J10naP0pGS7zPmzSkspSS+3OJDmYIK3o1nkntQrgBXq3dBob4xSlTDm5aOQ0kw6EvB9wQgtlyy4eKQ==", "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-checkbox": "^9.5.16", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-radio": "^9.5.16", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@formkit/tempo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@formkit/tempo/-/tempo-1.0.0.tgz", + "integrity": "sha512-2jhR8jnWFt4ajtbpHL2ZvJJ+ofj/XpAumb1OLmsY7U5iwCjGKuk0wqLR7dGsZfyvJeivfWLD9vzj9cdkUUWzyg==", + "license": "MIT" + }, + "node_modules/@griffel/core": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.20.1.tgz", + "integrity": "sha512-ld1mX04zpmeHn8agx4slSEh8kJ+8or3Y0x9gsJNKSKn6GdCkZBSiGUh+oBXCBn8RKzz8l60TA9IhVSStnyKekA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.4.0", + "csstype": "^3.1.3", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@griffel/react": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.6.1.tgz", + "integrity": "sha512-mNM4/+dIXzqeHboWpVZ1/jiwTAYNc5/8y/V/HasnQ2QXnV6gSUYpeUk/0n6IFU3NJmVJly9JrLSfNo0hM/IFeA==", + "license": "MIT", + "dependencies": { + "@griffel/core": "^1.20.1", + "tslib": "^2.1.0" }, "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "react": ">=16.8.0 <20.0.0" } }, - "node_modules/@fluentui/react-tabs": { - "version": "9.11.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.11.2.tgz", - "integrity": "sha512-zmWzySlPM9EwHJNW0/JhyxBCqBvmfZIj1OZLdRDpbPDsKjhO0aGZV6WjLHFYJmq58kbN0wHKUbxc7LfafHHUwA==", + "node_modules/@griffel/style-types": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.4.0.tgz", + "integrity": "sha512-vNDfOGV7RN/XkA7vxgf7Z5HgW8eiBm5cHT9wQPhsKB4pxWom5u6eQ9CkYE5mCCTSPl9H6Nd1NBai04d4P6BD7Q==", "license": "MIT", "dependencies": { - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "csstype": "^3.1.3" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@fluentui/react-tabster": { - "version": "9.26.13", - "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.26.13.tgz", - "integrity": "sha512-uOuJj7jn1ME52Vc685/Ielf6srK/sfFQA5zBIbXIvy2Eisfp7R1RmJe2sXWoszz/Fu/XDkPwdM/GLv23N3vrvQ==", - "license": "MIT", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", "dependencies": { - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1", - "keyborg": "^2.6.0", - "tabster": "^8.5.5" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": ">=12" } }, - "node_modules/@fluentui/react-tag-picker": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.8.2.tgz", - "integrity": "sha512-j8a9X3jychd9KQ7uhzjoyDT8hcAH40d+ZeHXCLQ8PcYfDdoZSDWcmLNc+xCGmlf+UkhWQU1Ks7hdWqBjGpr0MA==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-combobox": "^9.16.18", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-portal": "^9.8.11", - "@fluentui/react-positioning": "^9.22.0", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-tags": "^9.7.17", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@fluentui/react-tags": { - "version": "9.7.17", - "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.7.17.tgz", - "integrity": "sha512-LCJJqoXIiN+aNqFHC/5nddsQJqh56xzrywwpMbMrQYI/dbIk5UYlmZ6arIPhQ9HVKat3YzGKAvOGlhFhEHIwDg==", - "license": "MIT", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/@fluentui/react-teaching-popover": { - "version": "9.6.18", - "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.18.tgz", - "integrity": "sha512-cf76vSRZs40geZEw/RChfQvu6ioMyFKR0qvPc52QstPDC/cgGkOg+45G7SZo11IpYwBdkpUVWasnWUWSxTMiHw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-popover": "^9.14.0", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1", - "use-sync-external-store": "^1.2.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, - "peerDependencies": { - "@types/react": ">=16.8.0 <20.0.0", - "@types/react-dom": ">=16.8.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.8.0 <20.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/@fluentui/react-text": { - "version": "9.6.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.6.15.tgz", - "integrity": "sha512-YB1azhq8MGfnYTGlEAX1mzcFZ6CvqkkaxaCogU4TM9BtPgQ1YUAxE01RMenl8VVi8W9hNbJKkuc8R8GzYwzT4Q==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "p-locate": "^4.1.0" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/@fluentui/react-textarea": { - "version": "9.6.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.6.16.tgz", - "integrity": "sha512-d72Ufs//9T+X7lIrY1D28/9BiVqtKSjZ5hHVgBnJJwuPSFAKn5b4jlysXkNKHEdMjJz57kYMK4Ieneyz+Xkhrw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.16", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "p-try": "^2.0.0" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@fluentui/react-theme": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.2.1.tgz", - "integrity": "sha512-lJxfz7LmmglFz+c9C41qmMqaRRZZUPtPPl9DWQ79vH+JwZd4dkN7eA78OTRwcGCOTPEKoLTX72R+EFaWEDlX+w==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/tokens": "1.0.0-alpha.23", - "@swc/helpers": "^0.5.1" + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@fluentui/react-toast": { - "version": "9.7.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.15.tgz", - "integrity": "sha512-iuk4rf/WumpGrNIpRVLNamlPBY0rT9BhI4qTnVmzXqz5pY+8GmAq/TKUPER9/withtQW8V9srj91FWblxzpHRg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", - "@fluentui/react-portal": "^9.8.11", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/@fluentui/react-toolbar": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.7.4.tgz", - "integrity": "sha512-cpr+vJAzHJckN4S+JFSIeH4cg6q8pQuLVldH3ETrtNnWKERHeiY9ljAq3fbi/fU7ohgDit0DZnWUACrNu0pQQA==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, "license": "MIT", - "dependencies": { - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-divider": "^9.6.2", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-radio": "^9.5.16", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/@fluentui/react-tooltip": { - "version": "9.9.3", - "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.9.3.tgz", - "integrity": "sha512-a351JFoaBAOn0SnQ76tzuNv2ieHzAS+VO8Ncy4m9/emrIs5lvBBfKX8fvA4/efVxY+683XEQdoL1LuApuJuTWw==", + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-portal": "^9.8.11", - "@fluentui/react-positioning": "^9.22.0", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@fluentui/react-tree": { - "version": "9.15.13", - "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.15.13.tgz", - "integrity": "sha512-ITT8SlYXfG+Wi0FYPJOqwROTa6Po2VZEtolUq9jPjMy5/q+Vto++fdHyWaVn3a5Joq6w576RDP1ZnlS7qoFPgg==", + "node_modules/@jest/console/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-checkbox": "^9.5.16", - "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", - "@fluentui/react-radio": "^9.5.16", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@fluentui/react-utilities": { - "version": "9.26.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.26.2.tgz", - "integrity": "sha512-Yp2GGNoWifj8Z/VVir4HyRumRsqXnLJd4IP/Y70vEm9ruAvyqUvfn+1lQUuA+k/Reqw8GI+Ix7FTo3rogixZBg==", + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-shared-contexts": "^9.26.2", - "@swc/helpers": "^0.5.1" + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "react": ">=16.14.0 <20.0.0" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@fluentui/react-virtualizer": { - "version": "9.0.0-alpha.111", - "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.111.tgz", - "integrity": "sha512-yku++0779Ve1RNz6y/HWjlXKd2x1wCSbWMydT2IdCICBVwolXjPYMpkqqZUSjbJ0N9gl6BfsCBpU9Dfe2bR8Zg==", + "node_modules/@jest/core/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, "license": "MIT", "dependencies": { - "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-utilities": "^9.26.2", - "@griffel/react": "^1.5.32", - "@swc/helpers": "^0.5.1" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, - "peerDependencies": { - "@types/react": ">=16.14.0 <20.0.0", - "@types/react-dom": ">=16.9.0 <20.0.0", - "react": ">=16.14.0 <20.0.0", - "react-dom": ">=16.14.0 <20.0.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@fluentui/tokens": { - "version": "1.0.0-alpha.23", - "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", - "integrity": "sha512-uxrzF9Z+J10naP0pGS7zPmzSkspSS+3OJDmYIK3o1nkntQrgBXq3dBob4xSlTDm5aOQ0kw6EvB9wQgtlyy4eKQ==", + "node_modules/@jest/create-cache-key-function": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.2.0.tgz", + "integrity": "sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==", + "dev": true, "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.1" + "@jest/types": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@formkit/tempo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@formkit/tempo/-/tempo-1.0.0.tgz", - "integrity": "sha512-2jhR8jnWFt4ajtbpHL2ZvJJ+ofj/XpAumb1OLmsY7U5iwCjGKuk0wqLR7dGsZfyvJeivfWLD9vzj9cdkUUWzyg==", - "license": "MIT" - }, - "node_modules/@griffel/core": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.20.1.tgz", - "integrity": "sha512-ld1mX04zpmeHn8agx4slSEh8kJ+8or3Y0x9gsJNKSKn6GdCkZBSiGUh+oBXCBn8RKzz8l60TA9IhVSStnyKekA==", + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@griffel/style-types": "^1.4.0", - "csstype": "^3.1.3", - "rtl-css-js": "^1.16.1", - "stylis": "^4.2.0", - "tslib": "^2.1.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@griffel/react": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.6.1.tgz", - "integrity": "sha512-mNM4/+dIXzqeHboWpVZ1/jiwTAYNc5/8y/V/HasnQ2QXnV6gSUYpeUk/0n6IFU3NJmVJly9JrLSfNo0hM/IFeA==", + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, "license": "MIT", "dependencies": { - "@griffel/core": "^1.20.1", - "tslib": "^2.1.0" + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" }, - "peerDependencies": { - "react": ">=16.8.0 <20.0.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@griffel/style-types": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.4.0.tgz", - "integrity": "sha512-vNDfOGV7RN/XkA7vxgf7Z5HgW8eiBm5cHT9wQPhsKB4pxWom5u6eQ9CkYE5mCCTSPl9H6Nd1NBai04d4P6BD7Q==", + "node_modules/@jest/environment/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.1.3" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, "engines": { - "node": ">=18.18.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" + "expect": "30.3.0", + "jest-snapshot": "30.3.0" }, "engines": { - "node": ">=18.18.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@jest/fake-timers/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">=12" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@jest/globals/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@types/node": "*", + "jest-regex-util": "30.0.1" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@jest/reporters/node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/console": { + "node_modules/@jest/test-result": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", - "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", "dev": true, "license": "MIT", "dependencies": { + "@jest/console": "30.3.0", "@jest/types": "30.3.0", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "slash": "^3.0.0" + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/console/node_modules/@jest/types": { + "node_modules/@jest/test-result/node_modules/@jest/types": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", @@ -3435,54 +4538,49 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/core": { + "node_modules/@jest/test-sequencer": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", - "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.3.0", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.3.0", "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", "@jest/types": "30.3.0", - "@types/node": "*", - "ansi-escapes": "^4.3.2", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.3.0", - "jest-config": "30.3.0", "jest-haste-map": "30.3.0", - "jest-message-util": "30.3.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-resolve-dependencies": "30.3.0", - "jest-runner": "30.3.0", - "jest-runtime": "30.3.0", - "jest-snapshot": "30.3.0", "jest-util": "30.3.0", - "jest-validate": "30.3.0", - "jest-watcher": "30.3.0", - "pretty-format": "30.3.0", - "slash": "^3.0.0" + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } } }, - "node_modules/@jest/core/node_modules/@jest/types": { + "node_modules/@jest/transform/node_modules/@jest/types": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", @@ -3501,915 +4599,1118 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/create-cache-key-function": { + "node_modules/@jest/types": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.2.0.tgz", - "integrity": "sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/diff-sequences": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", - "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", - "dev": true, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=6.0.0" } }, - "node_modules/@jest/environment": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", - "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.3.0", - "@jest/types": "30.3.0", - "@types/node": "*", - "jest-mock": "30.3.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jest/environment/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jest/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "expect": "30.3.0", - "jest-snapshot": "30.3.0" + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jest/expect-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", - "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/1ds-core-js": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.10.tgz", + "integrity": "sha512-5fSZmkGwWkH+mrIA5M1GYPZdPM+SjXwCCl2Am7VhFoVwOBJNhRnwvIpAdzw6sFjiebN/rz+/YH0NdxztGZSa9Q==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" } }, - "node_modules/@jest/fake-timers": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", - "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", - "dev": true, + "node_modules/@microsoft/1ds-post-js": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.10.tgz", + "integrity": "sha512-VSLjc9cT+Y+eTiSfYltJHJCejn8oYr0E6Pq2BMhOEO7F6IyLGYIxzKKvo78ze9x+iHX7KPTATcZ+PFgjGXuNqg==", "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", - "@sinonjs/fake-timers": "^15.0.0", - "@types/node": "*", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "@microsoft/1ds-core-js": "4.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" } }, - "node_modules/@jest/fake-timers/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", - "dev": true, + "node_modules/@microsoft/applicationinsights-channel-js": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.10.tgz", + "integrity": "sha512-iolFLz1ocWAzIQqHIEjjov3gNTPkgFQ4ArHnBcJEYoffOGWlJt6copaevS5YPI5rHzmbySsengZ8cLJJBBrXzQ==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "@microsoft/applicationinsights-common": "3.3.10", + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "tslib": ">= 1.0.0" } }, - "node_modules/@jest/globals": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", - "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", - "dev": true, + "node_modules/@microsoft/applicationinsights-common": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.10.tgz", + "integrity": "sha512-RVIenPIvNgZCbjJdALvLM4rNHgAFuHI7faFzHCgnI6S2WCUNGHeXlQTs9EUUrL+n2TPp9/cd0KKMILU5VVyYiA==", "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/expect": "30.3.0", - "@jest/types": "30.3.0", - "jest-mock": "30.3.0" + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "tslib": ">= 1.0.0" } }, - "node_modules/@jest/globals/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", - "dev": true, + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.10.tgz", + "integrity": "sha512-5yKeyassZTq2l+SAO4npu6LPnbS++UD+M+Ghjm9uRzoBwD8tumFx0/F8AkSVqbniSREd+ztH/2q2foewa2RZyg==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "tslib": ">= 1.0.0" } }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, + "node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", "license": "MIT", "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, - "node_modules/@jest/reporters": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", - "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", - "dev": true, + "node_modules/@microsoft/applicationinsights-web-basic": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.10.tgz", + "integrity": "sha512-AZib5DAT3NU0VT0nLWEwXrnoMDDgZ/5S4dso01CNU5ELNxLdg+1fvchstlVdMy4FrAnxzs8Wf/GIQNFYOVgpAw==", "license": "MIT", "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "jest-worker": "30.3.0", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "@microsoft/applicationinsights-channel-js": "3.3.10", + "@microsoft/applicationinsights-common": "3.3.10", + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" }, "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "tslib": ">= 1.0.0" } }, - "node_modules/@jest/reporters/node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" + "node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" + } }, - "node_modules/@jest/reporters/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", - "dev": true, + "node_modules/@microsoft/vscode-azext-azureauth": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureauth/-/vscode-azext-azureauth-4.1.1.tgz", + "integrity": "sha512-ZHRoNBBwZBg8sqZ7/C+VMQOp7zusJxIpXzLlRpWGjrDI8PSFQk2bZloVdW877YeSLMe2464mjTlWMhEEa07Kaw==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "@azure/arm-resources-subscriptions": "^2.1.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.16.0", + "@azure/ms-rest-azure-env": "^2.0.0" } }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, + "node_modules/@microsoft/vscode-azext-azureutils": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-3.4.10.tgz", + "integrity": "sha512-lW3KZgGKn6alQ6SatsmHIVlxCaSktGrOdigM+TDYTQJR0+hryD+klYVNCf+c9J7VDCutI7q6M3wwsz1hvMu1bQ==", "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.34.0" + "@azure/arm-authorization": "^9.0.0", + "@azure/arm-authorization-profile-2020-09-01-hybrid": "^2.1.0", + "@azure/arm-msi": "^2.1.0", + "@azure/arm-resources": "^5.0.0", + "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", + "@azure/arm-resources-subscriptions": "^2.0.0", + "@azure/arm-storage": "^18.2.0", + "@azure/arm-storage-profile-2020-09-01-hybrid": "^2.0.0", + "@azure/core-client": "^1.6.0", + "@azure/core-rest-pipeline": "^1.9.0", + "@azure/logger": "^1.0.4", + "@microsoft/vscode-azext-utils": "^3.1.1", + "semver": "^7.3.7", + "uuid": "^9.0.0" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "@azure/ms-rest-azure-env": "^2.0.0" } }, - "node_modules/@jest/snapshot-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", - "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", - "dev": true, + "node_modules/@microsoft/vscode-azext-azureutils/node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" + "tslib": "^2.2.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12.0.0" } }, - "node_modules/@jest/snapshot-utils/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", - "dev": true, + "node_modules/@microsoft/vscode-azext-azureutils/node_modules/@azure/arm-resources": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-5.2.0.tgz", + "integrity": "sha512-wQyuhL8WQsLkW/KMdik8bLJIJCz3Z6mg/+AKm0KedgK73SKhicSqYP+ed3t+43tLlRFltcrmGKMcHLQ+Jhv/6A==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.7.0", + "@azure/core-lro": "^2.5.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=14.0.0" } }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "dev": true, + "node_modules/@microsoft/vscode-azext-azureutils/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@microsoft/vscode-azext-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-3.3.3.tgz", + "integrity": "sha512-rltLtVeUTUNHEeGzyw7A0GoRhHNBRWRpB6N2LEETBUXn5J06EqgXg/K6JxO2NCooCAi+eI+g1uSUCn2AM4DsTQ==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" + "@microsoft/vscode-azureresources-api": "^2.3.1", + "@vscode/extension-telemetry": "^0.9.6", + "dayjs": "^1.11.2", + "escape-string-regexp": "^2.0.0", + "html-to-text": "^8.2.0", + "semver": "^7.3.7", + "uuid": "^9.0.0", + "vscode-tas-client": "^0.1.84", + "vscode-uri": "^3.0.6" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "@azure/ms-rest-azure-env": "^2.0.0" } }, - "node_modules/@jest/test-result": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", - "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", - "dev": true, + "node_modules/@microsoft/vscode-azext-utils/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@microsoft/vscode-azureresources-api": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azureresources-api/-/vscode-azureresources-api-2.5.1.tgz", + "integrity": "sha512-CUlDVsau6RJA8F1IENnfA3N0XqyGQz/VJgh9QLnWjRkiBjW8/VSUpxNab6qflnN9SS38EHGv81v/92kEkoOQKQ==", + "license": "MIT", + "peerDependencies": { + "@azure/ms-rest-azure-env": "^2.0.0" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", "license": "MIT", "dependencies": { - "@jest/console": "30.3.0", - "@jest/types": "30.3.0", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "state-local": "^1.0.6" } }, - "node_modules/@jest/test-result/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", - "dev": true, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "@monaco-editor/loader": "^1.5.0" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@jest/test-sequencer": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", - "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", - "dev": true, - "license": "MIT", + "node_modules/@mongodb-js/devtools-connect": { + "version": "3.14.12", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.14.12.tgz", + "integrity": "sha512-QAXnktl6CVQjiKLNbbwxvP3Hy/Ee4bs5GMVvb8v/uCaRqvmHHCCe2XsEPiyxmx+4HjlTFHrIJyefLqWZ7dt1Gw==", + "license": "Apache-2.0", "dependencies": { - "@jest/test-result": "30.3.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", - "slash": "^3.0.0" + "@mongodb-js/devtools-proxy-support": "^0.7.5", + "@mongodb-js/oidc-http-server-pages": "1.2.6", + "lodash.merge": "^4.6.2", + "mongodb-connection-string-url": "^3.0.1 || ^7.0.0", + "socks": "^2.7.3" + }, + "optionalDependencies": { + "kerberos": "^2.1.0 || ^7.0.0", + "mongodb-client-encryption": "^6.5.0 || ^7.0.0", + "os-dns-native": "^2.0.1", + "resolve-mongodb-srv": "^1.1.1" }, + "peerDependencies": { + "@mongodb-js/oidc-plugin": "^2.0.0", + "mongodb": "^6.9.0 || ^7.0.0", + "mongodb-log-writer": "^2.5.6" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.7.5.tgz", + "integrity": "sha512-XyhBcPruuSiWm6ps82/5DPgjbW/N6/Y/d1H32hw8Q5bQxdGaMQDam4QoB+NJPneKVr/2DCQdFbU6yVK/qfG2og==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.4.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.17.0", + "system-ca": "^3.0.0" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "20 || >=22" } }, - "node_modules/@jest/transform": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", - "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", - "dev": true, - "license": "MIT", + "node_modules/@mongodb-js/explain-plan-helper": { + "version": "1.4.24", + "resolved": "https://registry.npmjs.org/@mongodb-js/explain-plan-helper/-/explain-plan-helper-1.4.24.tgz", + "integrity": "sha512-JKX44aUFBAUlGkIw6Ad7Ov0WCidmrt0Z8Q5aGijLFTYGKaRN99lA5o1hDnUHC2f3MFasze5GNTLaP7TIwDQaZw==", + "license": "SSPL", "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.3.0", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.3.0", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" + "@mongodb-js/shell-bson-parser": "^1.2.0", + "mongodb-explain-compat": "^3.3.23" + } + }, + "node_modules/@mongodb-js/oidc-http-server-pages": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.2.6.tgz", + "integrity": "sha512-z7ETHla5yCwtF18txA8l6G9WVskRNjDQRfM/D/6I6JdjnKaR1Dtsy9lH/LwyDn3O+z3RzPErGtU3mWRjVGWS5g==", + "license": "Apache-2.0" + }, + "node_modules/@mongodb-js/oidc-plugin": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-2.0.8.tgz", + "integrity": "sha512-UHmw5kppOZjOefxZuPcsWNtA3P7PbsjNYaKYz4tbWy6mlH0wkgdf55A3bHuW3SZmvL7ZhpuYTz+gTq2xCr7NVA==", + "license": "Apache-2.0", + "dependencies": { + "express": "^5.2.1", + "node-fetch": "^3.3.2", + "open": "^10.1.2", + "openid-client": "^6.6.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 20.19.2" } }, - "node_modules/@jest/transform/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", - "dev": true, + "node_modules/@mongodb-js/oidc-plugin/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.6" } }, - "node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, + "node_modules/@mongodb-js/oidc-plugin/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, + "node_modules/@mongodb-js/oidc-plugin/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, + "node_modules/@mongodb-js/oidc-plugin/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "engines": { + "node": ">=6.6.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "node_modules/@mongodb-js/oidc-plugin/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, + "node_modules/@mongodb-js/oidc-plugin/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "node_modules/@mongodb-js/oidc-plugin/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "engines": { + "node": ">= 0.8" } }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@mongodb-js/oidc-plugin/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, "engines": { - "node": ">=10.0" + "node": ">= 0.8" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@mongodb-js/oidc-plugin/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { - "node": ">=10.0" + "node": ">=0.10.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@mongodb-js/oidc-plugin/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { - "node": ">=10.0" + "node": ">= 0.8" + } + }, + "node_modules/@mongodb-js/oidc-plugin/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@mongodb-js/oidc-plugin/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">=10.0" + "node": ">=18" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@mongodb-js/oidc-plugin/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mongodb-js/oidc-plugin/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, - "peerDependencies": { - "tslib": "2" + "engines": { + "node": ">= 0.10" } }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@mongodb-js/oidc-plugin/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">=10.0" + "node": ">= 18" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@mongodb-js/oidc-plugin/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">=10.0" + "node": ">= 18" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@microsoft/1ds-core-js": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.10.tgz", - "integrity": "sha512-5fSZmkGwWkH+mrIA5M1GYPZdPM+SjXwCCl2Am7VhFoVwOBJNhRnwvIpAdzw6sFjiebN/rz+/YH0NdxztGZSa9Q==", + "node_modules/@mongodb-js/oidc-plugin/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-core-js": "3.3.10", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.4 < 2.x", - "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + "engines": { + "node": ">= 0.8" } }, - "node_modules/@microsoft/1ds-post-js": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.10.tgz", - "integrity": "sha512-VSLjc9cT+Y+eTiSfYltJHJCejn8oYr0E6Pq2BMhOEO7F6IyLGYIxzKKvo78ze9x+iHX7KPTATcZ+PFgjGXuNqg==", + "node_modules/@mongodb-js/oidc-plugin/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.3.10", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.4 < 2.x", - "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.10.tgz", - "integrity": "sha512-iolFLz1ocWAzIQqHIEjjov3gNTPkgFQ4ArHnBcJEYoffOGWlJt6copaevS5YPI5rHzmbySsengZ8cLJJBBrXzQ==", + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.3.10", - "@microsoft/applicationinsights-core-js": "3.3.10", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.4 < 2.x", - "@nevware21/ts-utils": ">= 0.11.8 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" + "sparse-bitfield": "^3.0.3" } }, - "node_modules/@microsoft/applicationinsights-common": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.10.tgz", - "integrity": "sha512-RVIenPIvNgZCbjJdALvLM4rNHgAFuHI7faFzHCgnI6S2WCUNGHeXlQTs9EUUrL+n2TPp9/cd0KKMILU5VVyYiA==", - "license": "MIT", + "node_modules/@mongodb-js/shell-bson-parser": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/shell-bson-parser/-/shell-bson-parser-1.5.6.tgz", + "integrity": "sha512-yzVLeOkRSE+r8scrDMJjL9zTSzypU/TLxF+INQLs3yQX9a2R6IfBDDqdSVFyHVWv1FhZN0lVeqEWTsX+Iz5BaA==", + "license": "Apache-2.0", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.3.10", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + "acorn": "^8.14.1" }, "peerDependencies": { - "tslib": ">= 1.0.0" + "bson": "^4.6.3 || ^5 || ^6.10.3 || ^7.0.0" } }, - "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.10.tgz", - "integrity": "sha512-5yKeyassZTq2l+SAO4npu6LPnbS++UD+M+Ghjm9uRzoBwD8tumFx0/F8AkSVqbniSREd+ztH/2q2foewa2RZyg==", + "node_modules/@mongodb-js/socksv5": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@mongodb-js/socksv5/-/socksv5-0.0.10.tgz", + "integrity": "sha512-JDz2fLKsjMiSNUxKrCpGptsgu7DzsXfu4gnUQ3RhUaBS1d4YbLrt6HejpckAiHIAa+niBpZAeiUsoop0IihWsw==", "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.4 < 2.x", - "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + "ip-address": "^9.0.5" }, - "peerDependencies": { - "tslib": ">= 1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/@microsoft/applicationinsights-shims": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", - "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", - "license": "MIT", + "node_modules/@mongosh/async-rewriter2": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@mongosh/async-rewriter2/-/async-rewriter2-2.4.15.tgz", + "integrity": "sha512-J8DcehSHbfkZGXMtYVgBu071l0LA7WGkmj1+EeYQevk2RVsuVQXliW9w14VLhekPZ23o80Mqb+X0DypNuJHnsA==", + "license": "Apache-2.0", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@babel/core": "^7.26.10", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/types": "^7.27.0" + }, + "bin": { + "async-rewrite": "bin/async-rewrite.js" + }, + "engines": { + "node": ">=14.15.1" } }, - "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.10.tgz", - "integrity": "sha512-AZib5DAT3NU0VT0nLWEwXrnoMDDgZ/5S4dso01CNU5ELNxLdg+1fvchstlVdMy4FrAnxzs8Wf/GIQNFYOVgpAw==", - "license": "MIT", - "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.3.10", - "@microsoft/applicationinsights-common": "3.3.10", - "@microsoft/applicationinsights-core-js": "3.3.10", - "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.4 < 2.x", - "@nevware21/ts-utils": ">= 0.11.8 < 2.x" - }, - "peerDependencies": { - "tslib": ">= 1.0.0" + "node_modules/@mongosh/errors": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@mongosh/errors/-/errors-2.4.6.tgz", + "integrity": "sha512-Z3CDoh+EbHTLad5g/qpd1ti+PoCeq3KcbtNLg1RnQkcwC+4AqoOZt3X2JXY+s616vDLPUxpfoHqZsZqToq6lIA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.15.1" } }, - "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", - "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", - "license": "MIT", + "node_modules/@mongosh/i18n": { + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/@mongosh/i18n/-/i18n-2.20.2.tgz", + "integrity": "sha512-hkDxSEBK2a8DyMg8+5J3GmO0kuuebYWLa+oWq/V9Ghgq3l81nkkGFmCuu90goR4ZE9dCnNAqC/qtsuSUnvuGaA==", + "license": "Apache-2.0", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.4 < 2.x" + "@mongosh/errors": "2.4.6" + }, + "engines": { + "node": ">=14.15.1" } }, - "node_modules/@microsoft/vscode-azext-azureauth": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureauth/-/vscode-azext-azureauth-4.1.1.tgz", - "integrity": "sha512-ZHRoNBBwZBg8sqZ7/C+VMQOp7zusJxIpXzLlRpWGjrDI8PSFQk2bZloVdW877YeSLMe2464mjTlWMhEEa07Kaw==", - "license": "MIT", + "node_modules/@mongosh/service-provider-core": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@mongosh/service-provider-core/-/service-provider-core-5.0.3.tgz", + "integrity": "sha512-+Ih4m4BIElS8yBTZZqVSkr917pO54WCeggxC+UR/bkZ7XEOcWFOCCt3UF4ba3cNvxwZJU7PUbh9iE92wUSl/VQ==", + "license": "Apache-2.0", "dependencies": { - "@azure/arm-resources-subscriptions": "^2.1.0", - "@azure/core-client": "^1.9.2", - "@azure/core-rest-pipeline": "^1.16.0", - "@azure/ms-rest-azure-env": "^2.0.0" + "@mongosh/errors": "2.4.6", + "@mongosh/shell-bson": "3.0.3", + "bson": "^7.2.0", + "mongodb": "^7.1.0", + "mongodb-build-info": "^1.9.5", + "mongodb-connection-string-url": "^7.0.1" + }, + "engines": { + "node": ">=14.15.1" } }, - "node_modules/@microsoft/vscode-azext-azureutils": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-3.4.10.tgz", - "integrity": "sha512-lW3KZgGKn6alQ6SatsmHIVlxCaSktGrOdigM+TDYTQJR0+hryD+klYVNCf+c9J7VDCutI7q6M3wwsz1hvMu1bQ==", + "node_modules/@mongosh/service-provider-core/node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", "license": "MIT", "dependencies": { - "@azure/arm-authorization": "^9.0.0", - "@azure/arm-authorization-profile-2020-09-01-hybrid": "^2.1.0", - "@azure/arm-msi": "^2.1.0", - "@azure/arm-resources": "^5.0.0", - "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", - "@azure/arm-resources-subscriptions": "^2.0.0", - "@azure/arm-storage": "^18.2.0", - "@azure/arm-storage-profile-2020-09-01-hybrid": "^2.0.0", - "@azure/core-client": "^1.6.0", - "@azure/core-rest-pipeline": "^1.9.0", - "@azure/logger": "^1.0.4", - "@microsoft/vscode-azext-utils": "^3.1.1", - "semver": "^7.3.7", - "uuid": "^9.0.0" - }, - "peerDependencies": { - "@azure/ms-rest-azure-env": "^2.0.0" + "@types/webidl-conversions": "*" } }, - "node_modules/@microsoft/vscode-azext-azureutils/node_modules/@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", - "license": "MIT", + "node_modules/@mongosh/service-provider-core/node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.2.0" + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=20.19.0" } }, - "node_modules/@microsoft/vscode-azext-azureutils/node_modules/@azure/arm-resources": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-5.2.0.tgz", - "integrity": "sha512-wQyuhL8WQsLkW/KMdik8bLJIJCz3Z6mg/+AKm0KedgK73SKhicSqYP+ed3t+43tLlRFltcrmGKMcHLQ+Jhv/6A==", - "license": "MIT", + "node_modules/@mongosh/service-provider-node-driver": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@mongosh/service-provider-node-driver/-/service-provider-node-driver-5.0.6.tgz", + "integrity": "sha512-/gpRKGViK2ympjyqYhikjY7mjLQDgZo3drURUaqtGfgOmlkz5pps+WfDyIunx2F+a1Y3HM5f9Aiu37WggNbu2Q==", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.7.0", - "@azure/core-lro": "^2.5.0", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.8.0", - "tslib": "^2.2.0" + "@aws-sdk/credential-providers": "^3.1009.0", + "@mongodb-js/devtools-connect": "^3.14.9", + "@mongodb-js/oidc-plugin": "^2.0.8", + "@mongosh/errors": "2.4.6", + "@mongosh/service-provider-core": "5.0.3", + "@mongosh/types": "^5.0.3", + "gcp-metadata": "^7.0.1", + "mongodb": "^7.1.0", + "mongodb-build-info": "^1.9.6", + "mongodb-connection-string-url": "^7.0.1", + "socks": "^2.8.3" }, "engines": { - "node": ">=14.0.0" + "node": ">=14.15.1" + }, + "optionalDependencies": { + "kerberos": "^7.0.0", + "mongodb-client-encryption": "^7.0.0" } }, - "node_modules/@microsoft/vscode-azext-azureutils/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "node_modules/@mongosh/service-provider-node-driver/node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "dependencies": { + "@types/webidl-conversions": "*" } }, - "node_modules/@microsoft/vscode-azext-utils": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-3.3.3.tgz", - "integrity": "sha512-rltLtVeUTUNHEeGzyw7A0GoRhHNBRWRpB6N2LEETBUXn5J06EqgXg/K6JxO2NCooCAi+eI+g1uSUCn2AM4DsTQ==", - "license": "MIT", + "node_modules/@mongosh/service-provider-node-driver/node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "license": "Apache-2.0", "dependencies": { - "@microsoft/vscode-azureresources-api": "^2.3.1", - "@vscode/extension-telemetry": "^0.9.6", - "dayjs": "^1.11.2", - "escape-string-regexp": "^2.0.0", - "html-to-text": "^8.2.0", - "semver": "^7.3.7", - "uuid": "^9.0.0", - "vscode-tas-client": "^0.1.84", - "vscode-uri": "^3.0.6" + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" }, - "peerDependencies": { - "@azure/ms-rest-azure-env": "^2.0.0" + "engines": { + "node": ">=20.19.0" } }, - "node_modules/@microsoft/vscode-azext-utils/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "node_modules/@mongosh/shell-api": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@mongosh/shell-api/-/shell-api-5.1.4.tgz", + "integrity": "sha512-saQLOvYkqBHlbVZAcyOjHtioAeWDOkTvYzxPWjKsqFT+0n7asU9FLzzFsohl+fKCPdvcOmA6S2Bv9T8l5okLLg==", + "license": "Apache-2.0", + "dependencies": { + "@mongosh/arg-parser": "^5.0.2", + "@mongosh/errors": "2.4.6", + "@mongosh/i18n": "^2.20.2", + "@mongosh/service-provider-core": "5.0.3", + "@mongosh/shell-bson": "3.0.3", + "mongodb-redact": "^1.3.0", + "mongodb-schema": "^12.7.0" + }, + "engines": { + "node": ">=14.15.1" } }, - "node_modules/@microsoft/vscode-azureresources-api": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azureresources-api/-/vscode-azureresources-api-2.5.1.tgz", - "integrity": "sha512-CUlDVsau6RJA8F1IENnfA3N0XqyGQz/VJgh9QLnWjRkiBjW8/VSUpxNab6qflnN9SS38EHGv81v/92kEkoOQKQ==", - "license": "MIT", + "node_modules/@mongosh/shell-api/node_modules/@mongosh/arg-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@mongosh/arg-parser/-/arg-parser-5.0.2.tgz", + "integrity": "sha512-fOEGy6vOka2XX2V4EO4BKHomZPXDi4g6lqLb9ck+kLiOb1nxlUYLOhpTA5Vd5SPiJk48AVYjG5ICzcZsTdqovg==", + "license": "Apache-2.0", + "dependencies": { + "@mongosh/errors": "2.4.6", + "@mongosh/i18n": "^2.20.2", + "mongodb-connection-string-url": "^7.0.1", + "yargs-parser": "^20.2.4" + }, + "engines": { + "node": ">=14.15.1" + }, "peerDependencies": { - "@azure/ms-rest-azure-env": "^2.0.0" + "zod": "^3.25.76" } }, - "node_modules/@monaco-editor/loader": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", - "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "node_modules/@mongosh/shell-api/node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", "license": "MIT", "dependencies": { - "state-local": "^1.0.6" + "@types/webidl-conversions": "*" } }, - "node_modules/@monaco-editor/react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", - "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", - "license": "MIT", + "node_modules/@mongosh/shell-api/node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "license": "Apache-2.0", "dependencies": { - "@monaco-editor/loader": "^1.5.0" + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" }, - "peerDependencies": { - "monaco-editor": ">= 0.25.0 < 1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "engines": { + "node": ">=20.19.0" } }, - "node_modules/@mongodb-js/explain-plan-helper": { - "version": "1.4.24", - "resolved": "https://registry.npmjs.org/@mongodb-js/explain-plan-helper/-/explain-plan-helper-1.4.24.tgz", - "integrity": "sha512-JKX44aUFBAUlGkIw6Ad7Ov0WCidmrt0Z8Q5aGijLFTYGKaRN99lA5o1hDnUHC2f3MFasze5GNTLaP7TIwDQaZw==", - "license": "SSPL", - "dependencies": { - "@mongodb-js/shell-bson-parser": "^1.2.0", - "mongodb-explain-compat": "^3.3.23" + "node_modules/@mongosh/shell-api/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" } }, - "node_modules/@mongodb-js/explain-plan-helper/node_modules/@mongodb-js/shell-bson-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/shell-bson-parser/-/shell-bson-parser-1.4.0.tgz", - "integrity": "sha512-3HO90liE6pmEuUMi7SWR1HooVk23/jfx5iaBZHo250iYyF5uaqssepBGRF7J/14pmgTSwIGrrDd5rQtBYrY7wA==", + "node_modules/@mongosh/shell-api/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@mongosh/shell-bson": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@mongosh/shell-bson/-/shell-bson-3.0.3.tgz", + "integrity": "sha512-qDTMKIykd1GSsS7RoTs5tkHedkQnXbPCFFQd7xvRwtfeOryO5tHJYLPqcIDw1W+QQ0Y4neII30Y7o4qxW6w+Kw==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.14.1" + "@mongosh/errors": "^2.4.6" + }, + "engines": { + "node": ">=14.15.1" }, "peerDependencies": { - "bson": "^4.6.3 || ^5 || ^6" + "bson": "^7.2.0" } }, - "node_modules/@mongodb-js/explain-plan-helper/node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "node_modules/@mongosh/shell-evaluator": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@mongosh/shell-evaluator/-/shell-evaluator-5.1.4.tgz", + "integrity": "sha512-ozPbjHKeyd0QAyggdL6vZV1p0R+BNpay4jrJqf//hw6SQZ4VYqJODNnnz9HzdPVj9/gprQOYa9G5fQvJX/oRFw==", "license": "Apache-2.0", - "peer": true, + "dependencies": { + "@mongosh/async-rewriter2": "2.4.15", + "@mongosh/shell-api": "^5.1.4", + "mongodb-redact": "^1.3.0" + }, "engines": { - "node": ">=16.20.1" + "node": ">=14.15.1" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", - "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", - "license": "MIT", + "node_modules/@mongosh/types": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@mongosh/types/-/types-5.0.3.tgz", + "integrity": "sha512-6foBHuAFAxKc0TGgUzDmRojo4fft596soAzizikg8WI0lOMjJk5LA9eFc92MQuPyDg/Q8a7aLHa1lUcd2BSJug==", + "license": "Apache-2.0", "dependencies": { - "sparse-bitfield": "^3.0.3" + "@mongodb-js/devtools-connect": "^3.14.9" + }, + "engines": { + "node": ">=14.15.1" } }, "node_modules/@napi-rs/nice": { @@ -4446,27 +5747,231 @@ "@napi-rs/nice-win32-x64-msvc": "1.1.1" } }, - "node_modules/@napi-rs/nice-android-arm-eabi": { + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", - "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], "engines": { "node": ">= 10" } }, - "node_modules/@napi-rs/nice-android-arm64": { + "node_modules/@napi-rs/nice-openharmony-arm64": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", - "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", "cpu": [ "arm64" ], @@ -4474,16 +5979,16 @@ "license": "MIT", "optional": true, "os": [ - "android" + "openharmony" ], "engines": { "node": ">= 10" } }, - "node_modules/@napi-rs/nice-darwin-arm64": { + "node_modules/@napi-rs/nice-win32-arm64-msvc": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", - "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", "cpu": [ "arm64" ], @@ -4491,33 +5996,33 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">= 10" } }, - "node_modules/@napi-rs/nice-darwin-x64": { + "node_modules/@napi-rs/nice-win32-ia32-msvc": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", - "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">= 10" } }, - "node_modules/@napi-rs/nice-freebsd-x64": { + "node_modules/@napi-rs/nice-win32-x64-msvc": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", - "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", "cpu": [ "x64" ], @@ -4525,33 +6030,153 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { "node": ">= 10" } }, - "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", - "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nevware21/ts-async": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.5.tgz", + "integrity": "sha512-JPQZWPKQJjj7kAftdEZL0XDFfbMgXCGiUAZe0d7EhLC3QlXTlZdSckGqqRIQ2QNl0VTEZyZUvRBw6Ednw089Fw==", + "license": "MIT" + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-linux-arm64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", - "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -4559,18 +6184,64 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-linux-arm64-musl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", - "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ - "arm64" + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" ], "dev": true, "license": "MIT", @@ -4579,15 +6250,19 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-linux-ppc64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", - "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", @@ -4596,15 +6271,19 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-linux-riscv64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", - "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", @@ -4613,15 +6292,19 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-linux-s390x-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", - "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", @@ -4630,13 +6313,17 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-linux-x64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", - "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -4647,13 +6334,17 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-linux-x64-musl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", - "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -4664,13 +6355,17 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-openharmony-arm64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", - "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -4678,18 +6373,22 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-win32-arm64-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", - "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", @@ -4698,15 +6397,19 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@napi-rs/nice-win32-ia32-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", - "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", @@ -4715,1028 +6418,1207 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" } }, - "node_modules/@napi-rs/nice-win32-x64-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", - "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", - "cpu": [ - "x64" - ], + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" } }, - "node_modules/@nevware21/ts-async": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", - "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.11.6 < 2.x" + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@nevware21/ts-utils": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.5.tgz", - "integrity": "sha512-JPQZWPKQJjj7kAftdEZL0XDFfbMgXCGiUAZe0d7EhLC3QlXTlZdSckGqqRIQ2QNl0VTEZyZUvRBw6Ednw089Fw==", - "license": "MIT" - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 16" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://opencollective.com/pkgr" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.2.tgz", + "integrity": "sha512-IhIAD5n4XvGHuL9nAgWfsBR0TdxtjrUWETYKCBHxauYXEv+b+ctEbs9neEgPC7Ecgzv4bpZTBwesAoGDeFymzA==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "anser": "^2.1.1", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" }, "engines": { - "node": ">= 8" + "node": ">=18.12" + }, + "peerDependencies": { + "@types/webpack": "5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <6.0.0", + "webpack": "^5.0.0", + "webpack-dev-server": "^4.8.0 || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", "dev": true, "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, "engines": { - "node": ">= 8" + "node": ">=20.0.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" }, "engines": { - "node": ">= 8" + "node": ">=20.0.0" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" }, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "node": ">=20.0.0" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">= 10.0.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "dependencies": { + "node-sarif-builder": "^3.2.0" } }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@secretlint/types": "^10.2.2" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=20.0.0" } }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=20.0.0" } }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz", + "integrity": "sha512-J3jpy002TyBjd4N/p6s+s90eX42H2eRhK3SbsZuvTDv977/E8p2U3zikdiehyJja66do7FlxLomZLPlvl2/xaA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "domhandler": "^4.2.0", + "selderee": "^0.6.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://ko-fi.com/killymxi" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@selderee/plugin-htmlparser2/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">= 4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10.0.0" + "node": ">=14.16" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10.0.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "node_modules/@sinonjs/fake-timers": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@slickgrid-universal/binding": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@slickgrid-universal/binding/-/binding-9.13.0.tgz", + "integrity": "sha512-/3mWCmmekDsW8dXhcaodCgmasD9WMeWNtcD/NKWCYmYERx52EDUOVNxWf8tcpCFlN+eV8smAnTxWGPFZG7Oxaw==", + "license": "MIT" + }, + "node_modules/@slickgrid-universal/common": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@slickgrid-universal/common/-/common-9.13.0.tgz", + "integrity": "sha512-azi6T/xQXFvzehMP3My33jlY2OgjPXf0qjDzqToe/90ijI/JKZQV99o7LUjHpqQAKpPGHpukiPZTV5az3gzvNA==", + "license": "MIT", + "dependencies": { + "@excel-builder-vanilla/types": "^4.2.1", + "@formkit/tempo": "^1.0.0", + "@slickgrid-universal/binding": "9.13.0", + "@slickgrid-universal/event-pub-sub": "9.13.0", + "@slickgrid-universal/utils": "9.13.0", + "@types/sortablejs": "^1.15.9", + "@types/trusted-types": "^2.0.7", + "autocompleter": "^9.3.2", + "dequal": "^2.0.3", + "multiple-select-vanilla": "^4.4.1", + "sortablejs": "^1.15.6", + "un-flatten-tree": "^2.0.12", + "vanilla-calendar-pro": "^3.1.0" }, "engines": { - "node": ">=0.10" + "node": "^20.0.0 || >=22.0.0" + }, + "funding": { + "type": "ko_fi", + "url": "https://ko-fi.com/ghiscoding" } }, - "node_modules/@parcel/watcher/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, + "node_modules/@slickgrid-universal/custom-footer-component": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@slickgrid-universal/custom-footer-component/-/custom-footer-component-9.13.0.tgz", + "integrity": "sha512-8rmWNMNZ+tdyCw8AjgsWwyLZQYjAe22guwWW4F/rzOPHRZdCiUX9b1CeHzPDTZWGS/DSULQuVsxbWME4JDwrrQ==", "license": "MIT", - "optional": true + "dependencies": { + "@formkit/tempo": "^1.0.0", + "@slickgrid-universal/binding": "9.13.0", + "@slickgrid-universal/common": "9.13.0" + } }, - "node_modules/@peculiar/asn1-cms": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", - "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", - "dev": true, + "node_modules/@slickgrid-universal/empty-warning-component": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@slickgrid-universal/empty-warning-component/-/empty-warning-component-9.13.0.tgz", + "integrity": "sha512-11OQYqAFIQ8hKbj4i6Rh1WC9vFjaQqZ75fLSAJeRB+ow7TOa/M7PWP749Jk3A+V8hKhzIF0rNGZ5BZyMJQ5cQw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@slickgrid-universal/common": "9.13.0" } }, - "node_modules/@peculiar/asn1-csr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", - "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", - "dev": true, + "node_modules/@slickgrid-universal/event-pub-sub": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@slickgrid-universal/event-pub-sub/-/event-pub-sub-9.13.0.tgz", + "integrity": "sha512-PHacS+HbROsiDid4WwA08lC6b8U+fn4NgEPRriG99DAyHTpGuJRCaZyhGkmuhSbUpcZ8IB6g0igcVWWW0c89gw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@slickgrid-universal/utils": "9.13.0" } }, - "node_modules/@peculiar/asn1-ecc": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", - "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", - "dev": true, + "node_modules/@slickgrid-universal/pagination-component": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@slickgrid-universal/pagination-component/-/pagination-component-9.13.0.tgz", + "integrity": "sha512-xa4HdrYll1Kd8a6BGFiHcMVnxokf5NlKf86QlxPWzSIcFLLZFCSdCSE1pO9KorPw3mi83BSTh9ykQTCP52QXzw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@slickgrid-universal/binding": "9.13.0", + "@slickgrid-universal/common": "9.13.0" } }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", - "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", - "dev": true, + "node_modules/@slickgrid-universal/row-detail-view-plugin": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@slickgrid-universal/row-detail-view-plugin/-/row-detail-view-plugin-9.13.0.tgz", + "integrity": "sha512-8DvLouWwdqFl2o/4iObUOhpCSYfPHVWbK0lLRBYD7bkQHbWNj5bvI9aQr8QY7bo5OTZcED7D8yG5fSSXKnmx/Q==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@slickgrid-universal/common": "9.13.0", + "@slickgrid-universal/utils": "9.13.0" } }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", - "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", - "dev": true, - "license": "MIT", + "node_modules/@slickgrid-universal/utils": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@slickgrid-universal/utils/-/utils-9.13.0.tgz", + "integrity": "sha512-0zFVPRb4o9B9BDDl2lukz3w3z1SRDAFP9IbXb30pivERBYsg/eRPlo2zTiY2YlUFExAsdlRfmofYbf/b4ytNRQ==", + "license": "MIT" + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", - "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/config-resolver": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", + "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pfx": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", - "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/core": { + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", - "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "license": "Apache-2.0", "dependencies": { - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.27", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", + "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node_modules/@smithy/middleware-retry": { + "version": "4.4.44", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz", + "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/pkgr" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.2.tgz", - "integrity": "sha512-IhIAD5n4XvGHuL9nAgWfsBR0TdxtjrUWETYKCBHxauYXEv+b+ctEbs9neEgPC7Ecgzv4bpZTBwesAoGDeFymzA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-serde": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "license": "Apache-2.0", "dependencies": { - "anser": "^2.1.1", - "core-js-pure": "^3.23.3", - "error-stack-parser": "^2.0.6", - "html-entities": "^2.1.0", - "schema-utils": "^4.2.0", - "source-map": "^0.7.3" + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "@types/webpack": "5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <6.0.0", - "webpack": "^5.0.0", - "webpack-dev-server": "^4.8.0 || 5.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "1.x" - }, - "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { - "optional": true - }, - "webpack-plugin-serve": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@secretlint/config-creator": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", - "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-stack": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "license": "Apache-2.0", "dependencies": { - "@secretlint/types": "^10.2.2" + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@secretlint/config-loader": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", - "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/node-config-provider": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "license": "Apache-2.0", "dependencies": { - "@secretlint/profiler": "^10.2.2", - "@secretlint/resolver": "^10.2.2", - "@secretlint/types": "^10.2.2", - "ajv": "^8.17.1", - "debug": "^4.4.1", - "rc-config-loader": "^4.1.3" + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@secretlint/core": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", - "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/node-http-handler": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "license": "Apache-2.0", "dependencies": { - "@secretlint/profiler": "^10.2.2", - "@secretlint/types": "^10.2.2", - "debug": "^4.4.1", - "structured-source": "^4.0.0" + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@secretlint/formatter": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", - "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/property-provider": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "license": "Apache-2.0", "dependencies": { - "@secretlint/resolver": "^10.2.2", - "@secretlint/types": "^10.2.2", - "@textlint/linter-formatter": "^15.2.0", - "@textlint/module-interop": "^15.2.0", - "@textlint/types": "^15.2.0", - "chalk": "^5.4.1", - "debug": "^4.4.1", - "pluralize": "^8.0.0", - "strip-ansi": "^7.1.0", - "table": "^6.9.0", - "terminal-link": "^4.0.0" + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@secretlint/formatter/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/protocol-http": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@secretlint/node": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", - "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/querystring-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "license": "Apache-2.0", "dependencies": { - "@secretlint/config-loader": "^10.2.2", - "@secretlint/core": "^10.2.2", - "@secretlint/formatter": "^10.2.2", - "@secretlint/profiler": "^10.2.2", - "@secretlint/source-creator": "^10.2.2", - "@secretlint/types": "^10.2.2", - "debug": "^4.4.1", - "p-map": "^7.0.3" + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@secretlint/profiler": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", - "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", - "dev": true, - "license": "MIT" - }, - "node_modules/@secretlint/resolver": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", - "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@secretlint/secretlint-formatter-sarif": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", - "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/service-error-classification": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "license": "Apache-2.0", "dependencies": { - "node-sarif-builder": "^3.2.0" + "@smithy/types": "^4.13.1" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@secretlint/secretlint-rule-no-dotenv": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", - "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "license": "Apache-2.0", "dependencies": { - "@secretlint/types": "^10.2.2" + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@secretlint/secretlint-rule-preset-recommend": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", - "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/signature-v4": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@secretlint/source-creator": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", - "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/smithy-client": { + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", + "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", + "license": "Apache-2.0", "dependencies": { - "@secretlint/types": "^10.2.2", - "istextorbinary": "^9.5.0" + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@secretlint/types": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", - "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/types": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz", - "integrity": "sha512-J3jpy002TyBjd4N/p6s+s90eX42H2eRhK3SbsZuvTDv977/E8p2U3zikdiehyJja66do7FlxLomZLPlvl2/xaA==", - "license": "MIT", + "node_modules/@smithy/url-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "license": "Apache-2.0", "dependencies": { - "domhandler": "^4.2.0", - "selderee": "^0.6.0" + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://ko-fi.com/killymxi" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@selderee/plugin-htmlparser2/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "license": "BSD-2-Clause", + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", "dependencies": { - "domelementtype": "^2.2.0" + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "node": ">=18.0.0" } }, - "node_modules/@sinclair/typebox": { - "version": "0.34.41", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", - "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=14.16" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.43", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz", + "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==", + "license": "Apache-2.0", "dependencies": { - "type-detect": "4.0.8" + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", - "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.47", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz", + "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==", + "license": "Apache-2.0", "dependencies": { - "@sinonjs/commons": "^3.0.1" + "@smithy/config-resolver": "^4.4.13", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@slickgrid-universal/binding": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@slickgrid-universal/binding/-/binding-9.13.0.tgz", - "integrity": "sha512-/3mWCmmekDsW8dXhcaodCgmasD9WMeWNtcD/NKWCYmYERx52EDUOVNxWf8tcpCFlN+eV8smAnTxWGPFZG7Oxaw==", - "license": "MIT" + "node_modules/@smithy/util-endpoints": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@slickgrid-universal/common": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@slickgrid-universal/common/-/common-9.13.0.tgz", - "integrity": "sha512-azi6T/xQXFvzehMP3My33jlY2OgjPXf0qjDzqToe/90ijI/JKZQV99o7LUjHpqQAKpPGHpukiPZTV5az3gzvNA==", - "license": "MIT", + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", "dependencies": { - "@excel-builder-vanilla/types": "^4.2.1", - "@formkit/tempo": "^1.0.0", - "@slickgrid-universal/binding": "9.13.0", - "@slickgrid-universal/event-pub-sub": "9.13.0", - "@slickgrid-universal/utils": "9.13.0", - "@types/sortablejs": "^1.15.9", - "@types/trusted-types": "^2.0.7", - "autocompleter": "^9.3.2", - "dequal": "^2.0.3", - "multiple-select-vanilla": "^4.4.1", - "sortablejs": "^1.15.6", - "un-flatten-tree": "^2.0.12", - "vanilla-calendar-pro": "^3.1.0" + "tslib": "^2.6.2" }, "engines": { - "node": "^20.0.0 || >=22.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" }, - "funding": { - "type": "ko_fi", - "url": "https://ko-fi.com/ghiscoding" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@slickgrid-universal/custom-footer-component": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@slickgrid-universal/custom-footer-component/-/custom-footer-component-9.13.0.tgz", - "integrity": "sha512-8rmWNMNZ+tdyCw8AjgsWwyLZQYjAe22guwWW4F/rzOPHRZdCiUX9b1CeHzPDTZWGS/DSULQuVsxbWME4JDwrrQ==", - "license": "MIT", + "node_modules/@smithy/util-retry": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "license": "Apache-2.0", "dependencies": { - "@formkit/tempo": "^1.0.0", - "@slickgrid-universal/binding": "9.13.0", - "@slickgrid-universal/common": "9.13.0" + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@slickgrid-universal/empty-warning-component": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@slickgrid-universal/empty-warning-component/-/empty-warning-component-9.13.0.tgz", - "integrity": "sha512-11OQYqAFIQ8hKbj4i6Rh1WC9vFjaQqZ75fLSAJeRB+ow7TOa/M7PWP749Jk3A+V8hKhzIF0rNGZ5BZyMJQ5cQw==", - "license": "MIT", + "node_modules/@smithy/util-stream": { + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "license": "Apache-2.0", "dependencies": { - "@slickgrid-universal/common": "9.13.0" + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@slickgrid-universal/event-pub-sub": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@slickgrid-universal/event-pub-sub/-/event-pub-sub-9.13.0.tgz", - "integrity": "sha512-PHacS+HbROsiDid4WwA08lC6b8U+fn4NgEPRriG99DAyHTpGuJRCaZyhGkmuhSbUpcZ8IB6g0igcVWWW0c89gw==", - "license": "MIT", + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", "dependencies": { - "@slickgrid-universal/utils": "9.13.0" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@slickgrid-universal/pagination-component": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@slickgrid-universal/pagination-component/-/pagination-component-9.13.0.tgz", - "integrity": "sha512-xa4HdrYll1Kd8a6BGFiHcMVnxokf5NlKf86QlxPWzSIcFLLZFCSdCSE1pO9KorPw3mi83BSTh9ykQTCP52QXzw==", - "license": "MIT", + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", "dependencies": { - "@slickgrid-universal/binding": "9.13.0", - "@slickgrid-universal/common": "9.13.0" + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@slickgrid-universal/row-detail-view-plugin": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@slickgrid-universal/row-detail-view-plugin/-/row-detail-view-plugin-9.13.0.tgz", - "integrity": "sha512-8DvLouWwdqFl2o/4iObUOhpCSYfPHVWbK0lLRBYD7bkQHbWNj5bvI9aQr8QY7bo5OTZcED7D8yG5fSSXKnmx/Q==", - "license": "MIT", + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", "dependencies": { - "@slickgrid-universal/common": "9.13.0", - "@slickgrid-universal/utils": "9.13.0" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@slickgrid-universal/utils": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@slickgrid-universal/utils/-/utils-9.13.0.tgz", - "integrity": "sha512-0zFVPRb4o9B9BDDl2lukz3w3z1SRDAFP9IbXb30pivERBYsg/eRPlo2zTiY2YlUFExAsdlRfmofYbf/b4ytNRQ==", - "license": "MIT" - }, "node_modules/@swc/cli": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.8.0.tgz", @@ -6148,6 +8030,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@trpc/client": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.10.0.tgz", @@ -7277,6 +9165,14 @@ "win32" ] }, + "node_modules/@vscode-documentdb/documentdb-constants": { + "resolved": "packages/documentdb-constants", + "link": true + }, + "node_modules/@vscode-documentdb/schema-analyzer": { + "resolved": "packages/schema-analyzer", + "link": true + }, "node_modules/@vscode/extension-telemetry": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.9.tgz", @@ -8077,10 +9973,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -8199,7 +10094,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -8211,12 +10106,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/antlr4ts": { - "version": "0.5.0-alpha.4", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", - "license": "BSD-3-Clause" - }, "node_modules/antlr4ts-cli": { "version": "0.5.0-alpha.4", "resolved": "https://registry.npmjs.org/antlr4ts-cli/-/antlr4ts-cli-0.5.0-alpha.4.tgz", @@ -8293,7 +10182,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -8438,7 +10326,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -8456,6 +10343,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/asn1js": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", @@ -8471,6 +10367,18 @@ "node": ">=12.0.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -8492,7 +10400,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8515,7 +10422,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -8707,7 +10613,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -8728,12 +10634,20 @@ "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -8741,6 +10655,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -8751,6 +10674,15 @@ "node": "*" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bin-version": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", @@ -8815,11 +10747,20 @@ "url": "https://bevry.me/fund" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8832,7 +10773,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8955,6 +10895,12 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -8988,7 +10934,6 @@ "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -9042,9 +10987,9 @@ } }, "node_modules/bson": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", - "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", "license": "Apache-2.0", "engines": { "node": ">=20.19.0" @@ -9054,7 +10999,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -9098,6 +11043,15 @@ "dev": true, "license": "MIT" }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -9117,7 +11071,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9200,7 +11153,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -9219,7 +11171,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9233,7 +11184,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -9270,7 +11220,6 @@ "version": "1.0.30001769", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -9453,7 +11402,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, "license": "ISC", "optional": true }, @@ -9519,11 +11467,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "optional": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -9538,7 +11498,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -9548,7 +11508,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9561,7 +11521,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -9622,7 +11582,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -9635,7 +11595,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/colorette": { @@ -9645,6 +11605,16 @@ "dev": true, "license": "MIT" }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -9761,7 +11731,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9771,14 +11740,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9857,6 +11824,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -9999,11 +11980,19 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -10021,7 +12010,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -10039,7 +12027,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10113,7 +12100,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -10129,7 +12116,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -10157,7 +12144,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -10245,7 +12231,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -10275,7 +12260,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -10289,6 +12273,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -10312,7 +12310,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -10342,7 +12339,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "optional": true, "engines": { @@ -10483,7 +12479,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -10531,14 +12526,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", - "dev": true, "license": "ISC" }, "node_modules/embla-carousel": { @@ -10582,7 +12575,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/emojis-list": { @@ -10599,7 +12592,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -10623,7 +12615,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -10707,7 +12698,6 @@ "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", - "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -10776,7 +12766,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10786,7 +12775,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10832,7 +12820,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -10845,7 +12832,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10874,7 +12860,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7", @@ -10902,7 +12887,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10912,7 +12896,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -10924,6 +12907,37 @@ "node": ">=8" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { "version": "9.39.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", @@ -11480,7 +13494,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -11520,7 +13533,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -11540,7 +13552,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -11550,7 +13561,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11614,7 +13624,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, "license": "(MIT OR WTFPL)", "optional": true, "engines": { @@ -11797,6 +13806,41 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -11850,6 +13894,29 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -11888,6 +13955,13 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT", + "optional": true + }, "node_modules/filename-reserved-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", @@ -12055,7 +14129,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -12124,11 +14197,22 @@ "node": ">= 14.17" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -12148,7 +14232,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, "license": "MIT", "optional": true }, @@ -12186,7 +14269,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12196,7 +14278,6 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -12217,17 +14298,43 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12237,7 +14344,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -12247,7 +14353,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -12270,7 +14376,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -12305,7 +14410,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -12342,7 +14446,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -12356,11 +14459,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, "license": "MIT", "optional": true }, @@ -12481,7 +14606,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -12538,11 +14662,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12627,7 +14759,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12650,7 +14781,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -12663,7 +14793,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" @@ -12679,7 +14808,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12692,7 +14820,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -12708,7 +14835,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -12766,6 +14892,16 @@ "he": "bin/he" } }, + "node_modules/heap-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz", + "integrity": "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -13155,7 +15291,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -13260,14 +15396,12 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, "license": "ISC", "optional": true }, @@ -13291,7 +15425,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13312,6 +15445,25 @@ "node": ">=10.13.0" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -13322,6 +15474,13 @@ "node": ">= 10" } }, + "node_modules/ipv6-normalize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz", + "integrity": "sha512-Bm6H79i01DjgGTCWjUuCjJ6QDo1HB96PT/xCYuyJUP9WFbVDrLSbG4EZCvOCun2rNswZb0c3e4Jt/ws795esHA==", + "license": "MIT", + "optional": true + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -13350,7 +15509,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -13375,7 +15533,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, "license": "MIT", "dependencies": { "async-function": "^1.0.0", @@ -13395,7 +15552,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" @@ -13424,7 +15580,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -13448,7 +15603,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13477,7 +15631,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13495,7 +15648,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13547,7 +15699,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -13563,7 +15714,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -13583,7 +15734,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.4", @@ -13657,7 +15807,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13670,7 +15819,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13706,7 +15854,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -13752,11 +15899,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13775,7 +15927,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13788,7 +15939,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -13817,7 +15967,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -13834,7 +15983,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13852,7 +16000,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -13881,7 +16028,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13894,7 +16040,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -13910,7 +16055,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -13952,6 +16096,13 @@ "dev": true, "license": "ISC" }, + "node_modules/isnumber": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isnumber/-/isnumber-1.0.0.tgz", + "integrity": "sha512-JLiSz/zsZcGFXPrB4I/AGBvtStkt+8QmksyZBZnVXnnK9XdTEyz0tX8CRYljtwYDuIuZzih6DpHQdi+3Q6zHPw==", + "license": "MIT", + "optional": true + }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -15017,11 +17168,19 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -15038,11 +17197,16 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -15051,6 +17215,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -15083,7 +17256,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -15183,6 +17355,31 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kerberos": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-7.0.0.tgz", + "integrity": "sha512-Q8yUNeCM5fSXkURaa05WugXFsH6c57hDHDmsupMFCPaQEPym9FGwFp/2XSTcMuLldtEeBOsQ/9VGQ55lfHTT3Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "node-addon-api": "^8.5.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/kerberos/node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/keyborg": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.6.0.tgz", @@ -15403,7 +17600,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/lodash.once": { @@ -15485,6 +17681,31 @@ "node": ">=10" } }, + "node_modules/macos-export-certificate-and-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/macos-export-certificate-and-key/-/macos-export-certificate-and-key-2.0.1.tgz", + "integrity": "sha512-2Y2lbgJ1s4iglK7WCRApKhn+52x5xm6wgT+WCHn3bznLeVUACl14aHG5f3zb1o4tUzdLQ7scad5T6YxpvM92Tw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^8.5.0" + } + }, + "node_modules/macos-export-certificate-and-key/node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -15547,7 +17768,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -16270,7 +18490,6 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -16420,7 +18639,6 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, "license": "MIT", "optional": true }, @@ -16618,13 +18836,13 @@ } }, "node_modules/mongodb": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", - "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.1.tgz", + "integrity": "sha512-067DXiMjcpYQl6bGjWQoTUEE9UoRViTtKFcoqX7z08I+iDZv/emH1g8XEFiO3qiDfXAheT5ozl1VffDTKhIW/w==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.3.0", - "bson": "^7.0.0", + "bson": "^7.1.1", "mongodb-connection-string-url": "^7.0.0" }, "engines": { @@ -16663,6 +18881,41 @@ } } }, + "node_modules/mongodb-build-info": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/mongodb-build-info/-/mongodb-build-info-1.9.6.tgz", + "integrity": "sha512-i0ty4tJO2RaLkq9Z+wTE6mVs45R/fnmq33rngO8QCZ+jMmXfAeDTvTxteRPmEg0lXrf/AZP+oR4y59Cl4nyRnQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "mongodb-connection-string-url": "^3.0.1 || ^7.0.0" + } + }, + "node_modules/mongodb-client-encryption": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-7.0.0.tgz", + "integrity": "sha512-0egSmyCQ31MLdDFH2j5fHnX8OkAWytUC4ZoPuelU0E+lgPQ2/UcpxkYQXF20SW0rCzADIc0qouiULtqAKDs/uQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "node-addon-api": "^8.5.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-client-encryption/node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/mongodb-connection-string-url": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", @@ -16679,6 +18932,79 @@ "integrity": "sha512-3OygQjzjHr0hsT3y0f91yP7Ylp+2bbEM3IPil2yv+9avmGhA9Ru+CcgTXI+IfkfNlbcly/w7alvHDk+dlX28QQ==", "license": "SSPL" }, + "node_modules/mongodb-log-writer": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/mongodb-log-writer/-/mongodb-log-writer-2.5.6.tgz", + "integrity": "sha512-shIcoNlfGVGu4L6LjyOGEKdCZMZk6Isg9veOmjbmlfawJ4cvZqdeCIy2TKPuqIGCE4HrPB+847s9PaT3yJZVlA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "heap-js": "^2.3.0" + }, + "peerDependencies": { + "bson": "^4.6.3 || ^5 || ^6.10.3 || ^7.0.0" + } + }, + "node_modules/mongodb-ns": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/mongodb-ns/-/mongodb-ns-3.1.6.tgz", + "integrity": "sha512-NJs8wuqM9vAVc/9Oy/PMx31Lq87Dnud+qC5c6xXaSKOloNScM5NyxFjsl6XMt2wlQR5Z/be1nm4hfjDBDV7VEA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/mongodb-redact": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-1.4.6.tgz", + "integrity": "sha512-MF1t9Kx90vIn96Uj9S9EkOuReRgicdVKFds4Y1q6UExMexlTRYPedCahttW4HCg38V0INczoYHVxaSVFgRwhBQ==", + "license": "Apache-2.0", + "dependencies": { + "mongodb-connection-string-url": "^3.0.1 || ^7.0.0", + "regexp.escape": "^2.0.1" + } + }, + "node_modules/mongodb-schema": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/mongodb-schema/-/mongodb-schema-12.7.0.tgz", + "integrity": "sha512-TPfop6c2AAQJP1QhQwi/uJnuPEeLhMvN9ouJAZ6M9mouH6fCr0WOHLJWx5Z0dSeXJIVy//WTXxZB6yiol1bGEA==", + "license": "Apache-2.0", + "dependencies": { + "reservoir": "^0.1.2" + }, + "bin": { + "mongodb-schema": "bin/mongodb-schema" + }, + "optionalDependencies": { + "bson": "^6.7.0 || ^7.1.1", + "cli-table": "^0.3.4", + "js-yaml": "^4.0.0", + "mongodb": "^6.6.1 || ^7.0.0", + "mongodb-ns": "^3.0.1", + "numeral": "^2.0.6", + "progress": "^2.0.3", + "stats-lite": "^2.0.0", + "yargs": "^17.6.2" + } + }, + "node_modules/mongodb-schema/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0", + "optional": true + }, + "node_modules/mongodb-schema/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "optional": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/mongodb/node_modules/@types/whatwg-url": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", @@ -16757,6 +19083,13 @@ "dev": true, "license": "ISC" }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -16780,7 +19113,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, "license": "MIT", "optional": true }, @@ -16852,11 +19184,19 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-abi": { "version": "3.85.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -16874,6 +19214,26 @@ "license": "MIT", "optional": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -16903,6 +19263,24 @@ "semver": "bin/semver.js" } }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-html-markdown": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/node-html-markdown/-/node-html-markdown-1.3.0.tgz", @@ -16938,7 +19316,6 @@ "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, "license": "MIT" }, "node_modules/node-sarif-builder": { @@ -17039,6 +19416,25 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/oauth4webapi": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz", + "integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -17053,7 +19449,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17066,7 +19461,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17076,7 +19470,6 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -17173,7 +19566,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -17196,9 +19588,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", - "optional": true, "dependencies": { "wrappy": "1" } @@ -17247,6 +19637,19 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/openid-client": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.2.tgz", + "integrity": "sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==", + "license": "MIT", + "dependencies": { + "jose": "^6.1.3", + "oauth4webapi": "^3.8.4" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -17370,11 +19773,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/os-dns-native": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/os-dns-native/-/os-dns-native-2.0.1.tgz", + "integrity": "sha512-fessk9+Z0dcoVSMnLi1zK9xztEHi3x57A3gkX8KzU6k216uiii28IFbCh/Sf/+sXUX8kDCkUohDclQhvXDLJcA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "debug": "^4.3.3", + "ipv6-normalize": "^1.0.1", + "node-addon-api": "^8.5.0" + } + }, + "node_modules/os-dns-native/node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", @@ -17471,6 +19897,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -17632,7 +20090,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -17648,6 +20105,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -17723,7 +20195,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -17860,7 +20331,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17983,7 +20453,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -18098,6 +20567,16 @@ "dev": true, "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -18124,7 +20603,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -18138,7 +20616,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -18178,7 +20655,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -18246,7 +20722,6 @@ "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -18325,7 +20800,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -18395,7 +20869,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "optional": true, "dependencies": { @@ -18445,7 +20918,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -18670,7 +21142,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -18695,11 +21166,30 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/regexp.escape": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexp.escape/-/regexp.escape-2.0.1.tgz", + "integrity": "sha512-JItRb4rmyTzmERBkAf6J87LjDPy/RscIwmaJQ3gsFlAzrmZbZU8LwBw5IydFZXW9hqpgbPlGbMhtpqtuAhMgtg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "for-each": "^0.3.3", + "safe-regex-test": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -18753,7 +21243,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18786,6 +21276,12 @@ "dev": true, "license": "MIT" }, + "node_modules/reservoir": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reservoir/-/reservoir-0.1.2.tgz", + "integrity": "sha512-ysyw95gLBhMAzqIVrOHJ2yMrRQHAS+h97bS9r89Z7Ou10Jhl2k5KOsyjPqrxL+WfEanov0o5bAMVzQ7AKyENHA==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -18847,6 +21343,19 @@ "node": ">=4" } }, + "node_modules/resolve-mongodb-srv": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve-mongodb-srv/-/resolve-mongodb-srv-1.1.7.tgz", + "integrity": "sha512-JWwAqlR5wgIxqhO2aY+ERIXCGHybGvmYuKwYNzUkLT9iy1t1AZ85tCjzlDoqbymHDD1kgfL9qooPCiTXD+LyQg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "whatwg-url": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" + }, + "bin": { + "resolve-mongodb-srv": "bin/resolve-mongodb-srv.js" + } + }, "node_modules/responselike": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", @@ -18959,6 +21468,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rtl-css-js": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", @@ -19019,7 +21554,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -19039,7 +21573,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/safe-buffer": { @@ -19066,7 +21599,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -19083,14 +21615,12 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -19108,7 +21638,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -19532,7 +22061,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -19550,7 +22078,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -19566,7 +22093,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -19588,7 +22114,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, "license": "ISC" }, "node_modules/shallow-clone": { @@ -19644,7 +22169,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -19664,7 +22188,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -19681,7 +22204,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -19700,7 +22222,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -19727,7 +22248,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, "funding": [ { "type": "github", @@ -19749,7 +22269,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, "funding": [ { "type": "github", @@ -19835,6 +22354,16 @@ "react": ">=19.0.0" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -19847,6 +22376,43 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks/node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -20029,6 +22595,23 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -20055,6 +22638,19 @@ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", "license": "MIT" }, + "node_modules/stats-lite": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stats-lite/-/stats-lite-2.2.0.tgz", + "integrity": "sha512-/Kz55rgUIv2KP2MKphwYT/NCuSfAlbbMRv2ZWw7wyXayu230zdtzhxxuXXcvsc6EmmhS8bSJl3uS1wmMHFumbA==", + "license": "MIT", + "optional": true, + "dependencies": { + "isnumber": "~1.0.0" + }, + "engines": { + "node": ">=2.0.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -20082,7 +22678,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -20108,7 +22703,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -20118,7 +22713,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/string-length": { @@ -20162,7 +22757,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -20216,7 +22811,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -20226,7 +22821,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -20293,7 +22888,6 @@ "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -20315,7 +22909,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -20334,7 +22927,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -20446,6 +23038,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/strtok3": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", @@ -20600,6 +23204,16 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/system-ca": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/system-ca/-/system-ca-3.0.0.tgz", + "integrity": "sha512-QlQvvCHxjUuhW8mAbOsDh6GTWvdJPFKpu4xTLsAYDDCjwZXCGiOC0zhhzS9QXBJOpO1CcKOU/CuVCDlJA+pIIQ==", + "license": "Apache-2.0", + "optionalDependencies": { + "macos-export-certificate-and-key": "^2.0.1", + "win-export-certificate-and-key": "^3.0.2" + } + }, "node_modules/table": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", @@ -20671,7 +23285,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -20685,7 +23298,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -20701,7 +23313,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -21039,7 +23650,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -21332,7 +23942,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, "license": "Apache-2.0", "optional": true, "dependencies": { @@ -21342,6 +23951,12 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -21396,7 +24011,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -21411,7 +24025,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -21431,7 +24044,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -21453,7 +24065,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -21563,7 +24174,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -21739,7 +24349,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -21784,7 +24393,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -21841,7 +24449,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -21910,7 +24518,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -22091,6 +24698,15 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", @@ -22541,7 +25157,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", @@ -22561,7 +25176,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -22589,14 +25203,12 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/which-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -22615,7 +25227,6 @@ "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -22640,6 +25251,31 @@ "dev": true, "license": "MIT" }, + "node_modules/win-export-certificate-and-key": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/win-export-certificate-and-key/-/win-export-certificate-and-key-3.0.2.tgz", + "integrity": "sha512-whmC3h6M0UX3Ny31CqvUhutf0+atst2781xVrA7PFEEz3WF2loVuwZnrjDyrcQ+58bXenwdKwwW6Yfxhh7ZPYg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^8.5.0" + } + }, + "node_modules/win-export-certificate-and-key/node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -22766,9 +25402,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "5.0.1", @@ -22869,7 +25503,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=10" @@ -22886,7 +25520,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -22905,7 +25539,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -23025,6 +25659,22 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "packages/documentdb-constants": { + "name": "@vscode-documentdb/documentdb-constants", + "version": "1.0.0", + "license": "MIT" + }, + "packages/schema-analyzer": { + "name": "@vscode-documentdb/schema-analyzer", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "denque": "~2.1.0" + }, + "peerDependencies": { + "mongodb": ">=6.0.0" + } } } } diff --git a/package.json b/package.json index a3ad84c37..fcbda2d53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb", - "version": "0.7.2", + "version": "0.8.0-beta", "releaseNotesUrl": "https://github.com/microsoft/vscode-documentdb/discussions/489", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "publisher": "ms-azuretools", @@ -46,6 +46,9 @@ "type": "git", "url": "https://github.com/microsoft/vscode-documentdb" }, + "workspaces": [ + "packages/*" + ], "main": "./main", "l10n": "./l10n", "activationEvents": [ @@ -55,8 +58,9 @@ "onUri" ], "scripts": { + "prebuild": "npm run build --workspaces --if-present", "build": "tsc", - "clean": "git clean -dfx", + "clean": "rimraf out dist coverage && npm run clean --workspaces --if-present", "compile": "tsc -watch", "package": "run-script-os", "package:win32": "npm run webpack-prod && cd dist && npm pkg delete \"scripts.vscode:prepublish\" && npx vsce package --no-dependencies --out ../%npm_package_name%-%npm_package_version%.vsix", @@ -66,12 +70,12 @@ "package-prerelease:default": "npm run webpack-prod && cd dist && npm pkg delete \"scripts.vscode:prepublish\" && npx vsce package --pre-release --no-dependencies --out ../${npm_package_name}-${npm_package_version}-pre-release.vsix", "lint": "eslint --quiet .", "lint-fix": "eslint . --fix", - "prettier": "prettier -c \"(src|test|l10n|grammar|docs)/**/*.@(js|ts|jsx|tsx|json)\" \"./*.@(js|ts|jsx|tsx|json)\"", - "prettier-fix": "prettier -w \"(src|test|l10n|grammar|docs)/**/*.@(js|ts|jsx|tsx|json)\" \"./*.@(js|ts|jsx|tsx|json)\"", + "prettier": "prettier -c \"(src|test|l10n|grammar|docs|packages)/**/*.@(js|ts|jsx|tsx|json)\" \"./*.@(js|ts|jsx|tsx|json)\"", + "prettier-fix": "prettier -w \"(src|test|l10n|grammar|docs|packages)/**/*.@(js|ts|jsx|tsx|json)\" \"./*.@(js|ts|jsx|tsx|json)\"", "pretest": "npm run build", "test": "vscode-test", + "prejesttest": "npm run build --workspaces --if-present", "jesttest": "jest", - "update-grammar": "antlr4ts -visitor ./grammar/mongo.g4 -o src/documentdb/grammar", "webpack-dev": "rimraf ./dist && npm run webpack-dev-ext && npm run webpack-dev-wv", "webpack-prod": "rimraf ./dist && npm run webpack-prod-ext && npm run webpack-prod-wv", "webpack-dev-ext": "webpack --mode development --config ./webpack.config.ext.js", @@ -165,15 +169,24 @@ "@microsoft/vscode-azureresources-api": "~2.5.0", "@monaco-editor/react": "~4.7.0", "@mongodb-js/explain-plan-helper": "1.4.24", + "@mongodb-js/shell-bson-parser": "^1.5.6", + "@mongosh/errors": "^2.4.6", + "@mongosh/service-provider-core": "^5.0.3", + "@mongosh/service-provider-node-driver": "^5.0.6", + "@mongosh/shell-api": "^5.1.4", + "@mongosh/shell-evaluator": "^5.1.4", "@trpc/client": "~11.10.0", "@trpc/server": "~11.10.0", + "@vscode-documentdb/documentdb-constants": "*", + "@vscode-documentdb/schema-analyzer": "*", "@vscode/l10n": "~0.0.18", - "antlr4ts": "^0.5.0-alpha.4", - "bson": "~7.0.0", + "acorn": "^8.16.0", + "acorn-walk": "^8.3.5", + "bson": "^7.2.0", "denque": "~2.1.0", "es-toolkit": "~1.45.1", "monaco-editor": "~0.52.2", - "mongodb": "~7.0.0", + "mongodb": "^7.1.0", "mongodb-connection-string-url": "~3.0.2", "react-hotkeys-hook": "~5.2.1", "react-markdown": "^10.1.0", @@ -237,25 +250,22 @@ ], "languages": [ { - "id": "vscode-documentdb-scrapbook-language", - "aliases": [ - "DocumentDB Scrapbook" - ], + "id": "documentdb-scratchpad", "extensions": [ - ".vscode-documentdb-scrapbook" + ".documentdb", + ".documentdb.js" + ], + "aliases": [ + "DocumentDB Scratchpad" ], - "configuration": "./grammar/configuration.json" + "configuration": "./scratchpad-language-configuration.json" } ], "grammars": [ { - "language": "vscode-documentdb-scrapbook-language", - "scopeName": "source.mongo.js", - "path": "./grammar/JavaScript.tmLanguage.json" - }, - { - "scopeName": "source.mongo.js.regexp", - "path": "./grammar/Regular Expressions (JavaScript).tmLanguage" + "language": "documentdb-scratchpad", + "scopeName": "source.documentdb-scratchpad", + "path": "./syntaxes/documentdb-scratchpad.tmGrammar.json" } ], "commands": [ @@ -425,31 +435,6 @@ "command": "vscode-documentdb.command.createDatabase", "title": "Create Databaseโ€ฆ" }, - { - "//": "Scrapbook: Connect Database", - "category": "DocumentDB", - "command": "vscode-documentdb.command.scrapbook.connect", - "title": "Connect to this database" - }, - { - "//": "Scrapbook: Execute All Commands", - "category": "DocumentDB", - "command": "vscode-documentdb.command.scrapbook.executeAllCommands", - "title": "Execute All Commands" - }, - { - "//": "Scrapbook: Execute Command", - "category": "DocumentDB", - "command": "vscode-documentdb.command.scrapbook.executeCommand", - "title": "Execute Command" - }, - { - "//": "Scrapbook: New Scrapbook", - "category": "DocumentDB", - "command": "vscode-documentdb.command.scrapbook.new", - "title": "New DocumentDB Scrapbook", - "icon": "$(new-file)" - }, { "//": "Delete Collection", "category": "DocumentDB", @@ -533,43 +518,54 @@ "category": "DocumentDB", "command": "vscode-documentdb.command.pasteCollection", "title": "Paste Collectionโ€ฆ" + }, + { + "//": "[Scratchpad] New DocumentDB Scratchpad", + "category": "DocumentDB", + "command": "vscode-documentdb.command.scratchpad.new", + "title": "New Scratchpad", + "icon": "$(new-file)" + }, + { + "//": "[Scratchpad] Connect Scratchpad to Database", + "category": "DocumentDB", + "command": "vscode-documentdb.command.scratchpad.connect", + "title": "Connect Scratchpad to this database" + }, + { + "//": "[Scratchpad] Run All", + "category": "DocumentDB", + "command": "vscode-documentdb.command.scratchpad.runAll", + "title": "Scratchpad: Run All", + "icon": "$(run-all)" + }, + { + "//": "[Scratchpad] Run Selected / Current Block", + "category": "DocumentDB", + "command": "vscode-documentdb.command.scratchpad.runSelected", + "title": "Scratchpad: Run Selected", + "icon": "$(play)" + }, + { + "//": "[Diagnostics] Clear Schema Cache", + "category": "DocumentDB", + "command": "vscode-documentdb.command.clearSchemaCache", + "title": "Clear Schema Cache" + }, + { + "//": "[Diagnostics] Show Schema Store Stats", + "category": "DocumentDB", + "command": "vscode-documentdb.command.showSchemaStoreStats", + "title": "Show Schema Store Stats" } ], "submenus": [ { - "id": "documentDB.submenus.mongo.database.scrapbook", - "label": "DocumentDB Scrapbook" - }, - { - "id": "documentDB.submenus.mongo.collection.scrapbook", - "label": "DocumentDB Scrapbook" + "id": "documentDB.submenus.scratchpad", + "label": "DocumentDB Scratchpad" } ], "menus": { - "documentDB.submenus.mongo.database.scrapbook": [ - { - "//": "[Database] Scrapbook: New Scrapbook", - "command": "vscode-documentdb.command.scrapbook.new", - "group": "1@1" - }, - { - "//": "[Database] Scrapbook: Connect", - "command": "vscode-documentdb.command.scrapbook.connect", - "group": "1@2" - } - ], - "documentDB.submenus.mongo.collection.scrapbook": [ - { - "//": "[Collection] Mongo DB|Cluster Scrapbook New", - "command": "vscode-documentdb.command.scrapbook.new", - "group": "1@1" - }, - { - "//": "[Collection] Scrapbook / Connect", - "command": "vscode-documentdb.command.scrapbook.connect", - "group": "1@2" - } - ], "view/title": [ { "command": "vscode-documentdb.command.connectionsView.refresh", @@ -597,16 +593,7 @@ "group": "navigation@5" } ], - "editor/context": [ - { - "command": "vscode-documentdb.command.scrapbook.executeAllCommands", - "when": "resourceLangId==mongo" - }, - { - "command": "vscode-documentdb.command.scrapbook.executeCommand", - "when": "resourceLangId==mongo" - } - ], + "editor/context": [], "editor/title": [], "view/item/context": [ { @@ -763,12 +750,6 @@ "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_database\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", "group": "3@1" }, - { - "//": "[Database] Mongo DB|Cluster Scrapbook Submenu", - "submenu": "documentDB.submenus.mongo.database.scrapbook", - "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_database\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", - "group": "3@2" - }, { "//": "[Collection] Mongo DB|Cluster Open collection", "command": "vscode-documentdb.command.containerView.open", @@ -830,9 +811,9 @@ "group": "5@1" }, { - "//": "[Collection] Mongo DB|Cluster Scrapbook Submenu", - "submenu": "documentDB.submenus.mongo.collection.scrapbook", - "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_collection\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", + "//": "[Database/Collection] Scratchpad submenu", + "submenu": "documentDB.submenus.scratchpad", + "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_(database|collection)\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", "group": "5@2" }, { @@ -866,6 +847,16 @@ "group": "3@4" } ], + "documentDB.submenus.scratchpad": [ + { + "command": "vscode-documentdb.command.scratchpad.new", + "group": "1@1" + }, + { + "command": "vscode-documentdb.command.scratchpad.connect", + "group": "1@2" + } + ], "explorer/context": [], "commandPalette": [ { @@ -985,35 +976,31 @@ "when": "never" }, { - "command": "vscode-documentdb.command.scrapbook.new", - "when": "never" - }, - { - "command": "vscode-documentdb.command.scrapbook.connect", + "command": "vscode-documentdb.command.scratchpad.connect", "when": "never" }, { - "command": "vscode-documentdb.command.scrapbook.executeAllCommands", + "command": "vscode-documentdb.command.scratchpad.runAll", "when": "never" }, { - "command": "vscode-documentdb.command.scrapbook.executeCommand", + "command": "vscode-documentdb.command.scratchpad.runSelected", "when": "never" } ] }, "keybindings": [ { - "command": "vscode-documentdb.command.scrapbook.executeCommand", - "key": "ctrl+shift+'", - "mac": "cmd+shift+'", - "when": "editorLangId == 'vscode-documentdb-scrapbook-language' && editorTextFocus" + "command": "vscode-documentdb.command.scratchpad.runAll", + "key": "ctrl+shift+enter", + "mac": "cmd+shift+enter", + "when": "editorLangId == 'documentdb-scratchpad' && editorTextFocus" }, { - "command": "vscode-documentdb.command.scrapbook.executeAllCommands", - "key": "ctrl+shift+;", - "mac": "cmd+shift+;", - "when": "editorLangId == 'vscode-documentdb-scrapbook-language' && editorTextFocus" + "command": "vscode-documentdb.command.scratchpad.runSelected", + "key": "ctrl+enter", + "mac": "cmd+enter", + "when": "editorLangId == 'documentdb-scratchpad' && editorTextFocus" } ], "configuration": [ @@ -1077,7 +1064,7 @@ "string", "null" ], - "description": "Full path to folder and executable to start the Mongo shell, needed by some DocumentDB Scrapbook commands. The default is to search in the system path for 'mongosh'.", + "description": "Full path to folder and executable to start the Mongo shell. The default is to search in the system path for 'mongosh'.", "default": null }, "documentDB.mongoShell.args": { diff --git a/packages/documentdb-constants/README.md b/packages/documentdb-constants/README.md new file mode 100644 index 000000000..4835cdcc2 --- /dev/null +++ b/packages/documentdb-constants/README.md @@ -0,0 +1,85 @@ +# @vscode-documentdb/documentdb-constants + +Static operator metadata for all DocumentDB-supported operators, aggregation stages, accumulators, update operators, BSON type constructors, and system variables. + +> **Monorepo package** โ€” this package is part of the `vscode-documentdb` workspace. +> Dev dependencies (Jest, ts-jest, Prettier, ts-node, etc.) are provided by the +> root `package.json`. Always install from the repository root: +> +> ```bash +> cd +> npm install +> ``` + +## Purpose + +This package is the **single source of truth** for operator metadata when the connected database is DocumentDB. It provides: + +- `OperatorEntry` objects with value, description, snippet, documentation link, and type metadata +- Meta-tag based filtering (`getFilteredCompletions()`) for context-aware autocompletion +- Convenience presets for common completion contexts (filter bar, aggregation pipeline, etc.) +- Documentation URL generation (`getDocLink()`) + +## Data Source + +All operator data is derived from the official DocumentDB documentation: + +- **Compatibility reference:** [DocumentDB Query Language Compatibility](https://learn.microsoft.com/en-us/azure/documentdb/compatibility-query-language) โ€” lists every operator with its support status across DocumentDB versions 5.0โ€“8.0. +- **Per-operator docs:** [DocumentDB Operators](https://learn.microsoft.com/en-us/azure/documentdb/operators/) โ€” individual pages with descriptions and syntax for each operator. +- **Source repository:** [MicrosoftDocs/azure-databases-docs](https://github.com/MicrosoftDocs/azure-databases-docs) โ€” the GitHub repo containing the raw Markdown source for all documentation pages above (under `articles/documentdb/`). + +The scraper (`scripts/scrape-operator-docs.ts`) fetches data from these sources and generates the `resources/scraped/operator-reference.md` dump file that serves as the contract between the documentation and the TypeScript implementation. + +## Usage + +```typescript +import { + getFilteredCompletions, + getAllCompletions, + FILTER_COMPLETION_META, + STAGE_COMPLETION_META, +} from '@vscode-documentdb/documentdb-constants'; + +// Get operators for a filter/query context +const filterOps = getFilteredCompletions({ meta: FILTER_COMPLETION_META }); + +// Get operators for a specific BSON type +const stringOps = getFilteredCompletions({ + meta: FILTER_COMPLETION_META, + bsonTypes: ['string'], +}); + +// Get all stage names +const stages = getFilteredCompletions({ meta: STAGE_COMPLETION_META }); +``` + +## Scraper + +The operator data is sourced from the official DocumentDB documentation. To re-scrape: + +```bash +npm run scrape --workspace=@vscode-documentdb/documentdb-constants +``` + +This runs the scraper and then formats the output with Prettier. The scraper: + +1. **Verifies** upstream doc structure (early fail-fast) +2. **Extracts** all operators from the [compatibility page](https://learn.microsoft.com/en-us/azure/documentdb/compatibility-query-language) +3. **Fetches** per-operator documentation (descriptions, syntax) with a global file index fallback for operators filed in unexpected directories +4. **Generates** `resources/scraped/operator-reference.md` in a structured heading format (`### $operator` with description, syntax, and doc link) + +The dump serves as the authoritative reference for the TypeScript implementation. A Jest test (`src/operatorReference.test.ts`) validates that the implementation matches the dump. + +## Structure + +| File | Purpose | +| ------------------------------------------- | -------------------------------------------- | +| `src/types.ts` | `OperatorEntry` interface and `MetaTag` type | +| `src/metaTags.ts` | Meta tag constants and completion presets | +| `src/docLinks.ts` | Documentation URL generation | +| `src/getFilteredCompletions.ts` | Primary consumer API: filter by meta tags | +| `src/index.ts` | Barrel exports for all public API | +| `resources/scraped/operator-reference.md` | Auto-generated scraped operator dump | +| `resources/overrides/operator-overrides.md` | Hand-maintained overrides | +| `resources/overrides/operator-snippets.md` | Snippet templates per category | +| `scripts/scrape-operator-docs.ts` | Scraper script | diff --git a/packages/documentdb-constants/jest.config.js b/packages/documentdb-constants/jest.config.js new file mode 100644 index 000000000..a39810b1f --- /dev/null +++ b/packages/documentdb-constants/jest.config.js @@ -0,0 +1,11 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + // Limit workers to avoid OOM kills on machines with many cores. + // Each ts-jest worker loads the TypeScript compiler and consumes ~500MB+. + maxWorkers: '50%', + testEnvironment: 'node', + testMatch: ['/src/**/*.test.ts'], + transform: { + '^.+\\.tsx?$': ['ts-jest', {}], + }, +}; diff --git a/packages/documentdb-constants/package.json b/packages/documentdb-constants/package.json new file mode 100644 index 000000000..99f3fb017 --- /dev/null +++ b/packages/documentdb-constants/package.json @@ -0,0 +1,25 @@ +{ + "name": "@vscode-documentdb/documentdb-constants", + "version": "1.0.0", + "description": "Static operator metadata for DocumentDB-supported operators, stages, accumulators, and BSON constructors", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p . && tsc -p tsconfig.scripts.json --noEmit", + "clean": "rimraf dist tsconfig.tsbuildinfo", + "test": "jest --config jest.config.js", + "prettier-fix": "prettier -w \"(scripts|src)/**/*.@(js|ts|jsx|tsx|json|md)\" \"./*.@(js|ts|jsx|tsx|json|md)\"", + "scrape": "ts-node scripts/scrape-operator-docs.ts && prettier --write resources/scraped/operator-reference.md", + "generate": "ts-node scripts/generate-from-reference.ts", + "evaluate": "ts-node scripts/evaluate-overrides.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode-documentdb", + "directory": "packages/documentdb-constants" + }, + "license": "MIT" +} diff --git a/packages/documentdb-constants/resources/overrides/operator-overrides.md b/packages/documentdb-constants/resources/overrides/operator-overrides.md new file mode 100644 index 000000000..2fe988737 --- /dev/null +++ b/packages/documentdb-constants/resources/overrides/operator-overrides.md @@ -0,0 +1,321 @@ +# DocumentDB Operator Reference โ€” Overrides + + + + + + + + + + + + + + + + + + + + + + + +--- + +## String Expression Operators + +### $concat + +- **Description:** Concatenates two or more strings and returns the resulting string. + +### $indexOfBytes + +- **Description:** Returns the byte index of the first occurrence of a substring within a string. + +### $indexOfCP + +- **Description:** Returns the code point index of the first occurrence of a substring within a string. + +### $ltrim + +- **Description:** Removes whitespace or specified characters from the beginning of a string. + +### $regexFind + +- **Description:** Applies a regular expression to a string and returns the first match. + +### $regexFindAll + +- **Description:** Applies a regular expression to a string and returns all matches as an array. + +### $regexMatch + +- **Description:** Applies a regular expression to a string and returns a boolean indicating if a match was found. + +### $replaceOne + +- **Description:** Replaces the first occurrence of a search string with a replacement string. + +### $replaceAll + +- **Description:** Replaces all occurrences of a search string with a replacement string. + +### $rtrim + +- **Description:** Removes whitespace or specified characters from the end of a string. + +### $split + +- **Description:** Splits a string by a delimiter and returns an array of substrings. + +### $strLenBytes + +- **Description:** Returns the number of UTF-8 encoded bytes in the specified string. + +### $strLenCP + +- **Description:** Returns the number of UTF-8 code points in the specified string. + +### $strcasecmp + +- **Description:** Performs a case-insensitive comparison of two strings and returns an integer. + +### $substr + +- **Description:** Returns a substring of a string, starting at a specified index for a specified length. Deprecated โ€” use $substrBytes or $substrCP. + +### $substrBytes + +- **Description:** Returns a substring of a string by byte index, starting at a specified index for a specified number of bytes. + +### $substrCP + +- **Description:** Returns a substring of a string by code point index, starting at a specified index for a specified number of code points. + +### $toLower + +- **Description:** Converts a string to lowercase and returns the result. + +### $toUpper + +- **Description:** Converts a string to uppercase and returns the result. + +### $trim + +- **Description:** Removes whitespace or specified characters from both ends of a string. + +## Trigonometry Expression Operators + +### $sin + +- **Description:** Returns the sine of a value measured in radians. + +### $cos + +- **Description:** Returns the cosine of a value measured in radians. + +### $tan + +- **Description:** Returns the tangent of a value measured in radians. + +### $asin + +- **Description:** Returns the arcsine (inverse sine) of a value in radians. + +### $acos + +- **Description:** Returns the arccosine (inverse cosine) of a value in radians. + +### $atan + +- **Description:** Returns the arctangent (inverse tangent) of a value in radians. + +### $atan2 + +- **Description:** Returns the arctangent of the quotient of two values, using the signs to determine the quadrant. + +### $asinh + +- **Description:** Returns the inverse hyperbolic sine of a value. + +### $acosh + +- **Description:** Returns the inverse hyperbolic cosine of a value. + +### $atanh + +- **Description:** Returns the inverse hyperbolic tangent of a value. + +### $sinh + +- **Description:** Returns the hyperbolic sine of a value. + +### $cosh + +- **Description:** Returns the hyperbolic cosine of a value. + +### $tanh + +- **Description:** Returns the hyperbolic tangent of a value. + +### $degreesToRadians + +- **Description:** Converts a value from degrees to radians. + +### $radiansToDegrees + +- **Description:** Converts a value from radians to degrees. + +## Aggregation Pipeline Stages + +### $bucketAuto + +- **Description:** Categorizes documents into a specified number of groups based on a given expression, automatically determining bucket boundaries. + +### $graphLookup + +- **Description:** Performs a recursive search on a collection to return documents connected by a specified field relationship. + +### $limit + +- **Description:** Restricts the number of documents passed to the next stage in the pipeline. + +### $project + +- **Description:** Reshapes documents by including, excluding, or computing new fields. + +### $replaceRoot + +- **Description:** Replaces the input document with a specified embedded document, promoting it to the top level. + +### $search + +- **Description:** Performs full-text search on string fields using Atlas Search or compatible search indexes. + +### $searchMeta + +- **Description:** Returns metadata about an Atlas Search query without returning the matching documents. + +### $setWindowFields + +- **Description:** Adds computed fields to documents using window functions over a specified partition and sort order. + +### $unionWith + +- **Description:** Combines the results of two collections into a single result set, similar to SQL UNION ALL. + +### $currentOp + +- **Description:** Returns information on active and queued operations for the database instance. + +## Array Update Operators + +### $[] + +- **Description:** Positional all operator. Acts as a placeholder to update all elements in an array field. + +### $[identifier] + +- **Description:** Filtered positional operator. Acts as a placeholder to update elements that match an arrayFilters condition. + +### $position + +- **Description:** Specifies the position in the array at which the $push operator inserts elements. Used with $each. + +## Array Expression Operators + +### $objectToArray + +- **Description:** Converts an object into an array of key-value pair documents. + +## Variables in Aggregation Expressions + +### $$NOW + +- **Description:** Returns the current datetime as a Date object. Constant throughout a single aggregation pipeline. + +### $$ROOT + +- **Description:** References the root document โ€” the top-level document currently being processed in the pipeline stage. + +### $$REMOVE + +- **Description:** Removes a field from the output document. Used with $project or $addFields to conditionally exclude fields. + +### $$CURRENT + +- **Description:** References the current document in the pipeline stage. Equivalent to $$ROOT at the start of the pipeline. + +### $$DESCEND + +- **Description:** Used with $redact. Returns the document fields at the current level and continues descending into subdocuments. + +### $$PRUNE + +- **Description:** Used with $redact. Excludes all fields at the current document level and stops descending into subdocuments. + +### $$KEEP + +- **Description:** Used with $redact. Keeps all fields at the current document level without further descending into subdocuments. + +## Array Expression Operators + +### $minN + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$minn + +## Comparison Expression Operators + +### $cmp + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$cmp + +## Window Operators + +### $minN + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$minn + +## Geospatial Operators + +### $box + +- **Standalone:** false + +### $center + +- **Standalone:** false + +### $centerSphere + +- **Standalone:** false + +### $geometry + +- **Standalone:** false + +### $maxDistance + +- **Standalone:** false + +### $minDistance + +- **Standalone:** false + +### $polygon + +- **Standalone:** false + +## Projection Operators + +### $ + +- **Standalone:** false + +## Miscellaneous Query Operators + +### $natural + +- **Standalone:** false diff --git a/packages/documentdb-constants/resources/overrides/operator-snippets.md b/packages/documentdb-constants/resources/overrides/operator-snippets.md new file mode 100644 index 000000000..9b3adf63e --- /dev/null +++ b/packages/documentdb-constants/resources/overrides/operator-snippets.md @@ -0,0 +1,810 @@ +# Operator Snippets + + + +## Aggregation Pipeline Stages + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: { ${1} } }` + +### $match + +- **Snippet:** `{ $match: { ${1:query} } }` + +### $group + +- **Snippet:** `{ $group: { _id: "${1:\$field}", ${2:accumulator}: { ${3:\$sum}: 1 } } }` + +### $project + +- **Snippet:** `{ $project: { ${1:field}: 1 } }` + +### $sort + +- **Snippet:** `{ $sort: { ${1:field}: ${2:1} } }` + +### $limit + +- **Snippet:** `{ $limit: ${1:number} }` + +### $skip + +- **Snippet:** `{ $skip: ${1:number} }` + +### $unwind + +- **Snippet:** `{ $unwind: "${1:\$arrayField}" }` + +### $lookup + +- **Snippet:** `{ $lookup: { from: "${1:collection}", localField: "${2:field}", foreignField: "${3:field}", as: "${4:result}" } }` + +### $addFields + +- **Snippet:** `{ $addFields: { ${1:newField}: ${2:expression} } }` + +### $set + +- **Snippet:** `{ $set: { ${1:field}: ${2:expression} } }` + +### $unset + +- **Snippet:** `{ $unset: "${1:field}" }` + +### $replaceRoot + +- **Snippet:** `{ $replaceRoot: { newRoot: "${1:\$field}" } }` + +### $replaceWith + +- **Snippet:** `{ $replaceWith: "${1:\$field}" }` + +### $count + +- **Snippet:** `{ $count: "${1:countField}" }` + +### $out + +- **Snippet:** `{ $out: "${1:collection}" }` + +### $merge + +- **Snippet:** `{ $merge: { into: "${1:collection}" } }` + +### $bucket + +- **Snippet:** `{ $bucket: { groupBy: "${1:\$field}", boundaries: [${2:values}], default: "${3:Other}" } }` + +### $bucketAuto + +- **Snippet:** `{ $bucketAuto: { groupBy: "${1:\$field}", buckets: ${2:number} } }` + +### $facet + +- **Snippet:** `{ $facet: { ${1:outputField}: [{ ${2:stage} }] } }` + +### $graphLookup + +- **Snippet:** `{ $graphLookup: { from: "${1:collection}", startWith: "${2:\$field}", connectFromField: "${3:field}", connectToField: "${4:field}", as: "${5:result}" } }` + +### $sample + +- **Snippet:** `{ $sample: { size: ${1:number} } }` + +### $sortByCount + +- **Snippet:** `{ $sortByCount: "${1:\$field}" }` + +### $redact + +- **Snippet:** `{ $redact: { \$cond: { if: { ${1:expression} }, then: "${2:\$\$DESCEND}", else: "${3:\$\$PRUNE}" } } }` + +### $unionWith + +- **Snippet:** `{ $unionWith: { coll: "${1:collection}", pipeline: [${2}] } }` + +### $setWindowFields + +- **Snippet:** `{ $setWindowFields: { partitionBy: "${1:\$field}", sortBy: { ${2:field}: ${3:1} }, output: { ${4:newField}: { ${5:windowFunc} } } } }` + +### $densify + +- **Snippet:** `{ $densify: { field: "${1:field}", range: { step: ${2:1}, bounds: "full" } } }` + +### $fill + +- **Snippet:** `{ $fill: { output: { ${1:field}: { method: "${2:linear}" } } } }` + +### $documents + +- **Snippet:** `{ $documents: [${1:documents}] }` + +### $changeStream + +- **Snippet:** `{ $changeStream: {} }` + +### $collStats + +- **Snippet:** `{ $collStats: { storageStats: {} } }` + +### $currentOp + +- **Snippet:** `{ $currentOp: { allUsers: true } }` + +### $indexStats + +- **Snippet:** `{ $indexStats: {} }` + +### $listLocalSessions + +- **Snippet:** `{ $listLocalSessions: { allUsers: true } }` + +### $geoNear + +- **Snippet:** `{ $geoNear: { near: { type: "Point", coordinates: [${1:lng}, ${2:lat}] }, distanceField: "${3:distance}" } }` + +## Comparison Query Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: ${1:value} }` + +### $in + +- **Snippet:** `{ $in: [${1:value}] }` + +### $nin + +- **Snippet:** `{ $nin: [${1:value}] }` + +## Logical Query Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: [{ ${1:expression} }] }` + +### $not + +- **Snippet:** `{ $not: { ${1:expression} } }` + +## Element Query Operators + +### $exists + +- **Snippet:** `{ $exists: ${1:true} }` + +### $type + +- **Snippet:** `{ $type: "${1:type}" }` + +## Evaluation Query Operators + +### $expr + +- **Snippet:** `{ $expr: { ${1:expression} } }` + +### $regex + +- **Snippet:** `{ $regex: /${1:pattern}/ }` + +### $mod + +- **Snippet:** `{ $mod: [${1:divisor}, ${2:remainder}] }` + +### $text + +- **Snippet:** `{ $text: { \$search: "${1:text}" } }` + +### $jsonSchema + +- **Snippet:** `{ $jsonSchema: { bsonType: "${1:object}" } }` + +## Array Query Operators + +### $all + +- **Snippet:** `{ $all: [${1:value}] }` + +### $elemMatch + +- **Snippet:** `{ $elemMatch: { ${1:query} } }` + +### $size + +- **Snippet:** `{ $size: ${1:number} }` + +## Bitwise Query Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: ${1:bitmask} }` + +## Geospatial Operators + +### $near + +- **Snippet:** `{ $near: { \$geometry: { type: "Point", coordinates: [${1:lng}, ${2:lat}] }, \$maxDistance: ${3:distance} } }` + +### $nearSphere + +- **Snippet:** `{ $nearSphere: { \$geometry: { type: "Point", coordinates: [${1:lng}, ${2:lat}] }, \$maxDistance: ${3:distance} } }` + +### $geoIntersects + +- **Snippet:** `{ $geoIntersects: { \$geometry: { type: "${1:GeoJSON type}", coordinates: ${2:coordinates} } } }` + +### $geoWithin + +- **Snippet:** `{ $geoWithin: { \$geometry: { type: "${1:GeoJSON type}", coordinates: ${2:coordinates} } } }` + +### $box + +- **Snippet:** `[[${1:bottomLeftX}, ${2:bottomLeftY}], [${3:upperRightX}, ${4:upperRightY}]]` + +### $center + +- **Snippet:** `[[${1:x}, ${2:y}], ${3:radius}]` + +### $centerSphere + +- **Snippet:** `[[${1:x}, ${2:y}], ${3:radiusInRadians}]` + +### $geometry + +- **Snippet:** `{ type: "${1:Point}", coordinates: [${2:coordinates}] }` + +### $maxDistance + +- **Snippet:** `${1:distance}` + +### $minDistance + +- **Snippet:** `${1:distance}` + +### $polygon + +- **Snippet:** `[[${1:x1}, ${2:y1}], [${3:x2}, ${4:y2}], [${5:x3}, ${6:y3}]]` + +## Projection Operators + +### $elemMatch + +- **Snippet:** `{ $elemMatch: { ${1:query} } }` + +### $slice + +- **Snippet:** `{ $slice: ${1:number} }` + +## Miscellaneous Query Operators + +### $comment + +- **Snippet:** `{ $comment: "${1:comment}" }` + +### $rand + +- **Snippet:** `{ $rand: {} }` + +### $natural + +- **Snippet:** `{ $natural: ${1:1} }` + +## Field Update Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: { "${1:field}": ${2:value} } }` + +### $rename + +- **Snippet:** `{ $rename: { "${1:oldField}": "${2:newField}" } }` + +### $currentDate + +- **Snippet:** `{ $currentDate: { "${1:field}": true } }` + +## Array Update Operators + +### $addToSet + +- **Snippet:** `{ $addToSet: { "${1:field}": ${2:value} } }` + +### $pop + +- **Snippet:** `{ $pop: { "${1:field}": ${2:1} } }` + +### $pull + +- **Snippet:** `{ $pull: { "${1:field}": ${2:condition} } }` + +### $push + +- **Snippet:** `{ $push: { "${1:field}": ${2:value} } }` + +### $pullAll + +- **Snippet:** `{ $pullAll: { "${1:field}": [${2:values}] } }` + +### $each + +- **Snippet:** `{ $each: [${1:values}] }` + +### $position + +- **Snippet:** `{ $position: ${1:index} }` + +### $slice + +- **Snippet:** `{ $slice: ${1:number} }` + +### $sort + +- **Snippet:** `{ $sort: { "${1:field}": ${2:1} } }` + +## Bitwise Update Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: { "${1:field}": { "${2:and|or|xor}": ${3:value} } } }` + +## Accumulators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$field}" }` + +### $count + +- **Snippet:** `{ $count: {} }` + +### $bottom + +- **Snippet:** `{ $bottom: { sortBy: { ${1:field}: ${2:1} }, output: "${3:\$field}" } }` + +### $top + +- **Snippet:** `{ $top: { sortBy: { ${1:field}: ${2:1} }, output: "${3:\$field}" } }` + +### $bottomN + +- **Snippet:** `{ $bottomN: { n: ${1:number}, sortBy: { ${2:field}: ${3:1} }, output: "${4:\$field}" } }` + +### $topN + +- **Snippet:** `{ $topN: { n: ${1:number}, sortBy: { ${2:field}: ${3:1} }, output: "${4:\$field}" } }` + +### $firstN + +- **Snippet:** `{ $firstN: { input: "${1:\$field}", n: ${2:number} } }` + +### $lastN + +- **Snippet:** `{ $lastN: { input: "${1:\$field}", n: ${2:number} } }` + +### $maxN + +- **Snippet:** `{ $maxN: { input: "${1:\$field}", n: ${2:number} } }` + +### $minN + +- **Snippet:** `{ $minN: { input: "${1:\$field}", n: ${2:number} } }` + +### $percentile + +- **Snippet:** `{ $percentile: { input: "${1:\$field}", p: [${2:0.5}], method: "approximate" } }` + +### $median + +- **Snippet:** `{ $median: { input: "${1:\$field}", method: "approximate" } }` + +## Window Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$field}" }` + +### $shift + +- **Snippet:** `{ $shift: { output: "${1:\$field}", by: ${2:1}, default: ${3:null} } }` + +### $rank + +- **Snippet:** `{ $rank: {} }` + +### $denseRank + +- **Snippet:** `{ $denseRank: {} }` + +### $documentNumber + +- **Snippet:** `{ $documentNumber: {} }` + +### $expMovingAvg + +- **Snippet:** `{ $expMovingAvg: { input: "${1:\$field}", N: ${2:number} } }` + +### $derivative + +- **Snippet:** `{ $derivative: { input: "${1:\$field}", unit: "${2:hour}" } }` + +### $integral + +- **Snippet:** `{ $integral: { input: "${1:\$field}", unit: "${2:hour}" } }` + +## Arithmetic Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$field}" }` + +### $add + +- **Snippet:** `{ $add: ["${1:\$field1}", "${2:\$field2}"] }` + +### $subtract + +- **Snippet:** `{ $subtract: ["${1:\$field1}", "${2:\$field2}"] }` + +### $multiply + +- **Snippet:** `{ $multiply: ["${1:\$field1}", "${2:\$field2}"] }` + +### $divide + +- **Snippet:** `{ $divide: ["${1:\$field1}", "${2:\$field2}"] }` + +### $mod + +- **Snippet:** `{ $mod: ["${1:\$field1}", "${2:\$field2}"] }` + +### $pow + +- **Snippet:** `{ $pow: ["${1:\$field1}", "${2:\$field2}"] }` + +### $log + +- **Snippet:** `{ $log: ["${1:\$number}", ${2:base}] }` + +### $round + +- **Snippet:** `{ $round: ["${1:\$field}", ${2:place}] }` + +## Array Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$array}" }` + +### $arrayElemAt + +- **Snippet:** `{ $arrayElemAt: ["${1:\$array}", ${2:index}] }` + +### $concatArrays + +- **Snippet:** `{ $concatArrays: ["${1:\$array1}", "${2:\$array2}"] }` + +### $filter + +- **Snippet:** `{ $filter: { input: "${1:\$array}", as: "${2:item}", cond: { ${3:expression} } } }` + +### $in + +- **Snippet:** `{ $in: ["${1:\$field}", "${2:\$array}"] }` + +### $indexOfArray + +- **Snippet:** `{ $indexOfArray: ["${1:\$array}", "${2:value}"] }` + +### $isArray + +- **Snippet:** `{ $isArray: "${1:\$field}" }` + +### $map + +- **Snippet:** `{ $map: { input: "${1:\$array}", as: "${2:item}", in: { ${3:expression} } } }` + +### $objectToArray + +- **Snippet:** `{ $objectToArray: "${1:\$object}" }` + +### $range + +- **Snippet:** `{ $range: [${1:start}, ${2:end}, ${3:step}] }` + +### $reduce + +- **Snippet:** `{ $reduce: { input: "${1:\$array}", initialValue: ${2:0}, in: { ${3:expression} } } }` + +### $slice + +- **Snippet:** `{ $slice: ["${1:\$array}", ${2:n}] }` + +### $sortArray + +- **Snippet:** `{ $sortArray: { input: "${1:\$array}", sortBy: { ${2:field}: ${3:1} } } }` + +### $zip + +- **Snippet:** `{ $zip: { inputs: ["${1:\$array1}", "${2:\$array2}"] } }` + +### $maxN + +- **Snippet:** `{ $maxN: { input: "${1:\$array}", n: ${2:number} } }` + +### $minN + +- **Snippet:** `{ $minN: { input: "${1:\$array}", n: ${2:number} } }` + +### $firstN + +- **Snippet:** `{ $firstN: { input: "${1:\$array}", n: ${2:number} } }` + +### $lastN + +- **Snippet:** `{ $lastN: { input: "${1:\$array}", n: ${2:number} } }` + +## Boolean Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: ["${1:expression1}", "${2:expression2}"] }` + +### $not + +- **Snippet:** `{ $not: ["${1:expression}"] }` + +## Comparison Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: ["${1:\$field1}", "${2:\$field2}"] }` + +## Conditional Expression Operators + +### $cond + +- **Snippet:** `{ $cond: { if: { ${1:expression} }, then: ${2:trueValue}, else: ${3:falseValue} } }` + +### $ifNull + +- **Snippet:** `{ $ifNull: ["${1:\$field}", ${2:replacement}] }` + +### $switch + +- **Snippet:** `{ $switch: { branches: [{ case: { ${1:expression} }, then: ${2:value} }], default: ${3:defaultValue} } }` + +## Date Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$dateField}" }` + +### $dateAdd + +- **Snippet:** `{ $dateAdd: { startDate: "${1:\$dateField}", unit: "${2:day}", amount: ${3:1} } }` + +### $dateSubtract + +- **Snippet:** `{ $dateSubtract: { startDate: "${1:\$dateField}", unit: "${2:day}", amount: ${3:1} } }` + +### $dateDiff + +- **Snippet:** `{ $dateDiff: { startDate: "${1:\$startDate}", endDate: "${2:\$endDate}", unit: "${3:day}" } }` + +### $dateFromParts + +- **Snippet:** `{ $dateFromParts: { year: ${1:2024}, month: ${2:1}, day: ${3:1} } }` + +### $dateToParts + +- **Snippet:** `{ $dateToParts: { date: "${1:\$dateField}" } }` + +### $dateFromString + +- **Snippet:** `{ $dateFromString: { dateString: "${1:dateString}" } }` + +### $dateToString + +- **Snippet:** `{ $dateToString: { format: "${1:%Y-%m-%d}", date: "${2:\$dateField}" } }` + +### $dateTrunc + +- **Snippet:** `{ $dateTrunc: { date: "${1:\$dateField}", unit: "${2:day}" } }` + +### $toDate + +- **Snippet:** `{ $toDate: "${1:\$field}" }` + +## Object Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$object}" }` + +### $mergeObjects + +- **Snippet:** `{ $mergeObjects: ["${1:\$object1}", "${2:\$object2}"] }` + +### $setField + +- **Snippet:** `{ $setField: { field: "${1:fieldName}", input: "${2:\$object}", value: ${3:value} } }` + +## Set Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: ["${1:\$set1}", "${2:\$set2}"] }` + +### $anyElementTrue + +- **Snippet:** `{ $anyElementTrue: ["${1:\$array}"] }` + +### $allElementsTrue + +- **Snippet:** `{ $allElementsTrue: ["${1:\$array}"] }` + +## String Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$string}" }` + +### $concat + +- **Snippet:** `{ $concat: ["${1:\$string1}", "${2:\$string2}"] }` + +### $indexOfBytes + +- **Snippet:** `{ $indexOfBytes: ["${1:\$string}", "${2:substring}"] }` + +### $indexOfCP + +- **Snippet:** `{ $indexOfCP: ["${1:\$string}", "${2:substring}"] }` + +### $regexFind + +- **Snippet:** `{ $regexFind: { input: "${1:\$string}", regex: "${2:pattern}" } }` + +### $regexFindAll + +- **Snippet:** `{ $regexFindAll: { input: "${1:\$string}", regex: "${2:pattern}" } }` + +### $regexMatch + +- **Snippet:** `{ $regexMatch: { input: "${1:\$string}", regex: "${2:pattern}" } }` + +### $replaceOne + +- **Snippet:** `{ $replaceOne: { input: "${1:\$string}", find: "${2:find}", replacement: "${3:replacement}" } }` + +### $replaceAll + +- **Snippet:** `{ $replaceAll: { input: "${1:\$string}", find: "${2:find}", replacement: "${3:replacement}" } }` + +### $split + +- **Snippet:** `{ $split: ["${1:\$string}", "${2:delimiter}"] }` + +### $substr + +- **Snippet:** `{ $substr: ["${1:\$string}", ${2:start}, ${3:length}] }` + +### $substrBytes + +- **Snippet:** `{ $substrBytes: ["${1:\$string}", ${2:start}, ${3:length}] }` + +### $substrCP + +- **Snippet:** `{ $substrCP: ["${1:\$string}", ${2:start}, ${3:length}] }` + +### $strcasecmp + +- **Snippet:** `{ $strcasecmp: ["${1:\$string1}", "${2:\$string2}"] }` + +### $trim + +- **Snippet:** `{ $trim: { input: "${1:\$string}" } }` + +### $ltrim + +- **Snippet:** `{ $ltrim: { input: "${1:\$string}" } }` + +### $rtrim + +- **Snippet:** `{ $rtrim: { input: "${1:\$string}" } }` + +## Trigonometry Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$value}" }` + +### $degreesToRadians + +- **Snippet:** `{ $degreesToRadians: "${1:\$angle}" }` + +### $radiansToDegrees + +- **Snippet:** `{ $radiansToDegrees: "${1:\$angle}" }` + +## Type Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$field}" }` + +### $convert + +- **Snippet:** `{ $convert: { input: "${1:\$field}", to: "${2:type}" } }` + +## Data Size Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$field}" }` + +## Literal Expression Operator + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: ${1:value} }` + +## Miscellaneous Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: ${1:value} }` + +### $getField + +- **Snippet:** `{ $getField: { field: "${1:fieldName}", input: "${2:\$object}" } }` + +### $rand + +- **Snippet:** `{ $rand: {} }` + +### $sampleRate + +- **Snippet:** `{ $sampleRate: ${1:0.5} }` + +## Bitwise Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: [${1:value1}, ${2:value2}] }` + +### $bitNot + +- **Snippet:** `{ $bitNot: "${1:\$field}" }` + +## Timestamp Expression Operators + +### DEFAULT + +- **Snippet:** `{ {{VALUE}}: "${1:\$timestampField}" }` + +## Variable Expression Operators + +### $let + +- **Snippet:** `{ $let: { vars: { ${1:var}: ${2:expression} }, in: ${3:expression} } }` diff --git a/packages/documentdb-constants/resources/scraped/operator-reference.md b/packages/documentdb-constants/resources/scraped/operator-reference.md new file mode 100644 index 000000000..6ba385cfb --- /dev/null +++ b/packages/documentdb-constants/resources/scraped/operator-reference.md @@ -0,0 +1,4113 @@ +# DocumentDB Operator Reference + + + + + +## Summary + +| Category | Listed | Total | +| ------------------------------------------------------------- | ------- | ------- | +| Comparison Query Operators | 8 | 8 | +| Logical Query Operators | 4 | 4 | +| Element Query Operators | 2 | 2 | +| Evaluation Query Operators | 5 | 6 | +| Geospatial Operators | 11 | 11 | +| Array Query Operators | 3 | 3 | +| Bitwise Query Operators | 4 | 4 | +| Projection Operators | 3 | 4 | +| Miscellaneous Query Operators | 3 | 3 | +| Field Update Operators | 9 | 9 | +| Array Update Operators | 12 | 12 | +| Bitwise Update Operators | 1 | 1 | +| Arithmetic Expression Operators | 16 | 16 | +| Array Expression Operators | 20 | 20 | +| Bitwise Operators | 4 | 4 | +| Boolean Expression Operators | 3 | 3 | +| Comparison Expression Operators | 7 | 7 | +| Custom Aggregation Expression Operators | 0 | 2 | +| Data Size Operators | 2 | 2 | +| Date Expression Operators | 22 | 22 | +| Literal Expression Operator | 1 | 1 | +| Miscellaneous Operators | 3 | 3 | +| Object Expression Operators | 3 | 3 | +| Set Expression Operators | 7 | 7 | +| String Expression Operators | 23 | 23 | +| Text Expression Operator | 0 | 1 | +| Timestamp Expression Operators | 2 | 2 | +| Trigonometry Expression Operators | 15 | 15 | +| Type Expression Operators | 11 | 11 | +| Accumulators ($group, $bucket, $bucketAuto, $setWindowFields) | 21 | 22 | +| Accumulators (in Other Stages) | 10 | 10 | +| Variable Expression Operators | 1 | 1 | +| Window Operators | 27 | 27 | +| Conditional Expression Operators | 3 | 3 | +| Aggregation Pipeline Stages | 35 | 42 | +| Variables in Aggregation Expressions | 7 | 10 | +| **Total** | **308** | **324** | + +## Comparison Query Operators + +### $eq + +- **Description:** The $eq query operator compares the value of a field to a specified value +- **Syntax:** + +```javascript +{ + field: { + $eq: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$eq + +### $gt + +- **Description:** The $gt query operator retrieves documents where the value of a field is greater than a specified value +- **Syntax:** + +```javascript +{ + field: { + $gt: value; + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$gt + +### $gte + +- **Description:** The $gte operator retrieves documents where the value of a field is greater than or equal to a specified value +- **Syntax:** + +```javascript +{ + field: { + $gte: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$gte + +### $in + +- **Description:** The $in operator matches value of a field against an array of specified values +- **Syntax:** + +```javascript +{ + field: { + $in: [listOfValues]; + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$in + +### $lt + +- **Description:** The $lt operator retrieves documents where the value of field is less than a specified value +- **Syntax:** + +```javascript +{ + field: { + $lt: value; + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$lt + +### $lte + +- **Description:** The $lte operator retrieves documents where the value of a field is less than or equal to a specified value +- **Syntax:** + +```javascript +{ + field: { + $lte: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$lte + +### $ne + +- **Description:** The $ne operator retrieves documents where the value of a field doesn't equal a specified value +- **Syntax:** + +```javascript +{ + field: { + $ne: value; + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$ne + +### $nin + +- **Description:** The $nin operator retrieves documents where the value of a field doesn't match a list of values +- **Syntax:** + +```javascript +{ + field: { + $nin: [ < listOfValues > ] + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$nin + +## Logical Query Operators + +### $and + +- **Description:** The $and operator joins multiple query clauses and returns documents that match all specified conditions. +- **Syntax:** + +```javascript +{ + $and: [{ + < expression1 > + }, { + < expression2 > + }, ..., { + < expressionN > + }] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/logical-query/$and + +### $not + +- **Description:** The $not operator performs a logical NOT operation on a specified expression, selecting documents that don't match the expression. +- **Syntax:** + +```javascript +{ + field: { + $not: { + < operator - expression > + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/logical-query/$not + +### $nor + +- **Description:** The $nor operator performs a logical NOR on an array of expressions and retrieves documents that fail all the conditions. +- **Syntax:** + +```javascript +{ + $nor: [{ + < expression1 > + }, { + < expression2 > + }, ..., { + < expressionN > + }] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/logical-query/$nor + +### $or + +- **Description:** The $or operator joins query clauses with a logical OR and returns documents that match at least one of the specified conditions. +- **Syntax:** + +```javascript +{ + $or: [{ + < expression1 > + }, { + < expression2 > + }, ..., { + < expressionN > + }] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/logical-query/$or + +## Element Query Operators + +### $exists + +- **Description:** The $exists operator retrieves documents that contain the specified field in their document structure. +- **Syntax:** + +```javascript +{ + : { $exists: } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/element-query/$exists + +### $type + +- **Description:** The $type operator retrieves documents if the chosen field is of the specified type. +- **Syntax:** + +```javascript +{ + : { $type: | } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/element-query/$type + +## Evaluation Query Operators + +### $expr + +- **Description:** The $expr operator allows the use of aggregation expressions within the query language, enabling complex field comparisons and calculations. +- **Syntax:** + +```javascript +{ + $expr: { } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/evaluation-query/$expr + +### $jsonSchema + +- **Description:** The $jsonSchema operator validates documents against a JSON Schema definition for data validation and structure enforcement. Discover supported features and limitations. +- **Syntax:** + +```javascript +db.createCollection('collectionName', { + validator: { + $jsonSchema: { + bsonType: 'object', + required: ['field1', 'field2'], + properties: { + field1: { + bsonType: 'string', + }, + field2: { + bsonType: 'int', + minimum: 0, + description: 'Description of field2 requirements', + }, + }, + }, + }, + validationLevel: 'strict', // Optional: "strict" or "moderate" + validationAction: 'error', // Optional: "error" or "warn" +}); +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/evaluation-query/$jsonschema + +### $mod + +- **Description:** The $mod operator performs a modulo operation on the value of a field and selects documents with a specified result. +- **Syntax:** + +```javascript +{ + : { $mod: [ , ] } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/evaluation-query/$mod + +### $regex + +- **Description:** The $regex operator provides regular expression capabilities for pattern matching in queries, allowing flexible string matching and searching. +- **Syntax:** + +```javascript +{ + : { $regex: , $options: } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/evaluation-query/$regex + +### $text + +- **Description:** The $text operator performs text search on the content of indexed string fields, enabling full-text search capabilities. +- **Syntax:** + +```javascript +{ + $text: { + $search: , + $language: , + $caseSensitive: , + $diacriticSensitive: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/evaluation-query/$text + +## Geospatial Operators + +### $geoIntersects + +- **Description:** The $geoIntersects operator selects documents whose location field intersects with a specified GeoJSON object. +- **Syntax:** + +```javascript +{ + : { + $geoIntersects: { + $geometry: { + type: , + coordinates: + } + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$geointersects + +### $geoWithin + +- **Description:** The $geoWithin operator selects documents whose location field is completely within a specified geometry. +- **Syntax:** + +```javascript +// Using $box +{ + : { + $geoWithin: { + $box: [ [ ], [ ] ] + } + } +} + +// Using $center +{ + : { + $geoWithin: { + $center: [ [ , ], ] + } + } +} + +// Using $geometry +{ + : { + $geoWithin: { + $geometry: { + type: , + coordinates: + } + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$geowithin + +### $box + +- **Description:** The $box operator defines a rectangular area for geospatial queries using coordinate pairs. +- **Syntax:** + +```javascript +{ + : { + $geoWithin: { + $box: [ + [, ], + [, ] + ] + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$box + +### $center + +- **Description:** The $center operator specifies a circle using legacy coordinate pairs for $geoWithin queries. +- **Syntax:** + +```javascript +{ + $geoWithin: { + $center: [ [ , ], ] + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$center + +### $centerSphere + +- **Description:** The $centerSphere operator specifies a circle using spherical geometry for $geoWithin queries. +- **Syntax:** + +```javascript +{ + $geoWithin: { + $centerSphere: [ [ , ], ] + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$centersphere + +### $geometry + +- **Description:** The $geometry operator specifies a GeoJSON geometry for geospatial queries. +- **Syntax:** + +```javascript +{ + $geometry: { + type: , + coordinates: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$geometry + +### $maxDistance + +- **Description:** The $maxDistance operator specifies the maximum distance that can exist between two points in a geospatial query. +- **Syntax:** + +```javascript +{ + : { + $near: { + $geometry: { + type: "Point", + coordinates: [, ] + }, + $maxDistance: + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$maxdistance + +### $minDistance + +- **Description:** The $minDistance operator specifies the minimum distance that must exist between two points in a geospatial query. +- **Syntax:** + +```javascript +{ + : { + $near: { + $geometry: { + type: "Point", + coordinates: [, ] + }, + $minDistance: + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$mindistance + +### $polygon + +- **Description:** The $polygon operator defines a polygon for geospatial queries, allowing you to find locations within an irregular shape. +- **Syntax:** + +```javascript +{ + : { + $geoWithin: { + $geometry: { + type: "Polygon", + coordinates: [ + [[, ], ..., [, ], [, ]] + ] + } + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$polygon + +### $near + +- **Description:** The $near operator returns documents with location fields that are near a specified point, sorted by distance. +- **Syntax:** + +```javascript +{ + : { + $near: { + $geometry: { + type: "Point", + coordinates: [, ] + }, + $maxDistance: , + $minDistance: + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$near + +### $nearSphere + +- **Description:** The $nearSphere operator returns documents whose location fields are near a specified point on a sphere, sorted by distance on a spherical surface. +- **Syntax:** + +```javascript +{ + : { + $nearSphere: { + $geometry: { + type: "Point", + coordinates: [, ] + }, + $maxDistance: , + $minDistance: + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/geospatial/$nearsphere + +## Array Query Operators + +### $all + +- **Description:** The $all operator helps finding array documents matching all the elements. +- **Syntax:** + +```javascript +db.collection.find({ + field : { + $all: [ < value1 > , < value2 > ] + } +}) +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-query/$all + +### $elemMatch + +- **Description:** The $elemmatch operator returns complete array, qualifying criteria with at least one matching array element. +- **Syntax:** + +```javascript +db.collection.find({ : { $elemMatch: { , , ... } } }) +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-query/$elemmatch + +### $size + +- **Description:** The $size operator is used to query documents where an array field has a specified number of elements. +- **Syntax:** + +```javascript +db.collection.find({ : { $size: } }) +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-query/$size + +## Bitwise Query Operators + +### $bitsAllClear + +- **Description:** The $bitsAllClear operator is used to match documents where all the bit positions specified in a bitmask are clear. +- **Syntax:** + +```javascript +{ + : { $bitsAllClear: } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise-query/$bitsallclear + +### $bitsAllSet + +- **Description:** The bitsAllSet command is used to match documents where all the specified bit positions are set. +- **Syntax:** + +```javascript +{ + : { $bitsAllSet: } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise-query/$bitsallset + +### $bitsAnyClear + +- **Description:** The $bitsAnyClear operator matches documents where any of the specified bit positions in a bitmask are clear. +- **Syntax:** + +```javascript +{ + : { $bitsAnyClear: } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise-query/$bitsanyclear + +### $bitsAnySet + +- **Description:** The $bitsAnySet operator returns documents where any of the specified bit positions are set to 1. +- **Syntax:** + +```javascript +{ + : { $bitsAnySet: [ ] } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise-query/$bitsanyset + +## Projection Operators + +### $ + +- **Description:** The $ positional operator identifies an element in an array to update without explicitly specifying the position of the element in the array. +- **Syntax:** + +```javascript +db.collection.updateOne( + { : }, + { : { ".$": } } +) +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'projection/'. Content scraped from 'array-update/'. + +### $elemMatch + +- **Description:** The $elemmatch operator returns complete array, qualifying criteria with at least one matching array element. +- **Syntax:** + +```javascript +db.collection.find({ : { $elemMatch: { , , ... } } }) +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'projection/'. Content scraped from 'array-query/'. + +### $slice + +- **Description:** The $slice operator returns a subset of an array from any element onwards in the array. +- **Syntax:** + +```javascript +{ + $slice: [ , ] +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'projection/'. Content scraped from 'array-expression/'. + +## Miscellaneous Query Operators + +### $comment + +- **Description:** The $comment operator adds a comment to a query to help identify the query in logs and profiler output. +- **Syntax:** + +```javascript +{ + $comment: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/miscellaneous-query/$comment + +### $rand + +- **Description:** The $rand operator generates a random float value between 0 and 1. +- **Syntax:** + +```javascript +{ + $rand: { + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/miscellaneous-query/$rand + +### $natural + +- **Description:** The $natural operator forces the query to use the natural order of documents in a collection, providing control over document ordering and retrieval. +- **Syntax:** + +```javascript +{ + $natural: <1 | -1> +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/miscellaneous-query/$natural + +## Field Update Operators + +### $currentDate + +- **Description:** The $currentDate operator sets the value of a field to the current date, either as a Date or a timestamp. +- **Syntax:** + +```javascript +{ + $currentDate: { + : , + : , + ... + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/field-update/$currentdate + +### $inc + +- **Description:** The $inc operator increments the value of a field by a specified amount. +- **Syntax:** + +```javascript +{ + $inc: { + : , + : , + ... + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/field-update/$inc + +### $min + +- **Description:** Retrieves the minimum value for a specified field +- **Syntax:** + +```javascript +$min: +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'field-update/'. Content scraped from 'accumulators/'. + +### $max + +- **Description:** The $max operator returns the maximum value from a set of input values. +- **Syntax:** + +```javascript +$max: +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'field-update/'. Content scraped from 'accumulators/'. + +### $mul + +- **Description:** The $mul operator multiplies the value of a field by a specified number. +- **Syntax:** + +```javascript +{ + $mul: { + : , + : , + ... + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/field-update/$mul + +### $rename + +- **Description:** The $rename operator allows renaming fields in documents during update operations. +- **Syntax:** + +```javascript +{ + $rename: { + : , + : , + ... + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/field-update/$rename + +### $set + +- **Description:** The $set operator in Azure DocumentDB updates or creates a new field with a specified value +- **Syntax:** + +```javascript +{ + $set: { + newField: + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'field-update/'. Content scraped from 'aggregation/'. + +### $setOnInsert + +- **Description:** The $setOnInsert operator sets field values only when an upsert operation results in an insert of a new document. +- **Syntax:** + +```javascript +{ + $setOnInsert: { + : , + : , + ... + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/field-update/$setoninsert + +### $unset + +- **Description:** The $unset stage in the aggregation pipeline is used to remove specified fields from documents. +- **Syntax:** + +```javascript +{ + $unset: "" | ["", "", ...] +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'field-update/'. Content scraped from 'aggregation/'. + +## Array Update Operators + +### $ + +- **Description:** The $ positional operator identifies an element in an array to update without explicitly specifying the position of the element in the array. +- **Syntax:** + +```javascript +db.collection.updateOne( + { : }, + { : { ".$": } } +) +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$ + +### $[] + +### $[identifier] + +### $addToSet + +- **Description:** The addToSet operator adds elements to an array if they don't already exist, while ensuring uniqueness of elements within the set. +- **Syntax:** + +```javascript +{ + $addToSet: { : } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$addtoset + +### $pop + +- **Description:** Removes the first or last element of an array. +- **Syntax:** + +```javascript +{ + $pop: { + : + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$pop + +### $pull + +- **Description:** Removes all instances of a value from an array. +- **Syntax:** + +```javascript +{ + $pull: { : } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$pull + +### $push + +- **Description:** The $push operator adds a specified value to an array within a document. +- **Syntax:** + +```javascript +db.collection.update({ + < query > +}, { + $push: { + < field >: < value > + } +}, { + < options > +}) +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$push + +### $pullAll + +- **Description:** The $pullAll operator is used to remove all instances of the specified values from an array. +- **Syntax:** + +```javascript +{ + $pullAll: { : [ , ] } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$pullall + +### $each + +- **Description:** The $each operator is used within an `$addToSet`or`$push` operation to add multiple elements to an array field in a single update operation. +- **Syntax:** + +```javascript +{ + $push: { + : { + $each: [ , ], + : , + : + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$each + +### $position + +### $slice + +- **Description:** The $slice operator returns a subset of an array from any element onwards in the array. +- **Syntax:** + +```javascript +{ + $slice: [ , ] +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'array-update/'. Content scraped from 'array-expression/'. + +### $sort + +- **Description:** The $sort stage in the aggregation pipeline is used to order the documents in the pipeline by a specified field or fields. +- **Syntax:** + +```javascript +{ + $sort: { + < field1 >: < sort order > , + < field2 >: < sort order > + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'array-update/'. Content scraped from 'aggregation/'. + +## Bitwise Update Operators + +### $bit + +- **Description:** The `$bit` operator is used to perform bitwise operations on integer values. +- **Syntax:** + +```javascript +{ + $bit: { + < field >: { + < operator >: < number > + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise-update/$bit + +## Arithmetic Expression Operators + +### $abs + +- **Description:** The $abs operator returns the absolute value of a number. +- **Syntax:** + +```javascript +{ + $abs: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$abs + +### $add + +- **Description:** The $add operator returns the sum of two numbers or the sum of a date and numbers. +- **Syntax:** + +```javascript +{ + $add: [ ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$add + +### $ceil + +- **Description:** The $ceil operator returns the smallest integer greater than or equal to the specified number. +- **Syntax:** + +```javascript +{ + $ceil: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$ceil + +### $divide + +- **Description:** The $divide operator divides two numbers and returns the quotient. +- **Syntax:** + +```javascript +{ + $divide: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$divide + +### $exp + +- **Description:** The $exp operator raises e to the specified exponent and returns the result +- **Syntax:** + +```javascript +{ + $exp: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$exp + +### $floor + +- **Description:** The $floor operator returns the largest integer less than or equal to the specified number +- **Syntax:** + +```javascript +{ + $floor: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$floor + +### $ln + +- **Description:** The $ln operator calculates the natural logarithm of the input +- **Syntax:** + +```javascript +{ + $ln: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$ln + +### $log + +- **Description:** The $log operator calculates the logarithm of a number in the specified base +- **Syntax:** + +```javascript +{ + $log: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$log + +### $log10 + +- **Description:** The $log10 operator calculates the log of a specified number in base 10 +- **Syntax:** + +```javascript +{ + $log10: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$log10 + +### $mod + +- **Description:** The $mod operator performs a modulo operation on the value of a field and selects documents with a specified result. +- **Syntax:** + +```javascript +{ + : { $mod: [ , ] } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'arithmetic-expression/'. Content scraped from 'evaluation-query/'. + +### $multiply + +- **Description:** The $multiply operator multiplies the input numerical values +- **Syntax:** + +```javascript +{ + $multiply: [ ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$multiply + +### $pow + +- **Description:** The `$pow` operator calculates the value of a numerical value raised to the power of a specified exponent. +- **Syntax:** + +```javascript +{ + $pow: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$pow + +### $round + +- **Description:** The $round operator rounds a number to a specified decimal place. +- **Syntax:** + +```javascript +{ + $round: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$round + +### $sqrt + +- **Description:** The $sqrt operator calculates and returns the square root of an input number +- **Syntax:** + +```javascript +{ + $sqrt: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$sqrt + +### $subtract + +- **Description:** The $subtract operator subtracts two numbers and returns the result. +- **Syntax:** + +```javascript +{ + $subtract: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$subtract + +### $trunc + +- **Description:** The $trunc operator truncates a number to a specified decimal place. +- **Syntax:** + +```javascript +{ + $trunc: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/arithmetic-expression/$trunc + +## Array Expression Operators + +### $arrayElemAt + +- **Description:** The $arrayElemAt returns the element at the specified array index. +- **Syntax:** + +```javascript +{ + $arrayElemAt: ["", ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$arrayelemat + +### $arrayToObject + +- **Description:** The $arrayToObject allows converting an array into a single document. +- **Syntax:** + +```javascript +{ + $arrayToObject: ''; +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$arraytoobject + +### $concatArrays + +- **Description:** The $concatArrays is used to combine multiple arrays into a single array. +- **Syntax:** + +```javascript +{ + $concatArrays: ['', '']; +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$concatarrays + +### $filter + +- **Description:** The $filter operator filters for elements from an array based on a specified condition. +- **Syntax:** + +```javascript +{ + $filter: { + input: "", + as: "", + cond: "" + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$filter + +### $firstN + +- **Description:** The $firstN operator sorts documents on one or more fields specified by the query and returns the first N document matching the filtering criteria +- **Syntax:** + +```javascript +{ + $firstN: { + input: [listOfFields], + sortBy: { + : + }, + n: + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'array-expression/'. Content scraped from 'accumulators/'. + +### $in + +- **Description:** The $in operator matches value of a field against an array of specified values +- **Syntax:** + +```javascript +{ + field: { + $in: [listOfValues]; + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'array-expression/'. Content scraped from 'comparison-query/'. + +### $indexOfArray + +- **Description:** The $indexOfArray operator is used to search for an element in an array and return the index of the first occurrence of the element. +- **Syntax:** + +```javascript +{ + $indexOfArray: [ < array > , < searchElement > , < start > , < end > ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$indexofarray + +### $isArray + +- **Description:** The $isArray operator is used to determine if a specified value is an array. +- **Syntax:** + +```javascript +{ + $isArray: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$isarray + +### $lastN + +- **Description:** The $lastN accumulator operator returns the last N values in a group of documents. +- **Syntax:** + +```javascript +{ + $group: { + _id: < expression > , + < field >: { + $lastN: { + n: < number >, + input: < expression > + } + } + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'array-expression/'. Content scraped from 'accumulators/'. + +### $map + +- **Description:** The $map operator allows applying an expression to each element in an array. +- **Syntax:** + +```javascript +{ + $map: { + input: , + as: , + in: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$map + +### $maxN + +- **Description:** Retrieves the top N values based on a specified filtering criteria +- **Syntax:** + +```javascript +$maxN: { + input: < field or expression > , + n: < number of values to retrieve > +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'array-expression/'. Content scraped from 'accumulators/'. + +### $minN + +- **Description:** Retrieves the bottom N values based on a specified filtering criteria +- **Syntax:** + +```javascript +$minN: { + input: < field or expression > , + n: < number of values to retrieve > +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'array-expression/'. Content scraped from 'accumulators/'. + +### $objectToArray + +### $range + +- **Description:** The $range operator allows generating an array of sequential integers. +- **Syntax:** + +```javascript +{ + $range: [ , , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$range + +### $reduce + +- **Description:** The $reduce operator applies an expression to each element in an array & accumulate result as single value. +- **Syntax:** + +```javascript +$reduce: { + input: , + initialValue: , + in: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$reduce + +### $reverseArray + +- **Description:** The $reverseArray operator is used to reverse the order of elements in an array. +- **Syntax:** + +```javascript +{ + $reverseArray: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$reversearray + +### $size + +- **Description:** The $size operator is used to query documents where an array field has a specified number of elements. +- **Syntax:** + +```javascript +db.collection.find({ : { $size: } }) +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'array-expression/'. Content scraped from 'array-query/'. + +### $slice + +- **Description:** The $slice operator returns a subset of an array from any element onwards in the array. +- **Syntax:** + +```javascript +{ + $slice: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$slice + +### $sortArray + +- **Description:** The $sortArray operator helps in sorting the elements in an array. +- **Syntax:** + +```javascript +{ + $sortArray: { + input: , + sortBy: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$sortarray + +### $zip + +- **Description:** The $zip operator allows merging two or more arrays element-wise into a single array or arrays. +- **Syntax:** + +```javascript +{ + $zip: { + inputs: [ , , ... ], + useLongestLength: , // Optional + defaults: // Optional + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$zip + +## Bitwise Operators + +### $bitAnd + +- **Description:** The $bitAnd operator performs a bitwise AND operation on integer values and returns the result as an integer. +- **Syntax:** + +```javascript +{ + $bitAnd: [ , , ... ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise/$bitand + +### $bitNot + +- **Description:** The $bitNot operator performs a bitwise NOT operation on integer values and returns the result as an integer. +- **Syntax:** + +```javascript +{ + $bitNot: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise/$bitnot + +### $bitOr + +- **Description:** The $bitOr operator performs a bitwise OR operation on integer values and returns the result as an integer. +- **Syntax:** + +```javascript +{ + $bitOr: [ , , ... ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise/$bitor + +### $bitXor + +- **Description:** The $bitXor operator performs a bitwise XOR operation on integer values. +- **Syntax:** + +```javascript +{ + $bitXor: [ , , ... ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/bitwise/$bitxor + +## Boolean Expression Operators + +### $and + +- **Description:** The $and operator joins multiple query clauses and returns documents that match all specified conditions. +- **Syntax:** + +```javascript +{ + $and: [{ + < expression1 > + }, { + < expression2 > + }, ..., { + < expressionN > + }] +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'boolean-expression/'. Content scraped from 'logical-query/'. + +### $not + +- **Description:** The $not operator performs a logical NOT operation on a specified expression, selecting documents that don't match the expression. +- **Syntax:** + +```javascript +{ + field: { + $not: { + < operator - expression > + } + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'boolean-expression/'. Content scraped from 'logical-query/'. + +### $or + +- **Description:** The $or operator joins query clauses with a logical OR and returns documents that match at least one of the specified conditions. +- **Syntax:** + +```javascript +{ + $or: [{ + < expression1 > + }, { + < expression2 > + }, ..., { + < expressionN > + }] +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'boolean-expression/'. Content scraped from 'logical-query/'. + +## Comparison Expression Operators + +### $cmp + +- **Description:** The $cmp operator compares two values +- **Syntax:** + +```javascript +{ + $cmp: [, ] +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'comparison-expression/'. Content scraped from 'comparison-query/'. + +### $eq + +- **Description:** The $eq query operator compares the value of a field to a specified value +- **Syntax:** + +```javascript +{ + field: { + $eq: + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'comparison-expression/'. Content scraped from 'comparison-query/'. + +### $gt + +- **Description:** The $gt query operator retrieves documents where the value of a field is greater than a specified value +- **Syntax:** + +```javascript +{ + field: { + $gt: value; + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'comparison-expression/'. Content scraped from 'comparison-query/'. + +### $gte + +- **Description:** The $gte operator retrieves documents where the value of a field is greater than or equal to a specified value +- **Syntax:** + +```javascript +{ + field: { + $gte: + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'comparison-expression/'. Content scraped from 'comparison-query/'. + +### $lt + +- **Description:** The $lt operator retrieves documents where the value of field is less than a specified value +- **Syntax:** + +```javascript +{ + field: { + $lt: value; + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'comparison-expression/'. Content scraped from 'comparison-query/'. + +### $lte + +- **Description:** The $lte operator retrieves documents where the value of a field is less than or equal to a specified value +- **Syntax:** + +```javascript +{ + field: { + $lte: + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'comparison-expression/'. Content scraped from 'comparison-query/'. + +### $ne + +- **Description:** The $ne operator retrieves documents where the value of a field doesn't equal a specified value +- **Syntax:** + +```javascript +{ + field: { + $ne: value; + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'comparison-expression/'. Content scraped from 'comparison-query/'. + +## Data Size Operators + +### $bsonSize + +- **Description:** The $bsonSize operator returns the size of a document in bytes when encoded as BSON. +- **Syntax:** + +```javascript +{ + $bsonSize: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/data-size/$bsonsize + +### $binarySize + +- **Description:** The $binarySize operator is used to return the size of a binary data field. +- **Syntax:** + +```javascript +{ + $binarySize: ''; +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/data-size/$binarysize + +## Date Expression Operators + +### $dateAdd + +- **Description:** The $dateAdd operator adds a specified number of time units (day, hour, month etc) to a date. +- **Syntax:** + +```javascript +$dateAdd: { + startDate: , + unit: , + amount: , + timezone: // Optional +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$dateadd + +### $dateDiff + +- **Description:** The $dateDiff operator calculates the difference between two dates in various units such as years, months, days, etc. +- **Syntax:** + +```javascript +$dateDiff: { + startDate: , + endDate: , + unit: , + timezone: , // Optional + startOfWeek: // Optional +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datediff + +### $dateFromParts + +- **Description:** The $dateFromParts operator constructs a date from individual components. +- **Syntax:** + +```javascript +{ + $dateFromParts: { + year: < year > , + month: < month > , + day: < day > , + hour: < hour > , + minute: < minute > , + second: < second > , + millisecond: < millisecond > , + timezone: < timezone > + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datefromparts + +### $dateFromString + +- **Description:** The $dateDiff operator converts a date/time string to a date object. +- **Syntax:** + +```javascript +{ + $dateFromString: { + dateString: < string > , + format: < string > , + timezone: < string > , + onError: < expression > , + onNull: < expression > + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datefromstring + +### $dateSubtract + +- **Description:** The $dateSubtract operator subtracts a specified amount of time from a date. +- **Syntax:** + +```javascript +{ + $dateSubtract: { + startDate: , + unit: "", + amount: , + timezone: "" // optional + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datesubtract + +### $dateToParts + +- **Description:** The $dateToParts operator decomposes a date into its individual parts such as year, month, day, and more. +- **Syntax:** + +```javascript +$dateToParts: { + date: , + timezone: , // optional + iso8601: // optional +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datetoparts + +### $dateToString + +- **Description:** The $dateToString operator converts a date object into a formatted string. +- **Syntax:** + +```javascript +{ + $dateToString: { + format: "", + date: , + timezone: "", + onNull: "" + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datetostring + +### $dateTrunc + +- **Description:** The $dateTrunc operator truncates a date to a specified unit. +- **Syntax:** + +```javascript +$dateTrunc: { + date: , + unit: "", + binSize: , // optional + timezone: "", // optional + startOfWeek: "" // optional (used when unit is "week") + } +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datetrunc + +### $dayOfMonth + +- **Description:** The $dayOfMonth operator extracts the day of the month from a date. +- **Syntax:** + +```javascript +{ + $dayOfMonth: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$dayofmonth + +### $dayOfWeek + +- **Description:** The $dayOfWeek operator extracts the day of the week from a date. +- **Syntax:** + +```javascript +{ + $dayOfWeek: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$dayofweek + +### $dayOfYear + +- **Description:** The $dayOfYear operator extracts the day of the year from a date. +- **Syntax:** + +```javascript +{ + $dayOfYear: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$dayofyear + +### $hour + +- **Description:** The $hour operator returns the hour portion of a date as a number between 0 and 23. +- **Syntax:** + +```javascript +{ + $hour: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$hour + +### $isoDayOfWeek + +- **Description:** The $isoDayOfWeek operator returns the weekday number in ISO 8601 format, ranging from 1 (Monday) to 7 (Sunday). +- **Syntax:** + +```javascript +{ + $isoDayOfWeek: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$isodayofweek + +### $isoWeek + +- **Description:** The $isoWeek operator returns the week number of the year in ISO 8601 format, ranging from 1 to 53. +- **Syntax:** + +```javascript +{ + $isoWeek: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$isoweek + +### $isoWeekYear + +- **Description:** The $isoWeekYear operator returns the year number in ISO 8601 format, which can differ from the calendar year for dates at the beginning or end of the year. +- **Syntax:** + +```javascript +{ + $isoWeekYear: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$isoweekyear + +### $millisecond + +- **Description:** The $millisecond operator extracts the milliseconds portion from a date value. +- **Syntax:** + +```javascript +{ + $millisecond: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$millisecond + +### $minute + +- **Description:** The $minute operator extracts the minute portion from a date value. +- **Syntax:** + +```javascript +{ + $minute: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$minute + +### $month + +- **Description:** The $month operator extracts the month portion from a date value. +- **Syntax:** + +```javascript +{ + $month: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$month + +### $second + +- **Description:** The $second operator extracts the seconds portion from a date value. +- **Syntax:** + +```javascript +{ + $second: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$second + +### $toDate + +- **Description:** The $toDate operator converts supported types to a proper Date object. +- **Syntax:** + +```javascript +{ + $toDate: +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'date-expression/'. Content scraped from 'aggregation/type-expression/'. + +### $week + +- **Description:** The $week operator returns the week number for a date as a value between 0 and 53. +- **Syntax:** + +```javascript +{ + $week: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$week + +### $year + +- **Description:** The $year operator returns the year for a date as a four-digit number. +- **Syntax:** + +```javascript +{ + $year: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$year + +## Literal Expression Operator + +### $literal + +- **Description:** The $literal operator returns the specified value without parsing it as an expression, allowing literal values to be used in aggregation pipelines. +- **Syntax:** + +```javascript +{ + $literal: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/literal-expression/$literal + +## Miscellaneous Operators + +### $getField + +- **Description:** The $getField operator allows retrieving the value of a specified field from a document. +- **Syntax:** + +```javascript +{ + $getField: { + field: , + input: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/miscellaneous/$getfield + +### $rand + +- **Description:** The $rand operator generates a random float value between 0 and 1. +- **Syntax:** + +```javascript +{ + $rand: { + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'miscellaneous/'. Content scraped from 'miscellaneous-query/'. + +### $sampleRate + +- **Description:** The $sampleRate operator randomly samples documents from a collection based on a specified probability rate, useful for statistical analysis and testing. +- **Syntax:** + +```javascript +{ + $match: { + $sampleRate: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/miscellaneous/$samplerate + +## Object Expression Operators + +### $mergeObjects + +- **Description:** The $mergeObjects operator merges multiple documents into a single document +- **Syntax:** + +```javascript +{ + $mergeObjects: [ < document1 > , < document2 > , ...] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/object-expression/$mergeobjects + +### $objectToArray + +- **Description:** The objectToArray command is used to transform a document (object) into an array of key-value pairs. +- **Syntax:** + +```javascript +{ + $objectToArray: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/object-expression/$objecttoarray + +### $setField + +- **Description:** The setField command is used to add, update, or remove fields in embedded documents. +- **Syntax:** + +```javascript +{ + $setField: { + field: , + input: , + value: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/object-expression/$setfield + +## Set Expression Operators + +### $allElementsTrue + +- **Description:** The $allElementsTrue operator returns true if all elements in an array evaluate to true. +- **Syntax:** + +```javascript +{ + $allElementsTrue: [ ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/set-expression/$allelementstrue + +### $anyElementTrue + +- **Description:** The $anyElementTrue operator returns true if any element in an array evaluates to a value of true. +- **Syntax:** + +```javascript +{ + $anyElementTrue: [ ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/set-expression/$anyelementtrue + +### $setDifference + +- **Description:** The $setDifference operator returns a set with elements that exist in one set but not in a second set. +- **Syntax:** + +```javascript +{ + $setDifference: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/set-expression/$setdifference + +### $setEquals + +- **Description:** The $setEquals operator returns true if two sets have the same distinct elements. +- **Syntax:** + +```javascript +{ + $setEquals: [ , , ... ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/set-expression/$setequals + +### $setIntersection + +- **Description:** The $setIntersection operator returns the common elements that appear in all input arrays. +- **Syntax:** + +```javascript +{ + $setIntersection: [ , , ... ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/set-expression/$setintersection + +### $setIsSubset + +- **Description:** The $setIsSubset operator determines if one array is a subset of a second array. +- **Syntax:** + +```javascript +{ + $setIsSubset: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/set-expression/$setissubset + +### $setUnion + +- **Description:** The $setUnion operator returns an array that contains all the unique elements from the input arrays. +- **Syntax:** + +```javascript +{ + $setUnion: [ , , ... ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/set-expression/$setunion + +## String Expression Operators + +### $concat + +### $dateFromString + +- **Description:** The $dateDiff operator converts a date/time string to a date object. +- **Syntax:** + +```javascript +{ + $dateFromString: { + dateString: < string > , + format: < string > , + timezone: < string > , + onError: < expression > , + onNull: < expression > + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'string-expression/'. Content scraped from 'date-expression/'. + +### $dateToString + +- **Description:** The $dateToString operator converts a date object into a formatted string. +- **Syntax:** + +```javascript +{ + $dateToString: { + format: "", + date: , + timezone: "", + onNull: "" + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'string-expression/'. Content scraped from 'date-expression/'. + +### $indexOfBytes + +### $indexOfCP + +### $ltrim + +### $regexFind + +### $regexFindAll + +### $regexMatch + +### $replaceOne + +### $replaceAll + +### $rtrim + +### $split + +### $strLenBytes + +### $strLenCP + +### $strcasecmp + +### $substr + +### $substrBytes + +### $substrCP + +### $toLower + +### $toString + +- **Description:** The $toString operator converts an expression into a String +- **Syntax:** + +```javascript +{ + $toString: < expression > +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'string-expression/'. Content scraped from 'aggregation/type-expression/'. + +### $trim + +### $toUpper + +## Timestamp Expression Operators + +### $tsIncrement + +- **Description:** The $tsIncrement operator extracts the increment portion from a timestamp value. +- **Syntax:** + +```javascript +{ + $tsIncrement: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/timestamp-expression/$tsincrement + +### $tsSecond + +- **Description:** The $tsSecond operator extracts the seconds portion from a timestamp value. +- **Syntax:** + +```javascript +{ + $tsSecond: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/timestamp-expression/$tssecond + +## Trigonometry Expression Operators + +### $sin + +### $cos + +### $tan + +### $asin + +### $acos + +### $atan + +### $atan2 + +### $asinh + +### $acosh + +### $atanh + +### $sinh + +### $cosh + +### $tanh + +### $degreesToRadians + +### $radiansToDegrees + +## Type Expression Operators + +### $convert + +- **Description:** The $convert operator converts an expression into the specified type +- **Syntax:** + +```javascript +{ + $convert: { + input: < expression > , + to: < type > , + format: < binData format > , + onError: < value to return on error > , + onNull: < value to return on null > + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$convert + +### $isNumber + +- **Description:** The $isNumber operator checks if a specified expression is a numerical type +- **Syntax:** + +```javascript +{ + $isNumber: < expression > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$isnumber + +### $toBool + +- **Description:** The $toBool operator converts an expression into a Boolean type +- **Syntax:** + +```javascript +{ + $toBool: < expression > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$tobool + +### $toDate + +- **Description:** The $toDate operator converts supported types to a proper Date object. +- **Syntax:** + +```javascript +{ + $toDate: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$todate + +### $toDecimal + +- **Description:** The $toDecimal operator converts an expression into a Decimal type +- **Syntax:** + +```javascript +{ + $toDecimal: < expression > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$todecimal + +### $toDouble + +- **Description:** The $toDouble operator converts an expression into a Double value +- **Syntax:** + +```javascript +{ + $toDouble: < expression > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$todouble + +### $toInt + +- **Description:** The $toInt operator converts an expression into an Integer +- **Syntax:** + +```javascript +{ + $toInt: < expression > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$toint + +### $toLong + +- **Description:** The $toLong operator converts an expression into a Long value +- **Syntax:** + +```javascript +{ + $toLong: < expression > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$tolong + +### $toObjectId + +- **Description:** The $toObjectId operator converts an expression into an ObjectId +- **Syntax:** + +```javascript +{ + $toObject: < expression > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$toobjectid + +### $toString + +- **Description:** The $toString operator converts an expression into a String +- **Syntax:** + +```javascript +{ + $toString: < expression > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$tostring + +### $type + +- **Description:** The $type operator retrieves documents if the chosen field is of the specified type. +- **Syntax:** + +```javascript +{ + : { $type: | } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'aggregation/type-expression/'. Content scraped from 'element-query/'. + +## Accumulators ($group, $bucket, $bucketAuto, $setWindowFields) + +### $addToSet + +- **Description:** The addToSet operator adds elements to an array if they don't already exist, while ensuring uniqueness of elements within the set. +- **Syntax:** + +```javascript +{ + $addToSet: { : } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'accumulators/'. Content scraped from 'array-update/'. + +### $avg + +- **Description:** Computes the average of numeric values for documents in a group, bucket, or window. +- **Syntax:** + +```javascript +$avg: +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$avg + +### $bottom + +- **Description:** The $bottom operator returns the last document from the query's result set sorted by one or more fields +- **Syntax:** + +```javascript +{ + $bottom: { + output: [listOfFields], + sortBy: { + : < sortOrder > + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$bottom + +### $bottomN + +- **Description:** The $bottomN operator returns the last N documents from the result sorted by one or more fields +- **Syntax:** + +```javascript +{ + $bottomN: { + output: [listOfFields], + sortBy: { + : < sortOrder > + }, + n: < numDocumentsToReturn > + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$bottomn + +### $count + +- **Description:** The `$count` operator is used to count the number of documents that match a query filtering criteria. +- **Syntax:** + +```javascript +{ + $count: ''; +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$count + +### $first + +- **Description:** The $first operator returns the first value in a group according to the group's sorting order. +- **Syntax:** + +```javascript +{ + $first: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$first + +### $firstN + +- **Description:** The $firstN operator sorts documents on one or more fields specified by the query and returns the first N document matching the filtering criteria +- **Syntax:** + +```javascript +{ + $firstN: { + input: [listOfFields], + sortBy: { + : + }, + n: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$firstn + +### $last + +- **Description:** The $last operator returns the last document from the result sorted by one or more fields +- **Syntax:** + +```javascript +{ + "$last": +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$last + +### $lastN + +- **Description:** The $lastN accumulator operator returns the last N values in a group of documents. +- **Syntax:** + +```javascript +{ + $group: { + _id: < expression > , + < field >: { + $lastN: { + n: < number >, + input: < expression > + } + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$lastn + +### $max + +- **Description:** The $max operator returns the maximum value from a set of input values. +- **Syntax:** + +```javascript +$max: +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$max + +### $maxN + +- **Description:** Retrieves the top N values based on a specified filtering criteria +- **Syntax:** + +```javascript +$maxN: { + input: < field or expression > , + n: < number of values to retrieve > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$maxn + +### $median + +- **Description:** The $median operator calculates the median value of a numeric field in a group of documents. +- **Syntax:** + +```javascript +{ + $group: { + _id: < expression > , + medianValue: { + $median: { + input: < field or expression > , + method: < > + } + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$median + +### $mergeObjects + +- **Description:** The $mergeObjects operator merges multiple documents into a single document +- **Syntax:** + +```javascript +{ + $mergeObjects: [ < document1 > , < document2 > , ...] +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'accumulators/'. Content scraped from 'object-expression/'. + +### $min + +- **Description:** Retrieves the minimum value for a specified field +- **Syntax:** + +```javascript +$min: +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$min + +### $percentile + +- **Description:** The $percentile operator calculates the percentile of numerical values that match a filtering criteria +- **Syntax:** + +```javascript +$percentile: { + input: < field or expression > , + p: [ < percentile values > ], + method: < method > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$percentile + +### $push + +- **Description:** The $push operator adds a specified value to an array within a document. +- **Syntax:** + +```javascript +db.collection.update({ + < query > +}, { + $push: { + < field >: < value > + } +}, { + < options > +}) +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'accumulators/'. Content scraped from 'array-update/'. + +### $stdDevPop + +- **Description:** The $stddevpop operator calculates the standard deviation of the specified values +- **Syntax:** + +```javascript +{ + $stddevpop: { + fieldName; + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$stddevpop + +### $stdDevSamp + +- **Description:** The $stddevsamp operator calculates the standard deviation of a specified sample of values and not the entire population +- **Syntax:** + +```javascript +{ + $stddevsamp: { + fieldName; + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$stddevsamp + +### $sum + +- **Description:** The $sum operator calculates the sum of the values of a field based on a filtering criteria +- **Syntax:** + +```javascript +{ + $sum: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$sum + +### $top + +- **Description:** The $top operator returns the first document from the result set sorted by one or more fields +- **Syntax:** + +```javascript +{ + $top: { + output: [listOfFields], + sortBy: { + < fieldName >: < sortOrder > + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$top + +### $topN + +- **Description:** The $topN operator returns the first N documents from the result sorted by one or more fields +- **Syntax:** + +```javascript +{ + $topN: { + output: [listOfFields], + sortBy: { + : < sortOrder > + }, + n: < numDocumentsToReturn > + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$topn + +## Accumulators (in Other Stages) + +### $avg + +- **Description:** Computes the average of numeric values for documents in a group, bucket, or window. +- **Syntax:** + +```javascript +$avg: +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$avg + +### $first + +- **Description:** The $first operator returns the first value in a group according to the group's sorting order. +- **Syntax:** + +```javascript +{ + $first: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$first + +### $last + +- **Description:** The $last operator returns the last document from the result sorted by one or more fields +- **Syntax:** + +```javascript +{ + "$last": +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$last + +### $max + +- **Description:** The $max operator returns the maximum value from a set of input values. +- **Syntax:** + +```javascript +$max: +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$max + +### $median + +- **Description:** The $median operator calculates the median value of a numeric field in a group of documents. +- **Syntax:** + +```javascript +{ + $group: { + _id: < expression > , + medianValue: { + $median: { + input: < field or expression > , + method: < > + } + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$median + +### $min + +- **Description:** Retrieves the minimum value for a specified field +- **Syntax:** + +```javascript +$min: +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$min + +### $percentile + +- **Description:** The $percentile operator calculates the percentile of numerical values that match a filtering criteria +- **Syntax:** + +```javascript +$percentile: { + input: < field or expression > , + p: [ < percentile values > ], + method: < method > +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$percentile + +### $stdDevPop + +- **Description:** The $stddevpop operator calculates the standard deviation of the specified values +- **Syntax:** + +```javascript +{ + $stddevpop: { + fieldName; + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$stddevpop + +### $stdDevSamp + +- **Description:** The $stddevsamp operator calculates the standard deviation of a specified sample of values and not the entire population +- **Syntax:** + +```javascript +{ + $stddevsamp: { + fieldName; + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$stddevsamp + +### $sum + +- **Description:** The $sum operator calculates the sum of the values of a field based on a filtering criteria +- **Syntax:** + +```javascript +{ + $sum: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$sum + +## Variable Expression Operators + +### $let + +- **Description:** The $let operator allows defining variables for use in a specified expression, enabling complex calculations and reducing code repetition. +- **Syntax:** + +```javascript +{ + $let: { + vars: { + : , + : , + ... + }, + in: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/variable-expression/$let + +## Window Operators + +### $sum + +- **Description:** The $sum operator calculates the sum of the values of a field based on a filtering criteria +- **Syntax:** + +```javascript +{ + $sum: +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $push + +- **Description:** The $push operator adds a specified value to an array within a document. +- **Syntax:** + +```javascript +db.collection.update({ + < query > +}, { + $push: { + < field >: < value > + } +}, { + < options > +}) +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'array-update/'. + +### $addToSet + +- **Description:** The addToSet operator adds elements to an array if they don't already exist, while ensuring uniqueness of elements within the set. +- **Syntax:** + +```javascript +{ + $addToSet: { : } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'array-update/'. + +### $count + +- **Description:** The `$count` operator is used to count the number of documents that match a query filtering criteria. +- **Syntax:** + +```javascript +{ + $count: ''; +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $max + +- **Description:** The $max operator returns the maximum value from a set of input values. +- **Syntax:** + +```javascript +$max: +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $min + +- **Description:** Retrieves the minimum value for a specified field +- **Syntax:** + +```javascript +$min: +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $avg + +- **Description:** Computes the average of numeric values for documents in a group, bucket, or window. +- **Syntax:** + +```javascript +$avg: +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $stdDevPop + +- **Description:** The $stddevpop operator calculates the standard deviation of the specified values +- **Syntax:** + +```javascript +{ + $stddevpop: { + fieldName; + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $bottom + +- **Description:** The $bottom operator returns the last document from the query's result set sorted by one or more fields +- **Syntax:** + +```javascript +{ + $bottom: { + output: [listOfFields], + sortBy: { + : < sortOrder > + } + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $bottomN + +- **Description:** The $bottomN operator returns the last N documents from the result sorted by one or more fields +- **Syntax:** + +```javascript +{ + $bottomN: { + output: [listOfFields], + sortBy: { + : < sortOrder > + }, + n: < numDocumentsToReturn > + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $covariancePop + +- **Description:** The $covariancePop operator returns the covariance of two numerical expressions +- **Syntax:** + +```javascript +{ + $covariancePop: [ < numericalExpression1 > , < numericalExpression2 > ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$covariancepop + +### $covarianceSamp + +- **Description:** The $covarianceSamp operator returns the covariance of a sample of two numerical expressions +- **Syntax:** + +```javascript +{ + $covarianceSamp: [ < numericalExpression1 > , < numericalExpression2 > ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$covariancesamp + +### $denseRank + +- **Description:** The $denseRank operator assigns and returns a positional ranking for each document within a partition based on a specified sort order +- **Syntax:** + +```javascript +{ + $denseRank: { + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$denserank + +### $derivative + +- **Description:** The $derivative operator calculates the average rate of change of the value of a field within a specified window. +- **Syntax:** + +```javascript +{ + $derivative: { + input: < expression >, + unit: < timeWindow > + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$derivative + +### $documentNumber + +- **Description:** The $documentNumber operator assigns and returns a position for each document within a partition based on a specified sort order +- **Syntax:** + +```javascript +{ + $documentNumber: { + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$documentnumber + +### $expMovingAvg + +- **Description:** The $expMovingAvg operator calculates the moving average of a field based on the specified number of documents to hold the highest weight +- **Syntax:** + +```javascript +{ + $expMovingAvg: { + input: < field to use for calculation >, + N: < number of recent documents with the highest weight + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$expmovingavg + +### $first + +- **Description:** The $first operator returns the first value in a group according to the group's sorting order. +- **Syntax:** + +```javascript +{ + $first: +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $integral + +- **Description:** The $integral operator calculates the area under a curve with the specified range of documents forming the adjacent documents for the calculation. +- **Syntax:** + +```javascript +{ + $integral: { + input: < expression > , + unit: < time window > + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$integral + +### $last + +- **Description:** The $last operator returns the last document from the result sorted by one or more fields +- **Syntax:** + +```javascript +{ + "$last": +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $linearFill + +- **Description:** The $linearFill operator interpolates missing values in a sequence of documents using linear interpolation. +- **Syntax:** + +```javascript +{ + $linearFill: { + input: < expression > , + sortBy: { + < field >: < 1 or - 1 > + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$linearfill + +### $locf + +- **Description:** The $locf operator propagates the last observed non-null value forward within a partition in a windowed query. +- **Syntax:** + +```javascript +{ + $locf: { + input: , + sortBy: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$locf + +### $minN + +- **Description:** Retrieves the bottom N values based on a specified filtering criteria +- **Syntax:** + +```javascript +$minN: { + input: < field or expression > , + n: < number of values to retrieve > +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $rank + +- **Description:** The $rank operator ranks documents within a partition based on a specified sort order. +- **Syntax:** + +```javascript +{ + $setWindowFields: { + partitionBy: < expression > , + sortBy: { + < field >: < order > + }, + output: { + < outputField >: { + $rank: {} + } + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$rank + +### $shift + +- **Description:** A window operator that shifts values within a partition and returns the shifted value. +- **Syntax:** + +```javascript +{ + $shift: { + output: , + by: , + default: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$shift + +### $stdDevSamp + +- **Description:** The $stddevsamp operator calculates the standard deviation of a specified sample of values and not the entire population +- **Syntax:** + +```javascript +{ + $stddevsamp: { + fieldName; + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $top + +- **Description:** The $top operator returns the first document from the result set sorted by one or more fields +- **Syntax:** + +```javascript +{ + $top: { + output: [listOfFields], + sortBy: { + < fieldName >: < sortOrder > + } + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +### $topN + +- **Description:** The $topN operator returns the first N documents from the result sorted by one or more fields +- **Syntax:** + +```javascript +{ + $topN: { + output: [listOfFields], + sortBy: { + : < sortOrder > + }, + n: < numDocumentsToReturn > + } +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'window-operators/'. Content scraped from 'accumulators/'. + +## Conditional Expression Operators + +### $cond + +- **Description:** The $cond operator is used to evaluate a condition and return one of two expressions based on the result. +- **Syntax:** + +```javascript +{ + $cond: { + if: , + then: , + else: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/conditional-expression/$cond + +### $ifNull + +- **Description:** The $ifNull operator is used to evaluate an expression and return a specified value if the expression resolves to null. +- **Syntax:** + +```javascript +{ + $ifNull: [ , ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/conditional-expression/$ifnull + +### $switch + +- **Description:** The $switch operator is used to evaluate a series of conditions and return a value based on the first condition that evaluates to true. +- **Syntax:** + +```javascript +{ + $switch: { + branches: [ + { case: , then: }, + { case: , then: } + ], + default: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/conditional-expression/$switch + +## Aggregation Pipeline Stages + +### $addFields + +- **Description:** The $addFields stage in the aggregation pipeline is used to add new fields to documents. +- **Syntax:** + +```javascript +{ + $addFields: { + : , + : , + ... + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$addfields + +### $bucket + +- **Description:** Groups input documents into buckets based on specified boundaries. +- **Syntax:** + +```javascript +{ + $bucket: { + groupBy: , + boundaries: [ , , ... ], + default: , + output: { + : { }, + ... + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$bucket + +### $bucketAuto + +### $changeStream + +- **Description:** The $changeStream stage opens a change stream cursor to track data changes in real-time. +- **Syntax:** + +```javascript +{ + $changeStream: { + allChangesForCluster: , + fullDocument: , + fullDocumentBeforeChange: , + resumeAfter: , + startAfter: , + startAtOperationTime: , + showExpandedEvents: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$changestream + +### $collStats + +- **Description:** The $collStats stage in the aggregation pipeline is used to return statistics about a collection. +- **Syntax:** + +```javascript +{ + $collStats: { + latencyStats: { histograms: }, + storageStats: { scale: }, + count: {} + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$collstats + +### $count + +- **Description:** The `$count` operator is used to count the number of documents that match a query filtering criteria. +- **Syntax:** + +```javascript +{ + $count: ''; +} +``` + +- **Doc Link:** none +- **Scraper Comment:** Doc page not found in expected directory 'aggregation/'. Content scraped from 'accumulators/'. + +### $densify + +- **Description:** Adds missing data points in a sequence of values within an array or collection. +- **Syntax:** + +```javascript +{ + $densify: { + field: , + range: { + step: , + unit: , // Optional, e.g., "hour", "day", "month", etc. + bounds: [, ] // Optional + }, + partitionByFields: [, , ...] // Optional + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$densify + +### $documents + +- **Description:** The $documents stage creates a pipeline from a set of provided documents. +- **Syntax:** + +```javascript +{ + $documents: [ + , + , + ... + ] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$documents + +### $facet + +- **Description:** The $facet allows for multiple parallel aggregations to be executed within a single pipeline stage. +- **Syntax:** + +```javascript +{ + "$facet": { + "outputField1": [ { "stage1": {} }, { "stage2": {} } ], + "outputField2": [ { "stage1": {} }, { "stage2": {} } ] + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$facet + +### $fill + +- **Description:** The $fill stage allows filling missing values in documents based on specified methods and criteria. +- **Syntax:** + +```javascript +{ + $fill: { + sortBy: , + partitionBy: , + partitionByFields: , + output: { + : { value: }, + : { method: } + } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$fill + +### $geoNear + +- **Description:** The $geoNear operator finds and sorts documents by their proximity to a geospatial point, returning distance information for each document. +- **Syntax:** + +```javascript +{ + $geoNear: { + near: { + type: "Point", + coordinates: [, ] + }, + distanceField: , + maxDistance: , + minDistance: , + query: , + includeLocs: , + distanceMultiplier: , + spherical: , + key: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$geonear + +### $graphLookup + +### $group + +- **Description:** The $group stage groups documents by specified identifier expressions and applies accumulator expressions. +- **Syntax:** + +```javascript +{ + $group: { + _id: , + : { : }, + : { : } + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$group + +### $indexStats + +- **Description:** The $indexStats stage returns usage statistics for each index in the collection. +- **Syntax:** + +```javascript +{ + $indexStats: { + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$indexstats + +### $limit + +### $lookup + +- **Description:** The $lookup stage in the Aggregation Framework is used to perform left outer joins with other collections. +- **Syntax:** + +```javascript +{ + $lookup: { + from: , + localField: , + foreignField: , + as: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$lookup + +### $match + +- **Description:** The $match stage in the aggregation pipeline is used to filter documents that match a specified condition. +- **Syntax:** + +```javascript +{ + $match: { + + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$match + +### $merge + +- **Description:** The $merge stage in an aggregation pipeline writes the results of the aggregation to a specified collection. +- **Syntax:** + +```javascript +{ + $merge: { + into: , + on: , + whenMatched: , + whenNotMatched: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$merge + +### $out + +- **Description:** The `$out` stage in an aggregation pipeline writes the resulting documents to a specified collection. +- **Syntax:** + +```javascript +{ + $out: ''; +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$out + +### $project + +### $redact + +- **Description:** Filters the content of the documents based on access rights. +- **Syntax:** + +```javascript +{ + $redact: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$redact + +### $replaceRoot + +### $replaceWith + +- **Description:** The $replaceWith operator in Azure DocumentDB returns a document after replacing a document with the specified document +- **Syntax:** + +```javascript +{ + "$replaceWith": +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$replacewith + +### $sample + +- **Description:** The $sample operator in Azure DocumentDB returns a randomly selected number of documents +- **Syntax:** + +```javascript +{ + $sample: { size: } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$sample + +### $search + +### $searchMeta + +### $set + +- **Description:** The $set operator in Azure DocumentDB updates or creates a new field with a specified value +- **Syntax:** + +```javascript +{ + $set: { + newField: + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$set + +### $setWindowFields + +### $skip + +- **Description:** The $skip stage in the aggregation pipeline is used to skip a specified number of documents from the input and pass the remaining documents to the next stage in the pipeline. +- **Syntax:** + +```javascript +{ + $skip: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$skip + +### $sort + +- **Description:** The $sort stage in the aggregation pipeline is used to order the documents in the pipeline by a specified field or fields. +- **Syntax:** + +```javascript +{ + $sort: { + < field1 >: < sort order > , + < field2 >: < sort order > + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$sort + +### $sortByCount + +- **Description:** The $sortByCount stage in the aggregation pipeline is used to group documents by a specified expression and then sort the count of documents in each group in descending order. +- **Syntax:** + +```javascript +{ + $sortByCount: +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$sortbycount + +### $unionWith + +### $unset + +- **Description:** The $unset stage in the aggregation pipeline is used to remove specified fields from documents. +- **Syntax:** + +```javascript +{ + $unset: "" | ["", "", ...] +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$unset + +### $unwind + +- **Description:** The $unwind stage in the aggregation framework is used to deconstruct an array field from the input documents to output a document for each element. +- **Syntax:** + +```javascript +{ + $unwind: { + path: , + includeArrayIndex: , // Optional + preserveNullAndEmptyArrays: // Optional + } +} +``` + +- **Doc Link:** https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$unwind + +### $currentOp + +## Variables in Aggregation Expressions + +### $$NOW + +### $$ROOT + +### $$REMOVE + +### $$CURRENT + +### $$DESCEND + +### $$PRUNE + +### $$KEEP + +## Not Listed + +Operators below are present on the compatibility page but are not in scope +for this package (deprecated or not available in DocumentDB). + +- **$where** (Evaluation Query Operators) โ€” Deprecated in Mongo version 8.0 +- **$meta** (Projection Operators) โ€” Not in scope +- **$accumulator** (Custom Aggregation Expression Operators) โ€” Deprecated in Mongo version 8.0 +- **$function** (Custom Aggregation Expression Operators) โ€” Deprecated in Mongo version 8.0 +- **$meta** (Text Expression Operator) โ€” Not in scope +- **$accumulator** (Accumulators ($group, $bucket, $bucketAuto, $setWindowFields)) โ€” Deprecated in Mongo version 8.0 +- **$changeStreamSplitLargeEvent** (Aggregation Pipeline Stages) โ€” Not in scope +- **$listSampledQueries** (Aggregation Pipeline Stages) โ€” Not in scope +- **$listSearchIndexes** (Aggregation Pipeline Stages) โ€” Not in scope +- **$listSessions** (Aggregation Pipeline Stages) โ€” Not in scope +- **$planCacheStats** (Aggregation Pipeline Stages) โ€” Not in scope +- **$shardedDataDistribution** (Aggregation Pipeline Stages) โ€” Not in scope +- **$listLocalSessions** (Aggregation Pipeline Stages) โ€” Not in scope +- **$$CLUSTER_TIME** (Variables in Aggregation Expressions) โ€” Not in scope +- **$$SEARCH_META** (Variables in Aggregation Expressions) โ€” Not in scope +- **$$USER_ROLES** (Variables in Aggregation Expressions) โ€” Not in scope diff --git a/packages/documentdb-constants/scripts/README.md b/packages/documentdb-constants/scripts/README.md new file mode 100644 index 000000000..d642ecb08 --- /dev/null +++ b/packages/documentdb-constants/scripts/README.md @@ -0,0 +1,97 @@ +# Scripts + +Helper scripts for maintaining the `@vscode-documentdb/documentdb-constants` package. + +## scrape-operator-docs.ts + +Scrapes the DocumentDB compatibility page and per-operator documentation to produce `resources/scraped/operator-reference.md`. + +```bash +npm run scrape +``` + +**When to run:** When the upstream DocumentDB documentation changes (new operators, updated descriptions, etc.). This is infrequent โ€” typically once per DocumentDB release. + +**Output:** `resources/scraped/operator-reference.md` โ€” a machine-generated Markdown dump of all supported operators, their descriptions, syntax blocks, and doc links. + +## generate-from-reference.ts + +Reads the scraped dump, hand-maintained overrides file, and snippet templates, then generates the TypeScript operator data files in `src/`. + +```bash +npm run generate +``` + +**When to run:** + +- After running the scraper (`npm run scrape`) +- After editing `resources/overrides/operator-overrides.md` +- After editing `resources/overrides/operator-snippets.md` + +**Inputs:** + +| File | Purpose | +| ------------------------------------------- | ---------------------------------- | +| `resources/scraped/operator-reference.md` | Primary data (machine-generated) | +| `resources/overrides/operator-overrides.md` | Manual overrides (hand-maintained) | +| `resources/overrides/operator-snippets.md` | Snippet templates per category | + +**Outputs:** Seven TypeScript files in `src/`: + +- `queryOperators.ts` โ€” comparison, logical, element, evaluation, geospatial, array, bitwise, projection, misc query operators +- `updateOperators.ts` โ€” field, array, and bitwise update operators +- `expressionOperators.ts` โ€” arithmetic, array, bitwise, boolean, comparison, conditional, data-size, date, literal, misc, object, set, string, timestamp, trig, type, and variable expression operators +- `accumulators.ts` โ€” group and other-stage accumulators +- `windowOperators.ts` โ€” window function operators +- `stages.ts` โ€” aggregation pipeline stages +- `systemVariables.ts` โ€” system variables (`$$NOW`, `$$ROOT`, etc.) + +> **Do not edit the generated `src/` files by hand.** Put corrections in the overrides or snippets files instead. The generated files contain a header warning to this effect. + +## evaluate-overrides.ts + +Evaluates the relationship between scraped data, manual overrides, and snippet coverage. Produces a color-coded report. + +```bash +npm run evaluate +``` + +**When to run:** + +- After re-scraping (`npm run scrape`) to see if previously-missing descriptions are now available +- Periodically, to check coverage and detect redundant overrides + +**Report sections:** + +1. **GAPS** โ€” operators with empty scraped descriptions and no override (need attention) +2. **POTENTIALLY REDUNDANT** โ€” operators that have **both** a scraped description and an override description; the override may no longer be needed +3. **ACTIVE OVERRIDES** โ€” overrides filling real gaps, with both override and scraped values shown +4. **SNIPPET COVERAGE** โ€” operators with/without snippet templates per category +5. **SUMMARY** โ€” total counts and coverage percentage + +## Workflow + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Upstream docs change โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ–ผ + npm run scrape + โ”‚ + โ–ผ + scraped/operator-reference.md + โ”‚ + โ”œโ”€โ”€โ”€โ”€ npm run evaluate (check gaps, redundant overrides & snippet coverage) + โ”‚ + โ”œโ”€โ”€โ”€โ”€ overrides/operator-overrides.md (manual) + โ”œโ”€โ”€โ”€โ”€ overrides/operator-snippets.md (manual) + โ”‚ + โ–ผ + npm run generate + โ”‚ + โ–ผ + src/*.ts (generated) + โ”‚ + โ–ผ + npm run build +``` diff --git a/packages/documentdb-constants/scripts/evaluate-overrides.ts b/packages/documentdb-constants/scripts/evaluate-overrides.ts new file mode 100644 index 000000000..366bfc608 --- /dev/null +++ b/packages/documentdb-constants/scripts/evaluate-overrides.ts @@ -0,0 +1,598 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * evaluate-overrides.ts + * + * Evaluates the relationship between scraped operator data and manual overrides. + * Produces a report showing: + * + * 1. Operators with empty descriptions in the scrape AND no override + * (gaps that still need attention) + * 2. Operators that have overrides โ€” shows both the override value and the + * original scraped value so you can detect when an override is no longer + * needed (e.g. the upstream docs now have a description) + * 3. Summary statistics + * + * Usage: npm run evaluate + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +// --------------------------------------------------------------------------- +// Types (lightweight โ€” reuses the same Markdown format as the generator) +// --------------------------------------------------------------------------- + +interface ParsedEntry { + value: string; + description: string; + category: string; + docLink: string; +} + +interface OverrideEntry { + description?: string; + syntax?: string; + docLink?: string; + snippet?: string; +} + +// --------------------------------------------------------------------------- +// Parsers (simplified versions of the generator's parsers) +// --------------------------------------------------------------------------- + +function parseDump(content: string): ParsedEntry[] { + const lines = content.split('\n'); + const entries: ParsedEntry[] = []; + + let currentCategory = ''; + let currentOp: Partial | null = null; + let inCodeBlock = false; + + for (const line of lines) { + if (line.startsWith('```')) { + inCodeBlock = !inCodeBlock; + continue; + } + if (inCodeBlock) continue; + + const h2 = line.match(/^## (.+)$/); + if (h2) { + if (currentOp && currentCategory) { + entries.push({ + value: currentOp.value!, + description: currentOp.description || '', + category: currentCategory, + docLink: currentOp.docLink || '', + }); + } + currentOp = null; + const cat = h2[1].trim(); + if (cat === 'Summary' || cat === 'Not Listed') { + currentCategory = ''; + continue; + } + currentCategory = cat; + continue; + } + + const h3 = line.match(/^### (.+)$/); + if (h3 && currentCategory) { + if (currentOp) { + entries.push({ + value: currentOp.value!, + description: currentOp.description || '', + category: currentCategory, + docLink: currentOp.docLink || '', + }); + } + currentOp = { value: h3[1].trim(), description: '', docLink: '', category: currentCategory }; + continue; + } + + if (currentOp && line.startsWith('- **Description:**')) { + currentOp.description = line.replace('- **Description:**', '').trim(); + } + + // Parse doc link ('none' means scraper found no page at expected location) + if (currentOp && line.startsWith('- **Doc Link:**')) { + const rawLink = line.replace('- **Doc Link:**', '').trim(); + currentOp.docLink = rawLink === 'none' ? '' : rawLink; + } + } + + if (currentOp && currentCategory) { + entries.push({ + value: currentOp.value!, + description: currentOp.description || '', + category: currentCategory, + docLink: currentOp.docLink || '', + }); + } + + return entries; +} + +function parseOverrides(content: string): Map> { + const lines = content.split('\n'); + const result = new Map>(); + + let currentCategory = ''; + let currentOp: { value: string; entry: OverrideEntry } | null = null; + let inCodeBlock = false; + let syntaxLines: string[] = []; + + for (const line of lines) { + if (line.startsWith('```')) { + if (inCodeBlock) { + inCodeBlock = false; + if (currentOp) { + currentOp.entry.syntax = syntaxLines.join('\n').trim(); + } + syntaxLines = []; + continue; + } else { + inCodeBlock = true; + continue; + } + } + if (inCodeBlock) { + syntaxLines.push(line); + continue; + } + + const h2 = line.match(/^## (.+)$/); + if (h2) { + if (currentOp && currentCategory) { + saveOverride(result, currentCategory, currentOp); + } + currentOp = null; + currentCategory = h2[1].trim(); + continue; + } + + const h3 = line.match(/^### (.+)$/); + if (h3 && currentCategory) { + if (currentOp) { + saveOverride(result, currentCategory, currentOp); + } + currentOp = { value: h3[1].trim(), entry: {} }; + continue; + } + + if (currentOp) { + if (line.startsWith('- **Description:**')) { + currentOp.entry.description = line.replace('- **Description:**', '').trim(); + } + if (line.startsWith('- **Doc Link:**')) { + currentOp.entry.docLink = line.replace('- **Doc Link:**', '').trim(); + } + if (line.startsWith('- **Snippet:**')) { + let snippet = line.replace('- **Snippet:**', '').trim(); + if (snippet.startsWith('`') && snippet.endsWith('`')) { + snippet = snippet.slice(1, -1); + } + currentOp.entry.snippet = snippet; + } + } + } + + if (currentOp && currentCategory) { + saveOverride(result, currentCategory, currentOp); + } + + return result; +} + +function saveOverride( + map: Map>, + category: string, + op: { value: string; entry: OverrideEntry }, +): void { + if (!map.has(category)) map.set(category, new Map()); + map.get(category)!.set(op.value, op.entry); +} + +// --------------------------------------------------------------------------- +// Lookup helpers +// --------------------------------------------------------------------------- + +/** + * Find an override for a dump entry, mirroring how the generator resolves overrides. + * + * The generator's `applyOverrides` iterates override categories: + * 1. If the override category exists in the dump, it looks for the operator in that exact category. + * 2. If the override category does NOT exist in the dump, it falls back to cross-category search. + * + * So for a dump entry (operatorValue, category), an override matches only if: + * (a) The override is in the same category as the dump entry (exact match), OR + * (b) The override is in a category that doesn't exist in the dump at all, and no + * earlier dump category already claimed this operator via cross-category fallback. + * + * We pass `dumpCategories` (all category names in the dump) to distinguish (a) from (b). + */ +function findOverride( + overrides: Map>, + operatorValue: string, + category: string, + dumpCategories: Set, +): { override: OverrideEntry; overrideCategory: string } | undefined { + // Exact category match: override category === dump entry category + const catOverrides = overrides.get(category); + if (catOverrides) { + const entry = catOverrides.get(operatorValue); + if (entry) return { override: entry, overrideCategory: category }; + } + + // Cross-category fallback: only if override category doesn't exist in the dump. + // This mirrors the generator, which only enters the cross-category path when + // `categorizedOps.get(category)` returns undefined. + for (const [overrideCat, opMap] of overrides) { + if (overrideCat === category) continue; + // If this override category exists in the dump, the generator would do an + // exact-category-only lookup there โ€” it would NOT spill into other categories. + if (dumpCategories.has(overrideCat)) continue; + const entry = opMap.get(operatorValue); + if (entry) return { override: entry, overrideCategory: overrideCat }; + } + + return undefined; +} + +// --------------------------------------------------------------------------- +// ANSI colors for terminal output +// --------------------------------------------------------------------------- + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const YELLOW = '\x1b[33m'; +const CYAN = '\x1b[36m'; +const DIM = '\x1b[2m'; +const BOLD = '\x1b[1m'; +const RESET = '\x1b[0m'; + +// --------------------------------------------------------------------------- +// Category โ†’ meta tag mapping (mirrors generator's CATEGORY_TO_META) +// --------------------------------------------------------------------------- + +const CATEGORY_TO_META: Record = { + 'Comparison Query Operators': 'META_QUERY_COMPARISON', + 'Logical Query Operators': 'META_QUERY_LOGICAL', + 'Element Query Operators': 'META_QUERY_ELEMENT', + 'Evaluation Query Operators': 'META_QUERY_EVALUATION', + 'Geospatial Operators': 'META_QUERY_GEOSPATIAL', + 'Array Query Operators': 'META_QUERY_ARRAY', + 'Bitwise Query Operators': 'META_QUERY_BITWISE', + 'Projection Operators': 'META_QUERY_PROJECTION', + 'Miscellaneous Query Operators': 'META_QUERY_MISC', + 'Field Update Operators': 'META_UPDATE_FIELD', + 'Array Update Operators': 'META_UPDATE_ARRAY', + 'Bitwise Update Operators': 'META_UPDATE_BITWISE', + 'Arithmetic Expression Operators': 'META_EXPR_ARITH', + 'Array Expression Operators': 'META_EXPR_ARRAY', + 'Bitwise Operators': 'META_EXPR_BITWISE', + 'Boolean Expression Operators': 'META_EXPR_BOOL', + 'Comparison Expression Operators': 'META_EXPR_COMPARISON', + 'Data Size Operators': 'META_EXPR_DATASIZE', + 'Date Expression Operators': 'META_EXPR_DATE', + 'Literal Expression Operator': 'META_EXPR_LITERAL', + 'Miscellaneous Operators': 'META_EXPR_MISC', + 'Object Expression Operators': 'META_EXPR_OBJECT', + 'Set Expression Operators': 'META_EXPR_SET', + 'String Expression Operators': 'META_EXPR_STRING', + 'Timestamp Expression Operators': 'META_EXPR_TIMESTAMP', + 'Trigonometry Expression Operators': 'META_EXPR_TRIG', + 'Type Expression Operators': 'META_EXPR_TYPE', + 'Accumulators ($group, $bucket, $bucketAuto, $setWindowFields)': 'META_ACCUMULATOR', + 'Accumulators (in Other Stages)': 'META_ACCUMULATOR', + Accumulators: 'META_ACCUMULATOR', + 'Variable Expression Operators': 'META_EXPR_VARIABLE', + 'Window Operators': 'META_WINDOW', + 'Conditional Expression Operators': 'META_EXPR_CONDITIONAL', + 'Aggregation Pipeline Stages': 'META_STAGE', + 'Variables in Aggregation Expressions': 'META_VARIABLE', +}; + +// --------------------------------------------------------------------------- +// Snippet file parser +// --------------------------------------------------------------------------- + +function parseSnippetsFile(content: string): Map> { + const lines = content.split('\n'); + const result = new Map>(); + + let currentMeta = ''; + let currentOp = ''; + let inCodeBlock = false; + + for (const line of lines) { + if (line.startsWith('```')) { + inCodeBlock = !inCodeBlock; + continue; + } + if (inCodeBlock) continue; + + const h2 = line.match(/^## (.+)$/); + if (h2) { + const cat = h2[1].trim(); + const meta = CATEGORY_TO_META[cat]; + if (meta) { + currentMeta = meta; + if (!result.has(currentMeta)) { + result.set(currentMeta, new Map()); + } + } else { + currentMeta = ''; + } + currentOp = ''; + continue; + } + + const h3 = line.match(/^### (.+)$/); + if (h3 && currentMeta) { + currentOp = h3[1].trim(); + continue; + } + + if (currentMeta && currentOp && line.startsWith('- **Snippet:**')) { + let snippet = line.replace('- **Snippet:**', '').trim(); + if (snippet.startsWith('`') && snippet.endsWith('`')) { + snippet = snippet.slice(1, -1); + } + if (snippet) { + result.get(currentMeta)!.set(currentOp, snippet); + } + continue; + } + } + + return result; +} + +function operatorHasSnippet( + snippets: Map>, + meta: string, + operatorValue: string, + overrideSnippet: string | undefined, +): boolean { + if (overrideSnippet) return true; + const catSnippets = snippets.get(meta); + if (!catSnippets) return false; + if (catSnippets.has(operatorValue)) return true; + if (catSnippets.has('DEFAULT')) return true; + return false; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main(): void { + const dumpPath = path.join(__dirname, '..', 'resources', 'scraped', 'operator-reference.md'); + const overridePath = path.join(__dirname, '..', 'resources', 'overrides', 'operator-overrides.md'); + const snippetsPath = path.join(__dirname, '..', 'resources', 'overrides', 'operator-snippets.md'); + + if (!fs.existsSync(dumpPath)) { + console.error(`โŒ Scraped dump not found: ${dumpPath}`); + process.exit(1); + } + + console.log(`${BOLD}๐Ÿ“Š Evaluating operator overrides${RESET}\n`); + + // Parse both files + const dumpContent = fs.readFileSync(dumpPath, 'utf-8'); + const dumpEntries = parseDump(dumpContent); + + let overrides = new Map>(); + let totalOverrideCount = 0; + if (fs.existsSync(overridePath)) { + const overrideContent = fs.readFileSync(overridePath, 'utf-8'); + overrides = parseOverrides(overrideContent); + for (const [, catMap] of overrides) { + totalOverrideCount += catMap.size; + } + } + + // Categorize every scraped entry + const gaps: ParsedEntry[] = []; // empty description, no override + const overridden: { entry: ParsedEntry; override: OverrideEntry; overrideCategory: string }[] = []; + const redundantOverrides: { entry: ParsedEntry; override: OverrideEntry; overrideCategory: string }[] = []; + const docLinkOnlyOverrides: { entry: ParsedEntry; override: OverrideEntry; overrideCategory: string }[] = []; + const descriptionsOk: ParsedEntry[] = []; + + // Collect all dump category names so findOverride can distinguish exact vs cross-category + const dumpCategories = new Set(dumpEntries.map((e) => e.category)); + + for (const entry of dumpEntries) { + const match = findOverride(overrides, entry.value, entry.category, dumpCategories); + const hasScrapedDescription = entry.description.trim().length > 0; + + if (match) { + const hasDescOverride = !!match.override.description; + const hasDocLinkOverride = !!match.override.docLink; + const hasSnippetOverride = !!match.override.snippet; + + if (hasScrapedDescription && hasDescOverride) { + // Has both scraped description AND an override description + redundantOverrides.push({ entry, override: match.override, overrideCategory: match.overrideCategory }); + } else if (!hasDescOverride && hasDocLinkOverride && !hasSnippetOverride) { + // Override provides only a doc link (no description, no snippet) + docLinkOnlyOverrides.push({ + entry, + override: match.override, + overrideCategory: match.overrideCategory, + }); + } else { + // Override is filling a description gap (or overriding snippet) + overridden.push({ entry, override: match.override, overrideCategory: match.overrideCategory }); + } + } else if (!hasScrapedDescription) { + gaps.push(entry); + } else { + descriptionsOk.push(entry); + } + } + + // ----------------------------------------------------------------------- + // Section 1: Gaps โ€” empty description, no override + // ----------------------------------------------------------------------- + console.log(`${BOLD}${RED}โ•โ•โ• GAPS: Empty description, no override (${gaps.length}) โ•โ•โ•${RESET}`); + if (gaps.length === 0) { + console.log(` ${GREEN}โœ… No gaps โ€” all operators have descriptions or overrides.${RESET}\n`); + } else { + const byCategory = groupByCategory(gaps); + for (const [cat, ops] of byCategory) { + console.log(` ${CYAN}${cat}${RESET}`); + for (const op of ops) { + console.log(` ${RED}โš ${RESET} ${op.value}`); + } + } + console.log(''); + } + + // ----------------------------------------------------------------------- + // Section 2: Potentially redundant overrides + // (scraped dump NOW has a description, but override also provides one) + // ----------------------------------------------------------------------- + console.log(`${BOLD}${YELLOW}โ•โ•โ• POTENTIALLY REDUNDANT OVERRIDES (${redundantOverrides.length}) โ•โ•โ•${RESET}`); + if (redundantOverrides.length === 0) { + console.log(` ${GREEN}โœ… No redundant overrides โ€” all overrides are filling gaps.${RESET}\n`); + } else { + console.log( + ` ${DIM}These operators now have scraped descriptions. The override may no longer be needed.${RESET}`, + ); + console.log( + ` ${DIM}Compare the values below โ€” if the scraped one is good enough, remove the override.${RESET}\n`, + ); + for (const { entry, override, overrideCategory } of redundantOverrides) { + console.log(` ${CYAN}${entry.value}${RESET} ${DIM}(${entry.category})${RESET}`); + console.log(` ${DIM}Override (${overrideCategory}):${RESET} ${override.description}`); + console.log(` ${DIM}Scraped:${RESET} ${entry.description}`); + console.log(''); + } + } + + // ----------------------------------------------------------------------- + // Section 3: Active overrides filling gaps + // ----------------------------------------------------------------------- + console.log(`${BOLD}${GREEN}โ•โ•โ• ACTIVE OVERRIDES FILLING GAPS (${overridden.length}) โ•โ•โ•${RESET}`); + if (overridden.length === 0) { + console.log(` ${DIM}No active overrides.${RESET}\n`); + } else { + const byCategory = new Map(); + for (const item of overridden) { + const cat = item.overrideCategory; + if (!byCategory.has(cat)) byCategory.set(cat, []); + byCategory.get(cat)!.push(item); + } + for (const [cat, items] of byCategory) { + console.log(` ${CYAN}${cat}${RESET} (${items.length} overrides)`); + for (const { entry, override } of items) { + const overrideDesc = override.description || '(no description override)'; + const scrapedDesc = entry.description || '(empty)'; + console.log(` ${GREEN}โœ“${RESET} ${entry.value}`); + console.log(` ${DIM}Override:${RESET} ${overrideDesc}`); + if (scrapedDesc !== '(empty)') { + console.log(` ${DIM}Scraped:${RESET} ${scrapedDesc}`); + } + } + } + console.log(''); + } + + // ----------------------------------------------------------------------- + // Section 3b: Doc link overrides (operators with 'none' in dump, link provided via override) + // ----------------------------------------------------------------------- + console.log(`${BOLD}${GREEN}โ•โ•โ• DOC LINK OVERRIDES (${docLinkOnlyOverrides.length}) โ•โ•โ•${RESET}`); + if (docLinkOnlyOverrides.length === 0) { + console.log(` ${DIM}No doc-link-only overrides.${RESET}\n`); + } else { + console.log(` ${DIM}These operators have 'none' in the dump (doc page not at expected directory).${RESET}`); + console.log( + ` ${DIM}The override provides a doc link that the generator can't infer via cross-reference.${RESET}\n`, + ); + for (const { entry, override, overrideCategory } of docLinkOnlyOverrides) { + const dumpLink = entry.docLink || 'none'; + console.log(` ${CYAN}${entry.value}${RESET} ${DIM}(${entry.category})${RESET}`); + console.log(` ${DIM}Override (${overrideCategory}):${RESET} ${override.docLink}`); + console.log(` ${DIM}Dump link:${RESET} ${dumpLink}`); + console.log(''); + } + } + + // ----------------------------------------------------------------------- + // Section 4: Snippet coverage + // ----------------------------------------------------------------------- + let snippets = new Map>(); + if (fs.existsSync(snippetsPath)) { + const snippetsContent = fs.readFileSync(snippetsPath, 'utf-8'); + snippets = parseSnippetsFile(snippetsContent); + } + + const withSnippet: ParsedEntry[] = []; + const withoutSnippet: ParsedEntry[] = []; + + for (const entry of dumpEntries) { + const meta = CATEGORY_TO_META[entry.category]; + if (!meta) { + withoutSnippet.push(entry); + continue; + } + const match = findOverride(overrides, entry.value, entry.category, dumpCategories); + const overrideSnippet = match?.override.snippet; + if (operatorHasSnippet(snippets, meta, entry.value, overrideSnippet)) { + withSnippet.push(entry); + } else { + withoutSnippet.push(entry); + } + } + + console.log(`${BOLD}${CYAN}โ•โ•โ• SNIPPET COVERAGE (${withSnippet.length}/${dumpEntries.length}) โ•โ•โ•${RESET}`); + if (withoutSnippet.length === 0) { + console.log(` ${GREEN}โœ… All operators have snippet templates.${RESET}\n`); + } else { + console.log(` ${DIM}Operators without snippet templates (by category):${RESET}\n`); + const byCategory = groupByCategory(withoutSnippet); + for (const [cat, ops] of byCategory) { + console.log(` ${CYAN}${cat}${RESET}`); + for (const op of ops) { + console.log(` ${DIM}โ€”${RESET} ${op.value}`); + } + } + console.log(''); + } + + // ----------------------------------------------------------------------- + // Section 5: Summary + // ----------------------------------------------------------------------- + console.log(`${BOLD}โ•โ•โ• SUMMARY โ•โ•โ•${RESET}`); + console.log(` Total scraped operators: ${dumpEntries.length}`); + console.log(` With scraped description: ${descriptionsOk.length + redundantOverrides.length}`); + console.log(` Filled by override: ${overridden.length}`); + console.log(` Doc-link-only overrides: ${docLinkOnlyOverrides.length}`); + console.log(` Potentially redundant: ${YELLOW}${redundantOverrides.length}${RESET}`); + console.log(` ${RED}Gaps remaining:${RESET} ${gaps.length}`); + console.log(` Total overrides in file: ${totalOverrideCount}`); + console.log(` With snippet template: ${withSnippet.length}`); + console.log(` Without snippet: ${withoutSnippet.length}`); + console.log(` Description coverage: ${((1 - gaps.length / dumpEntries.length) * 100).toFixed(1)}%`); + console.log(` Snippet coverage: ${((withSnippet.length / dumpEntries.length) * 100).toFixed(1)}%`); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function groupByCategory(entries: ParsedEntry[]): Map { + const map = new Map(); + for (const e of entries) { + if (!map.has(e.category)) map.set(e.category, []); + map.get(e.category)!.push(e); + } + return map; +} + +main(); diff --git a/packages/documentdb-constants/scripts/generate-from-reference.ts b/packages/documentdb-constants/scripts/generate-from-reference.ts new file mode 100644 index 000000000..0e198b548 --- /dev/null +++ b/packages/documentdb-constants/scripts/generate-from-reference.ts @@ -0,0 +1,871 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Generates TypeScript operator data files from the scraped reference dump. + * + * Reads: + * resources/scraped/operator-reference.md โ€” scraped operator data (primary) + * resources/overrides/operator-overrides.md โ€” hand-written overrides (wins) + * resources/overrides/operator-snippets.md โ€” snippet templates per category + * + * Writes: + * src/queryOperators.ts, src/updateOperators.ts, src/expressionOperators.ts, + * src/accumulators.ts, src/windowOperators.ts, src/stages.ts, + * src/systemVariables.ts + * + * The override file uses the same Markdown format as the dump. Any field + * specified in an override entry replaces the corresponding scraped value. + * Omitted fields keep their scraped values. + * + * Snippets are resolved in order: + * 1. Snippet override from operator-overrides.md (highest priority) + * 2. Per-operator snippet from operator-snippets.md + * 3. DEFAULT snippet from operator-snippets.md ({{VALUE}} โ†’ operator name) + * 4. No snippet + * + * Usage: npm run generate + * Note: This script overwrites the generated src/ files. Do NOT edit + * those files by hand โ€” put corrections in the overrides/snippets + * files instead. + */ + +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { getDocLink } from '../src/docLinks'; +import * as MetaTags from '../src/metaTags'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface ParsedOperator { + value: string; + description: string; + syntax: string; + docLink: string; + category: string; + snippetOverride?: string; + standalone?: boolean; +} + +interface FileSpec { + fileName: string; + variableName: string; + metaImport: string; + metaValue: string; + operators: ParsedOperator[]; + extraImports?: string; +} + +// --------------------------------------------------------------------------- +// Category โ†’ meta tag mapping +// --------------------------------------------------------------------------- + +const CATEGORY_TO_META: Record = { + 'Comparison Query Operators': 'META_QUERY_COMPARISON', + 'Logical Query Operators': 'META_QUERY_LOGICAL', + 'Element Query Operators': 'META_QUERY_ELEMENT', + 'Evaluation Query Operators': 'META_QUERY_EVALUATION', + 'Geospatial Operators': 'META_QUERY_GEOSPATIAL', + 'Array Query Operators': 'META_QUERY_ARRAY', + 'Bitwise Query Operators': 'META_QUERY_BITWISE', + 'Projection Operators': 'META_QUERY_PROJECTION', + 'Miscellaneous Query Operators': 'META_QUERY_MISC', + 'Field Update Operators': 'META_UPDATE_FIELD', + 'Array Update Operators': 'META_UPDATE_ARRAY', + 'Bitwise Update Operators': 'META_UPDATE_BITWISE', + 'Arithmetic Expression Operators': 'META_EXPR_ARITH', + 'Array Expression Operators': 'META_EXPR_ARRAY', + 'Bitwise Operators': 'META_EXPR_BITWISE', + 'Boolean Expression Operators': 'META_EXPR_BOOL', + 'Comparison Expression Operators': 'META_EXPR_COMPARISON', + 'Data Size Operators': 'META_EXPR_DATASIZE', + 'Date Expression Operators': 'META_EXPR_DATE', + 'Literal Expression Operator': 'META_EXPR_LITERAL', + 'Miscellaneous Operators': 'META_EXPR_MISC', + 'Object Expression Operators': 'META_EXPR_OBJECT', + 'Set Expression Operators': 'META_EXPR_SET', + 'String Expression Operators': 'META_EXPR_STRING', + 'Timestamp Expression Operators': 'META_EXPR_TIMESTAMP', + 'Trigonometry Expression Operators': 'META_EXPR_TRIG', + 'Type Expression Operators': 'META_EXPR_TYPE', + 'Accumulators ($group, $bucket, $bucketAuto, $setWindowFields)': 'META_ACCUMULATOR', + 'Accumulators (in Other Stages)': 'META_ACCUMULATOR', + Accumulators: 'META_ACCUMULATOR', + 'Variable Expression Operators': 'META_EXPR_VARIABLE', + 'Window Operators': 'META_WINDOW', + 'Conditional Expression Operators': 'META_EXPR_CONDITIONAL', + 'Aggregation Pipeline Stages': 'META_STAGE', + 'Variables in Aggregation Expressions': 'META_VARIABLE', +}; + +/** + * Maps META constant names (like 'META_EXPR_STRING') to their string values + * (like 'expr:string') so we can call getDocLink() at generation time to + * compare the computed URL against the dump's verified URL. + */ +const META_CONST_TO_VALUE: Record = Object.fromEntries( + Object.entries(MetaTags) + .filter(([, v]) => typeof v === 'string') + .map(([k, v]) => [k, v as string]), +); + +// Category โ†’ output file mapping +const CATEGORY_TO_FILE: Record = { + 'Comparison Query Operators': 'queryOperators', + 'Logical Query Operators': 'queryOperators', + 'Element Query Operators': 'queryOperators', + 'Evaluation Query Operators': 'queryOperators', + 'Geospatial Operators': 'queryOperators', + 'Array Query Operators': 'queryOperators', + 'Bitwise Query Operators': 'queryOperators', + 'Projection Operators': 'queryOperators', + 'Miscellaneous Query Operators': 'queryOperators', + 'Field Update Operators': 'updateOperators', + 'Array Update Operators': 'updateOperators', + 'Bitwise Update Operators': 'updateOperators', + 'Arithmetic Expression Operators': 'expressionOperators', + 'Array Expression Operators': 'expressionOperators', + 'Bitwise Operators': 'expressionOperators', + 'Boolean Expression Operators': 'expressionOperators', + 'Comparison Expression Operators': 'expressionOperators', + 'Data Size Operators': 'expressionOperators', + 'Date Expression Operators': 'expressionOperators', + 'Literal Expression Operator': 'expressionOperators', + 'Miscellaneous Operators': 'expressionOperators', + 'Object Expression Operators': 'expressionOperators', + 'Set Expression Operators': 'expressionOperators', + 'String Expression Operators': 'expressionOperators', + 'Timestamp Expression Operators': 'expressionOperators', + 'Trigonometry Expression Operators': 'expressionOperators', + 'Type Expression Operators': 'expressionOperators', + 'Conditional Expression Operators': 'expressionOperators', + 'Variable Expression Operators': 'expressionOperators', + 'Accumulators ($group, $bucket, $bucketAuto, $setWindowFields)': 'accumulators', + 'Accumulators (in Other Stages)': 'accumulators', + 'Window Operators': 'windowOperators', + 'Aggregation Pipeline Stages': 'stages', + 'Variables in Aggregation Expressions': 'systemVariables', +}; + +// --------------------------------------------------------------------------- +// Parser +// --------------------------------------------------------------------------- + +function parseDump(content: string): Map { + const lines = content.split('\n'); + const categorizedOps = new Map(); + + let currentCategory = ''; + let currentOp: Partial | null = null; + let inCodeBlock = false; + let syntaxLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Track code blocks + if (line.startsWith('```')) { + if (inCodeBlock) { + // End of code block + inCodeBlock = false; + if (currentOp) { + currentOp.syntax = syntaxLines.join('\n').trim(); + } + syntaxLines = []; + continue; + } else { + inCodeBlock = true; + continue; + } + } + + if (inCodeBlock) { + syntaxLines.push(line); + continue; + } + + // H2 = category + const h2Match = line.match(/^## (.+)$/); + if (h2Match) { + // Save previous operator + if (currentOp && currentCategory) { + saveOperator(categorizedOps, currentCategory, currentOp as ParsedOperator); + } + currentOp = null; + + const cat = h2Match[1].trim(); + if (cat === 'Summary' || cat === 'Not Listed') { + currentCategory = ''; + continue; + } + currentCategory = cat; + if (!categorizedOps.has(currentCategory)) { + categorizedOps.set(currentCategory, []); + } + continue; + } + + // H3 = operator + const h3Match = line.match(/^### (.+)$/); + if (h3Match && currentCategory) { + // Save previous operator + if (currentOp) { + saveOperator(categorizedOps, currentCategory, currentOp as ParsedOperator); + } + currentOp = { + value: h3Match[1].trim(), + description: '', + syntax: '', + docLink: '', + category: currentCategory, + }; + continue; + } + + // Description line + if (currentOp && line.startsWith('- **Description:**')) { + currentOp.description = line.replace('- **Description:**', '').trim(); + continue; + } + + // Doc link line ('none' means the scraper found no page at the expected location) + if (currentOp && line.startsWith('- **Doc Link:**')) { + const rawLink = line.replace('- **Doc Link:**', '').trim(); + currentOp.docLink = rawLink === 'none' ? '' : rawLink; + continue; + } + } + + // Save last operator + if (currentOp && currentCategory) { + saveOperator(categorizedOps, currentCategory, currentOp as ParsedOperator); + } + + return categorizedOps; +} + +function saveOperator(map: Map, category: string, op: Partial): void { + if (!op.value) return; + const list = map.get(category) || []; + list.push({ + value: op.value || '', + description: op.description || '', + syntax: op.syntax || '', + docLink: op.docLink || '', + category: category, + snippetOverride: op.snippetOverride, + }); + map.set(category, list); +} + +// --------------------------------------------------------------------------- +// Override parsing and merging +// --------------------------------------------------------------------------- + +interface OverrideEntry { + description?: string; + syntax?: string; + docLink?: string; + snippet?: string; + standalone?: boolean; +} + +function parseOverrides(content: string): Map> { + const lines = content.split('\n'); + const result = new Map>(); + + let currentCategory = ''; + let currentOp: { value: string; entry: OverrideEntry } | null = null; + let inCodeBlock = false; + let syntaxLines: string[] = []; + + for (const line of lines) { + if (line.startsWith('```')) { + if (inCodeBlock) { + inCodeBlock = false; + if (currentOp) { + currentOp.entry.syntax = syntaxLines.join('\n').trim(); + } + syntaxLines = []; + continue; + } else { + inCodeBlock = true; + continue; + } + } + if (inCodeBlock) { + syntaxLines.push(line); + continue; + } + + const h2 = line.match(/^## (.+)$/); + if (h2) { + if (currentOp && currentCategory) { + saveOverride(result, currentCategory, currentOp); + } + currentOp = null; + currentCategory = h2[1].trim(); + continue; + } + + const h3 = line.match(/^### (.+)$/); + if (h3 && currentCategory) { + if (currentOp) { + saveOverride(result, currentCategory, currentOp); + } + currentOp = { value: h3[1].trim(), entry: {} }; + continue; + } + + if (currentOp && line.startsWith('- **Description:**')) { + currentOp.entry.description = line.replace('- **Description:**', '').trim(); + } + if (currentOp && line.startsWith('- **Doc Link:**')) { + currentOp.entry.docLink = line.replace('- **Doc Link:**', '').trim(); + } + if (currentOp && line.startsWith('- **Snippet:**')) { + let snippet = line.replace('- **Snippet:**', '').trim(); + if (snippet.startsWith('`') && snippet.endsWith('`')) { + snippet = snippet.slice(1, -1); + } + currentOp.entry.snippet = snippet; + } + if (currentOp && line.startsWith('- **Standalone:**')) { + const val = line.replace('- **Standalone:**', '').trim().toLowerCase(); + currentOp.entry.standalone = val !== 'false' ? undefined : false; + } + } + + if (currentOp && currentCategory) { + saveOverride(result, currentCategory, currentOp); + } + + return result; +} + +function saveOverride( + map: Map>, + category: string, + op: { value: string; entry: OverrideEntry }, +): void { + if (!map.has(category)) map.set(category, new Map()); + map.get(category)!.set(op.value, op.entry); +} + +function applyOverrides( + categorizedOps: Map, + overrides: Map>, +): void { + let applied = 0; + let missed = 0; + + for (const [category, opOverrides] of overrides) { + const ops = categorizedOps.get(category); + if (!ops) { + // Try to find operators across all categories (override category + // may not match dump category exactly for cross-category operators) + for (const [opName, override] of opOverrides) { + const matches: Array<{ category: string; op: ParsedOperator }> = []; + for (const [cat, catOps] of categorizedOps) { + const op = catOps.find((o) => o.value === opName); + if (op) matches.push({ category: cat, op }); + } + if (matches.length === 0) { + console.warn(`โš ๏ธ Override target not found: ${opName} in "${category}"`); + missed++; + } else { + if (matches.length > 1) { + const catList = matches.map((m) => `"${m.category}"`).join(', '); + console.warn( + `โš ๏ธ Ambiguous override fallback: "${opName}" โ€” found in ${matches.length} categories: [${catList}]. Override from "${category}" applied to first match. Specify the correct category to disambiguate.`, + ); + } else { + console.log( + `โ„น๏ธ Override fallback: "${opName}" not found in "${category}", applied to match in "${matches[0].category}".`, + ); + } + mergeOverride(matches[0].op, override); + applied++; + } + } + continue; + } + + for (const [opName, override] of opOverrides) { + const op = ops.find((o) => o.value === opName); + if (op) { + mergeOverride(op, override); + applied++; + } else { + console.warn(`โš ๏ธ Override target not found: ${opName} in "${category}"`); + missed++; + } + } + } + + console.log(` Applied ${applied} overrides (${missed} missed)`); +} + +function mergeOverride(op: ParsedOperator, override: OverrideEntry): void { + if (override.description !== undefined && override.description !== '') { + op.description = override.description; + } + if (override.syntax !== undefined && override.syntax !== '') { + op.syntax = override.syntax; + } + if (override.docLink !== undefined && override.docLink !== '') { + op.docLink = override.docLink; + } + if (override.snippet !== undefined && override.snippet !== '') { + op.snippetOverride = override.snippet; + } + if (override.standalone !== undefined) { + op.standalone = override.standalone; + } +} + +// --------------------------------------------------------------------------- +// Snippet loading (from resources/overrides/operator-snippets.md) +// --------------------------------------------------------------------------- + +/** + * Parses the operator-snippets.md file into a map of meta-tag โ†’ (operator|DEFAULT โ†’ snippet). + * Uses the same heading conventions as the dump/overrides parsers. + */ +function parseSnippets(content: string): Map> { + const lines = content.split('\n'); + const result = new Map>(); + + let currentMeta = ''; + let currentOp = ''; + let inCodeBlock = false; + + for (const line of lines) { + if (line.startsWith('```')) { + inCodeBlock = !inCodeBlock; + continue; + } + if (inCodeBlock) continue; + + // H2 = category + const h2 = line.match(/^## (.+)$/); + if (h2) { + const cat = h2[1].trim(); + const meta = CATEGORY_TO_META[cat]; + if (meta) { + currentMeta = meta; + if (!result.has(currentMeta)) { + result.set(currentMeta, new Map()); + } + } else { + currentMeta = ''; + console.warn(`โš ๏ธ Unknown snippet category: "${cat}"`); + } + currentOp = ''; + continue; + } + + // H3 = operator name or DEFAULT + const h3 = line.match(/^### (.+)$/); + if (h3 && currentMeta) { + currentOp = h3[1].trim(); + continue; + } + + // Snippet value (backticks are stripped if present: `...` โ†’ ...) + if (currentMeta && currentOp && line.startsWith('- **Snippet:**')) { + let snippet = line.replace('- **Snippet:**', '').trim(); + if (snippet.startsWith('`') && snippet.endsWith('`')) { + snippet = snippet.slice(1, -1); + } + if (snippet) { + result.get(currentMeta)!.set(currentOp, snippet); + } + continue; + } + } + + return result; +} + +/** + * Looks up a snippet for an operator from the parsed snippets map. + * + * Resolution order: + * 1. Exact operator match in the category + * 2. DEFAULT entry in the category (with {{VALUE}} replaced by operator name) + * 3. undefined (no snippet) + */ +function lookupSnippet( + snippets: Map>, + meta: string, + operatorValue: string, +): string | undefined { + const catSnippets = snippets.get(meta); + if (!catSnippets) return undefined; + + // Exact operator match + const exact = catSnippets.get(operatorValue); + if (exact !== undefined) return exact; + + // Fall back to category DEFAULT + const def = catSnippets.get('DEFAULT'); + if (def) return def.replace(/\{\{VALUE\}\}/g, operatorValue); + + return undefined; +} + +// --------------------------------------------------------------------------- +// BSON type applicability +// --------------------------------------------------------------------------- + +function getApplicableBsonTypes(op: ParsedOperator, meta: string): string[] | undefined { + const v = op.value; + + // String-specific operators + if (v === '$regex' || v === '$text') return ['string']; + if (meta === 'META_EXPR_STRING' || meta === 'META_EXPR_TRIG') return undefined; // expression context, not filter-level + + // Array-specific operators (query context) + if (meta === 'META_QUERY_ARRAY') return ['array']; + + // Bitwise query operators โ€” use 'int32' to match SchemaAnalyzer BSON types + if (meta === 'META_QUERY_BITWISE') return ['int32', 'long']; + + return undefined; +} + +// --------------------------------------------------------------------------- +// Cross-reference: resolve missing doc links from other categories +// --------------------------------------------------------------------------- + +/** + * Builds a map of operator name โ†’ URL from all categories. + * For operators that appear with a URL in ANY category, we can use that URL + * when the same operator appears without one in a different category. + * + * Returns the number of operators whose links were inferred. + */ +function crossReferenceMissingLinks(categorizedOps: Map): number { + // Build global URL lookup: operator name โ†’ first known URL + const urlLookup = new Map(); + for (const ops of categorizedOps.values()) { + for (const op of ops) { + if (op.docLink && !urlLookup.has(op.value)) { + urlLookup.set(op.value, op.docLink); + } + } + } + + // Fill in missing links from the cross-reference + let inferred = 0; + for (const [category, ops] of categorizedOps.entries()) { + for (const op of ops) { + if (!op.docLink) { + const altUrl = urlLookup.get(op.value); + if (altUrl) { + op.docLink = altUrl; + // Mark as inferred so generateSection can annotate it + (op as ParsedOperator & { inferredLink?: boolean }).inferredLink = true; + inferred++; + console.log(` Inferred link: ${op.value} (${category}) โ†’ ${altUrl}`); + } + } + } + } + + return inferred; +} + +// --------------------------------------------------------------------------- +// File generation +// --------------------------------------------------------------------------- + +function generateFileContent(specs: FileSpec[], snippets: Map>): string { + const copyright = `/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED โ€” DO NOT EDIT BY HAND +// +// Generated by: npm run generate (scripts/generate-from-reference.ts) +// Sources: resources/scraped/operator-reference.md +// resources/overrides/operator-overrides.md +// resources/overrides/operator-snippets.md +// +// To change operator data, edit the overrides/snippets files and re-run the generator. +`; + + // Collect all unique meta imports + const allMetaImports = new Set(); + for (const spec of specs) { + allMetaImports.add(spec.metaImport); + } + + const metaImportsList = [...allMetaImports].sort().join(',\n '); + + // Pre-generate all sections so we can detect whether getDocLink is used + const sections: string[] = []; + for (const spec of specs) { + sections.push(generateSection(spec, snippets)); + } + const sectionsStr = sections.join('\n'); + + // Only import getDocLink if at least one operator uses it in this file + const needsDocLink = sectionsStr.includes('getDocLink('); + const docLinkImport = needsDocLink ? `\nimport { getDocLink } from './docLinks';` : ''; + + let content = `${copyright} +import { type OperatorEntry } from './types'; +import { ${metaImportsList} } from './metaTags';${docLinkImport} +import { registerOperators } from './getFilteredCompletions'; + +`; + + content += sectionsStr; + + // Derive the exported load function name from the first spec's fileName + // e.g. "queryOperators" โ†’ "loadQueryOperators" + const fileName = specs[0]?.fileName ?? 'operators'; + const loadFnName = 'load' + fileName.charAt(0).toUpperCase() + fileName.slice(1); + + // Emit an explicit load function instead of a side-effect registration call + const allVarNames = specs.map((s) => `...${s.variableName}`).join(',\n '); + content += `// ---------------------------------------------------------------------------\n`; + content += `// Registration\n`; + content += `// ---------------------------------------------------------------------------\n\n`; + content += `export function ${loadFnName}(): void {\n`; + content += ` registerOperators([\n ${allVarNames},\n ]);\n`; + content += `}\n`; + + return content; +} + +function generateSection(spec: FileSpec, snippets: Map>): string { + let section = `// ---------------------------------------------------------------------------\n`; + section += `// ${spec.operators[0]?.category || spec.variableName}\n`; + section += `// ---------------------------------------------------------------------------\n\n`; + + section += `const ${spec.variableName}: readonly OperatorEntry[] = [\n`; + + // Resolve the meta tag's string value for runtime getDocLink comparison + const metaStringValue = META_CONST_TO_VALUE[spec.metaImport] || ''; + + for (const op of spec.operators) { + const snippet = op.snippetOverride || lookupSnippet(snippets, spec.metaImport, op.value); + const bsonTypes = getApplicableBsonTypes(op, spec.metaImport); + + // Determine the correct link emission strategy: + // - If dump has a URL that matches what getDocLink() would produce โ†’ use getDocLink() (compact) + // - If the URL was inferred via cross-reference โ†’ emit hardcoded string with comment + // - If dump has a URL that differs from getDocLink() โ†’ emit hardcoded string + // - If dump has no URL โ†’ omit the link property + const computedLink = getDocLink(op.value, metaStringValue); + const dumpLink = op.docLink || ''; + const isInferred = (op as ParsedOperator & { inferredLink?: boolean }).inferredLink === true; + let linkLine: string; + if (!dumpLink) { + // No documentation page exists โ€” omit the link + linkLine = ''; + } else if (isInferred) { + // Link was inferred from another category via cross-reference (scraper confirmed + // no page exists at this operator's own category URL โ€” use the real page found) + linkLine = ` link: '${escapeString(dumpLink)}', // inferred from another category\n`; + } else if (dumpLink === computedLink) { + // The computed URL matches โ€” use the compact getDocLink() call + linkLine = ` link: getDocLink('${escapeString(op.value)}', ${spec.metaImport}),\n`; + } else { + // The dump has a verified URL that differs from getDocLink() โ€” emit hardcoded + linkLine = ` link: '${escapeString(dumpLink)}',\n`; + } + + section += ` {\n`; + section += ` value: '${escapeString(op.value)}',\n`; + section += ` meta: ${spec.metaImport},\n`; + section += ` description: '${escapeString(op.description)}',\n`; + if (snippet) { + section += ` snippet: '${escapeString(snippet)}',\n`; + } + if (linkLine) { + section += linkLine; + } + if (bsonTypes) { + section += ` applicableBsonTypes: [${bsonTypes.map((t) => `'${t}'`).join(', ')}],\n`; + } + if (op.standalone === false) { + section += ` standalone: false,\n`; + } + section += ` },\n`; + } + + section += `];\n\n`; + return section; +} + +function escapeString(s: string): string { + return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); +} + +// --------------------------------------------------------------------------- +// Group operators by file and generate +// --------------------------------------------------------------------------- + +function buildFileSpecs(categorizedOps: Map): Map { + const fileGroups = new Map(); + + // Track seen operators per file to deduplicate + const seenPerFile = new Map>(); + + for (const [category, ops] of categorizedOps) { + const fileName = CATEGORY_TO_FILE[category]; + const metaConst = CATEGORY_TO_META[category]; + + if (!fileName || !metaConst) { + console.warn(`โš ๏ธ No mapping for category: "${category}" (${ops.length} operators)`); + continue; + } + + if (!seenPerFile.has(fileName)) { + seenPerFile.set(fileName, new Set()); + } + const seen = seenPerFile.get(fileName)!; + + // Deduplicate operators (e.g., $elemMatch appears in both query:array and projection) + const uniqueOps = ops.filter((op) => { + if (seen.has(op.value + ':' + metaConst)) return false; + seen.add(op.value + ':' + metaConst); + return true; + }); + + if (uniqueOps.length === 0) continue; + + // Create a camelCase variable name from the category + const varName = categoryToVarName(category); + + const spec: FileSpec = { + fileName, + variableName: varName, + metaImport: metaConst, + metaValue: metaConst, + operators: uniqueOps, + }; + + if (!fileGroups.has(fileName)) { + fileGroups.set(fileName, []); + } + fileGroups.get(fileName)!.push(spec); + } + + return fileGroups; +} + +function categoryToVarName(category: string): string { + // "Comparison Query Operators" โ†’ "comparisonQueryOperators" + // "Accumulators ($group, $bucket, $bucketAuto, $setWindowFields)" โ†’ "groupAccumulators" + + if (category === 'Accumulators ($group, $bucket, $bucketAuto, $setWindowFields)') { + return 'groupAccumulators'; + } + if (category === 'Accumulators (in Other Stages)') { + return 'otherStageAccumulators'; + } + if (category === 'Variables in Aggregation Expressions') { + return 'systemVariables'; + } + + const words = category + .replace(/[()$,]/g, '') + .split(/\s+/) + .filter((w) => w.length > 0); + return words + .map((w, i) => (i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())) + .join(''); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main(): void { + const dumpPath = path.join(__dirname, '..', 'resources', 'scraped', 'operator-reference.md'); + const overridePath = path.join(__dirname, '..', 'resources', 'overrides', 'operator-overrides.md'); + const snippetsPath = path.join(__dirname, '..', 'resources', 'overrides', 'operator-snippets.md'); + const srcDir = path.join(__dirname, '..', 'src'); + + console.log('๐Ÿ“– Reading operator reference dump...'); + const content = fs.readFileSync(dumpPath, 'utf-8'); + + console.log('๐Ÿ” Parsing operators...'); + const categorizedOps = parseDump(content); + + let totalOps = 0; + for (const [cat, ops] of categorizedOps) { + console.log(` ${cat}: ${ops.length} operators`); + totalOps += ops.length; + } + console.log(` Total: ${totalOps} operators\n`); + + // Apply overrides if the file exists + if (fs.existsSync(overridePath)) { + console.log('๐Ÿ“ Reading overrides...'); + const overrideContent = fs.readFileSync(overridePath, 'utf-8'); + const overrides = parseOverrides(overrideContent); + applyOverrides(categorizedOps, overrides); + console.log(''); + } else { + console.log('โ„น๏ธ No overrides file found, skipping.\n'); + } + + // Cross-reference missing doc links from other categories + console.log('๐Ÿ”— Cross-referencing missing doc links...'); + const inferred = crossReferenceMissingLinks(categorizedOps); + console.log(` Inferred ${inferred} links from other categories\n`); + + // Load snippet templates + let snippetsMap = new Map>(); + if (fs.existsSync(snippetsPath)) { + console.log('๐Ÿ“‹ Reading snippet templates...'); + const snippetsContent = fs.readFileSync(snippetsPath, 'utf-8'); + snippetsMap = parseSnippets(snippetsContent); + let snippetCount = 0; + for (const [, catMap] of snippetsMap) { + snippetCount += catMap.size; + } + console.log(` Loaded ${snippetCount} snippet entries across ${snippetsMap.size} categories\n`); + } else { + console.log('โ„น๏ธ No snippets file found, skipping.\n'); + } + + console.log('๐Ÿ“ Building file specs...'); + const fileGroups = buildFileSpecs(categorizedOps); + + for (const [fileName, specs] of fileGroups) { + const filePath = path.join(srcDir, `${fileName}.ts`); + console.log( + `โœ๏ธ Generating ${fileName}.ts (${specs.reduce((n, s) => n + s.operators.length, 0)} operators)...`, + ); + const fileContent = generateFileContent(specs, snippetsMap); + fs.writeFileSync(filePath, fileContent, 'utf-8'); + } + + // Format generated files with Prettier + const generatedFiles = [...fileGroups.keys()].map((f) => path.join(srcDir, `${f}.ts`)); + console.log('\n๐ŸŽจ Formatting generated files with Prettier...'); + execSync(`npx prettier --write ${generatedFiles.map((f) => `"${f}"`).join(' ')}`, { + stdio: 'inherit', + }); + + console.log('\nโœ… Done! Generated files:'); + for (const [fileName, specs] of fileGroups) { + const count = specs.reduce((n, s) => n + s.operators.length, 0); + console.log(` src/${fileName}.ts โ€” ${count} operators`); + } +} + +main(); diff --git a/packages/documentdb-constants/scripts/scrape-operator-docs.ts b/packages/documentdb-constants/scripts/scrape-operator-docs.ts new file mode 100644 index 000000000..a4780a1d8 --- /dev/null +++ b/packages/documentdb-constants/scripts/scrape-operator-docs.ts @@ -0,0 +1,964 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * scrape-operator-docs.ts + * + * Scrapes the DocumentDB compatibility page and per-operator documentation + * to generate the resources/scraped/operator-reference.md dump file. + * + * Usage: + * npx ts-node packages/documentdb-constants/scripts/scrape-operator-docs.ts + * + * The scraper has three phases: + * Phase 1: Fetch and parse the compatibility page (operator list + support status) + * Phase 2: Fetch per-operator doc pages (descriptions + syntax) + * Phase 3: Generate the Markdown dump file + * + * Before doing real work, a verification step checks that the upstream + * documentation structure is as expected by fetching a few known URLs. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface OperatorInfo { + operator: string; + category: string; + listed: boolean; + /** Human-readable reason if not listed */ + notListedReason?: string; + /** Description from the per-operator doc page YAML frontmatter */ + description?: string; + /** Syntax snippet from the per-operator doc page */ + syntax?: string; + /** Documentation URL (derived from the directory where the .md file was found) */ + docLink?: string; + /** + * Human-readable note added when the scraper resolves a doc page from a + * different directory than the operator's primary category, or when other + * notable resolution decisions are made. Written to the dump as + * `- **Scraper Comment:**` for traceability. + */ + scraperComment?: string; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const COMPAT_PAGE_URL = + 'https://raw.githubusercontent.com/MicrosoftDocs/azure-databases-docs/main/articles/documentdb/compatibility-query-language.md'; + +const OPERATOR_DOC_BASE = + 'https://raw.githubusercontent.com/MicrosoftDocs/azure-databases-docs/main/articles/documentdb/operators'; + +const DOC_LINK_BASE = 'https://learn.microsoft.com/en-us/azure/documentdb/operators'; + +/** + * Maps category names (as they appear in column 1 of the compat page table) + * to the docs directory used for per-operator doc pages. + * + * This mapping is derived from the operators TOC.yml in the azure-databases-docs repo. + * Category names are trimmed before lookup, so leading/trailing spaces are OK. + */ +const CATEGORY_TO_DIR: Record = { + // Query operators + 'Comparison Query Operators': 'comparison-query', + 'Logical Query Operators': 'logical-query', + 'Element Query Operators': 'element-query', + 'Evaluation Query Operators': 'evaluation-query', + 'Array Query Operators': 'array-query', + 'Bitwise Query Operators': 'bitwise-query', + 'Geospatial Operators': 'geospatial', + 'Projection Operators': 'projection', + 'Miscellaneous Query Operators': 'miscellaneous-query', + // Update operators + 'Field Update Operators': 'field-update', + 'Array Update Operators': 'array-update', + 'Bitwise Update Operators': 'bitwise-update', + // Aggregation + 'Aggregation Pipeline Stages': 'aggregation', + 'Accumulators ($group, $bucket, $bucketAuto, $setWindowFields)': 'accumulators', + 'Accumulators (in Other Stages)': 'accumulators', + // Expression operators + 'Arithmetic Expression Operators': 'arithmetic-expression', + 'Array Expression Operators': 'array-expression', + 'Bitwise Operators': 'bitwise', + 'Boolean Expression Operators': 'boolean-expression', + 'Comparison Expression Operators': 'comparison-expression', + 'Conditional Expression Operators': 'conditional-expression', + 'Data Size Operators': 'data-size', + 'Date Expression Operators': 'date-expression', + 'Literal Expression Operator': 'literal-expression', + 'Miscellaneous Operators': 'miscellaneous', + 'Object Expression Operators': 'object-expression', + 'Set Expression Operators': 'set-expression', + 'String Expression Operators': 'string-expression', + 'Trigonometry Expression Operators': 'trigonometry-expression', + 'Type Expression Operators': 'aggregation/type-expression', + 'Timestamp Expression Operators': 'timestamp-expression', + 'Variable Expression Operators': 'variable-expression', + 'Text Expression Operator': 'miscellaneous', + 'Custom Aggregation Expression Operators': 'miscellaneous', + // Window + 'Window Operators': 'window-operators', + // System variables โ€” no per-operator doc pages + 'Variables in Aggregation Expressions': '', +}; + +/** Delay between batches of concurrent requests (ms) */ +const BATCH_DELAY_MS = 200; + +/** Number of concurrent requests per batch */ +const BATCH_SIZE = 10; + +/** Maximum number of retry attempts for transient HTTP errors */ +const MAX_RETRIES = 3; + +/** Base delay for exponential backoff (ms). Doubled on each retry. */ +const BACKOFF_BASE_MS = 1000; + +// --------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------- + +interface FetchResult { + content: string | null; + /** Non-null when content is null โ€” e.g. "404 Not Found" or "NetworkError: ..." */ + failReason?: string; +} + +/** + * Returns true for HTTP status codes that are transient and worth retrying: + * - 429 Too Many Requests + * - 5xx Server errors + */ +function isRetryableStatus(status: number): boolean { + return status === 429 || status >= 500; +} + +/** + * Fetches a URL as text with exponential backoff for transient errors. + * + * Retries on 429 (rate-limited) and 5xx (server errors). Respects + * Retry-After headers when present. Non-retryable failures (e.g., 404) + * are returned immediately without retry. + */ +async function fetchText(url: string): Promise { + let lastError: string | undefined; + + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + try { + const response = await fetch(url); + + if (response.ok) { + return { content: await response.text() }; + } + + const reason = `${response.status} ${response.statusText}`; + + if (!isRetryableStatus(response.status)) { + // Non-retryable (e.g., 404, 403) โ€” fail immediately + return { content: null, failReason: reason }; + } + + lastError = reason; + + // Calculate backoff: honour Retry-After header if present, + // otherwise use exponential backoff + const retryAfter = response.headers.get('Retry-After'); + let delayMs: number; + if (retryAfter) { + const seconds = Number(retryAfter); + delayMs = Number.isNaN(seconds) ? BACKOFF_BASE_MS * 2 ** attempt : seconds * 1000; + } else { + delayMs = BACKOFF_BASE_MS * 2 ** attempt; + } + + if (attempt < MAX_RETRIES) { + console.log( + `\n โณ ${reason} for ${url} โ€” retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES})`, + ); + await sleep(delayMs); + } + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + lastError = `NetworkError: ${msg}`; + + if (attempt < MAX_RETRIES) { + const delayMs = BACKOFF_BASE_MS * 2 ** attempt; + console.log(`\n โณ ${lastError} โ€” retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES})`); + await sleep(delayMs); + } + } + } + + return { content: null, failReason: lastError }; +} + +interface FetchJsonResult { + data: T | null; + failReason?: string; +} + +/** + * Fetches a URL as JSON with exponential backoff for transient errors. + * Same retry semantics as {@link fetchText}. + */ +async function fetchJson(url: string): Promise> { + let lastError: string | undefined; + + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + try { + const response = await fetch(url); + + if (response.ok) { + return { data: (await response.json()) as T }; + } + + const reason = `${response.status} ${response.statusText}`; + + if (!isRetryableStatus(response.status)) { + return { data: null, failReason: reason }; + } + + lastError = reason; + + const retryAfter = response.headers.get('Retry-After'); + let delayMs: number; + if (retryAfter) { + const seconds = Number(retryAfter); + delayMs = Number.isNaN(seconds) ? BACKOFF_BASE_MS * 2 ** attempt : seconds * 1000; + } else { + delayMs = BACKOFF_BASE_MS * 2 ** attempt; + } + + if (attempt < MAX_RETRIES) { + console.log( + `\n โณ ${reason} for ${url} โ€” retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES})`, + ); + await sleep(delayMs); + } + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + lastError = `NetworkError: ${msg}`; + + if (attempt < MAX_RETRIES) { + const delayMs = BACKOFF_BASE_MS * 2 ** attempt; + console.log(`\n โณ ${lastError} โ€” retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES})`); + await sleep(delayMs); + } + } + } + + return { data: null, failReason: lastError }; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Resolves a category name to its docs directory. + */ +function getCategoryDir(category: string): string | undefined { + return CATEGORY_TO_DIR[category]; +} + +/** + * Extracts the YAML frontmatter description from a docs Markdown file. + * Normalizes CRLF line endings before parsing. + */ +function extractDescription(markdown: string): string | undefined { + const normalized = markdown.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + const fmMatch = normalized.match(/^---\s*\n([\s\S]*?)\n---/); + if (!fmMatch) return undefined; + + const frontmatter = fmMatch[1]; + // Look for description field โ€” may be indented (e.g. " description: ...") + const descMatch = frontmatter.match(/^\s*description:\s*(.+)$/m); + if (descMatch) { + return descMatch[1].trim().replace(/^['"]|['"]$/g, ''); + } + return undefined; +} + +/** + * Extracts the first code block after a ## Syntax heading. + * Normalizes CRLF line endings to LF. + */ +function extractSyntax(markdown: string): string | undefined { + // Find ## Syntax (or ### Syntax) section + const syntaxSectionMatch = markdown.match(/##\s*Syntax\s*\n([\s\S]*?)(?=\n##\s|\n$)/i); + if (!syntaxSectionMatch) return undefined; + + const section = syntaxSectionMatch[1]; + // Find first code block in this section + const codeBlockMatch = section.match(/```[\w]*\s*\n([\s\S]*?)```/); + if (codeBlockMatch) { + return codeBlockMatch[1].replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim(); + } + return undefined; +} + +/** + * Escape pipe characters and collapse whitespace in table cell content. + * Handles both \n and \r\n line endings (GitHub raw content may use CRLF). + */ +function escapeTableCell(text: string): string { + return text + .replace(/\r\n|\r|\n/g, ' ') + .replace(/\\/g, '\\\\') + .replace(/\|/g, '\\|') + .replace(/\s+/g, ' ') + .trim(); +} + +// --------------------------------------------------------------------------- +// Phase 0: Verification +// --------------------------------------------------------------------------- + +interface VerificationResult { + passed: boolean; + checks: Array<{ name: string; passed: boolean; detail: string }>; +} + +async function runVerification(): Promise { + console.log(''); + console.log('='.repeat(60)); + console.log(' VERIFICATION STEP'); + console.log(' Checking that upstream documentation structure is as expected'); + console.log('='.repeat(60)); + console.log(''); + + const checks: VerificationResult['checks'] = []; + + // Check 1: Compatibility page is accessible and has expected structure + console.log(' [1/4] Fetching compatibility page...'); + const compatResult = await fetchText(COMPAT_PAGE_URL); + if (compatResult.content) { + const hasTable = /\|.*\|.*\|/.test(compatResult.content); + const hasOperators = /\$\w+/.test(compatResult.content); + const passed = hasTable && hasOperators; + checks.push({ + name: 'Compatibility page accessible & has tables + operators', + passed, + detail: passed + ? `OK โ€” ${(compatResult.content.length / 1024).toFixed(1)} KB, tables found` + : `FAIL โ€” tables: ${hasTable}, operators: ${hasOperators}`, + }); + } else { + checks.push({ + name: 'Compatibility page accessible', + passed: false, + detail: `FAIL โ€” could not fetch ${COMPAT_PAGE_URL} (${compatResult.failReason})`, + }); + } + + // Check 2: A known operator doc page exists ($match โ€” aggregation stage) + console.log(' [2/4] Fetching known operator page ($match)...'); + const matchUrl = `${OPERATOR_DOC_BASE}/aggregation/$match.md`; + const matchResult = await fetchText(matchUrl); + if (matchResult.content) { + const hasDescription = extractDescription(matchResult.content) !== undefined; + checks.push({ + name: '$match doc page has YAML frontmatter with description', + passed: hasDescription, + detail: hasDescription + ? `OK โ€” description: "${extractDescription(matchResult.content)}"` + : 'FAIL โ€” no description in frontmatter', + }); + } else { + checks.push({ + name: '$match doc page accessible', + passed: false, + detail: `FAIL โ€” could not fetch ${matchUrl} (${matchResult.failReason})`, + }); + } + + // Check 3: A known query operator doc page exists ($eq โ€” comparison query) + console.log(' [3/4] Fetching known operator page ($eq)...'); + const eqUrl = `${OPERATOR_DOC_BASE}/comparison-query/$eq.md`; + const eqResult = await fetchText(eqUrl); + if (eqResult.content) { + const desc = extractDescription(eqResult.content); + const syntax = extractSyntax(eqResult.content); + const passed = desc !== undefined; + checks.push({ + name: '$eq doc page has frontmatter description', + passed, + detail: passed + ? `OK โ€” description: "${desc}", syntax: ${syntax ? 'found' : 'not found'}` + : 'FAIL โ€” no description in frontmatter', + }); + } else { + checks.push({ + name: '$eq doc page accessible', + passed: false, + detail: `FAIL โ€” could not fetch ${eqUrl} (${eqResult.failReason})`, + }); + } + + // Check 4: A known accumulator doc page exists ($sum) + console.log(' [4/4] Fetching known operator page ($sum)...'); + const sumUrl = `${OPERATOR_DOC_BASE}/accumulators/$sum.md`; + const sumResult = await fetchText(sumUrl); + if (sumResult.content) { + const desc = extractDescription(sumResult.content); + const passed = desc !== undefined; + checks.push({ + name: '$sum doc page has frontmatter description', + passed, + detail: passed ? `OK โ€” description: "${desc}"` : 'FAIL โ€” no description in frontmatter', + }); + } else { + checks.push({ + name: '$sum doc page accessible', + passed: false, + detail: `FAIL โ€” could not fetch ${sumUrl} (${sumResult.failReason})`, + }); + } + + // Print results + console.log(''); + console.log('-'.repeat(60)); + console.log(' Verification Results:'); + console.log('-'.repeat(60)); + const allPassed = checks.every((c) => c.passed); + for (const check of checks) { + const icon = check.passed ? 'โœ…' : 'โŒ'; + console.log(` ${icon} ${check.name}`); + console.log(` ${check.detail}`); + } + console.log('-'.repeat(60)); + if (allPassed) { + console.log(' โœ… VERIFICATION PASSED โ€” all checks succeeded'); + } else { + console.log(' โŒ VERIFICATION FAILED โ€” some checks did not pass'); + console.log(' The documentation structure may have changed.'); + console.log(' Review the failures above before proceeding.'); + } + console.log('='.repeat(60)); + console.log(''); + + return { passed: allPassed, checks }; +} + +// --------------------------------------------------------------------------- +// Phase 1: Compatibility table extraction +// --------------------------------------------------------------------------- + +/** + * Sections we explicitly skip (not operators โ€” commands, indexes, etc.) + */ +const SKIP_SECTIONS = ['Database commands', 'Index types', 'Index properties', 'Related content']; + +function parseCompatibilityTables(markdown: string): OperatorInfo[] { + const operators: OperatorInfo[] = []; + const lines = markdown.split('\n'); + + // The compatibility page has a single "## Operators" section with one big table: + // | Category | Operator | Supported (v5.0) | Supported (v6.0) | Supported (v7.0) | Supported (v8.0) | + // | --- | --- | --- | --- | --- | --- | + // | Comparison Query Operators | `$eq` | โœ… Yes | โœ… Yes | โœ… Yes | โœ… Yes | + + let currentSection = ''; + let inTable = false; + let separatorSeen = false; + + for (const line of lines) { + // Detect section headings + const h2Match = line.match(/^##\s+(.+)/); + if (h2Match) { + currentSection = h2Match[1].trim(); + inTable = false; + separatorSeen = false; + continue; + } + + // Skip sections we don't care about + if (SKIP_SECTIONS.some((s) => currentSection.startsWith(s))) { + continue; + } + + // Only process lines that start with | + if (!line.startsWith('|')) { + if (inTable) { + inTable = false; + separatorSeen = false; + } + continue; + } + + // Parse table rows + const cells = line + .split('|') + .map((c) => c.trim()) + .filter((c) => c.length > 0); + + if (cells.length < 2) continue; + + // Detect separator row (| --- | --- | ... |) + if (cells.every((c) => /^-+$/.test(c) || /^:?-+:?$/.test(c))) { + separatorSeen = true; + inTable = true; + continue; + } + + // Skip header row (before separator) + if (!separatorSeen) { + continue; + } + + // Data row: | Category | Operator | v5.0 | v6.0 | v7.0 | v8.0 | + if (inTable && cells.length >= 2) { + const rawCategory = cells[0].replace(/`/g, '').replace(/\*\*/g, '').trim(); + let rawOperator = cells[1]; + + // Extract from markdown links like [`$eq`](...) + const linkMatch = rawOperator.match(/\[([^\]]+)\]/); + if (linkMatch) { + rawOperator = linkMatch[1]; + } + rawOperator = rawOperator.replace(/`/g, '').replace(/\*+$/, '').trim(); + + // Handle $[identifier] which may be parsed incorrectly + // The compat page shows `$[identifier]` โ€” square brackets get stripped by link parsing + if (rawOperator === 'identifier' && rawCategory.includes('Array Update')) { + rawOperator = '$[identifier]'; + } + + // For Variables in Aggregation Expressions, add $$ prefix + if (rawCategory === 'Variables in Aggregation Expressions' && !rawOperator.startsWith('$')) { + rawOperator = '$$' + rawOperator; + } + + if (!rawOperator || rawOperator === 'Operator' || rawOperator === 'Command') { + continue; + } + + // Skip summary table rows where "operator" column contains numbers + // (e.g., "| **Aggregation Stages** | 60 | 58 | 96.67% |") + if (/^\d+$/.test(rawOperator)) { + continue; + } + + // Skip rows where category contains percentage or "Total" + if (rawCategory.includes('%') || rawCategory === 'Total') { + continue; + } + + // Check support status from version columns (cells 2+) + const versionCells = cells.slice(2); + const hasYes = versionCells.some((c) => c.includes('Yes') || c.includes('โœ…') || c.includes('โœ“')); + const hasNo = versionCells.some((c) => c.includes('No') || c.includes('โŒ') || c.includes('โœ—')); + const hasDeprecated = versionCells.some((c) => c.toLowerCase().includes('deprecated')); + + let listed: boolean; + let notListedReason: string | undefined; + + if (hasDeprecated) { + listed = false; + const depCell = versionCells.find((c) => c.toLowerCase().includes('deprecated')); + notListedReason = depCell?.replace(/[*`]/g, '').trim() || 'Deprecated'; + } else if (hasNo && !hasYes) { + listed = false; + notListedReason = 'Not in scope'; + } else { + listed = true; + } + + operators.push({ + operator: rawOperator, + category: rawCategory, + listed, + notListedReason, + }); + } + } + + return operators; +} + +// --------------------------------------------------------------------------- +// Phase 2: Per-operator doc fetching +// --------------------------------------------------------------------------- + +/** + * Builds a global index of all operator doc files in the docs repo + * by crawling each known directory. Returns a map from lowercase filename + * (e.g. "$eq.md") to the directory path it lives in. + * + * This allows the scraper to find operators that are filed in a different + * directory than expected (e.g. $cmp is a comparison expression operator + * but lives in comparison-query/). + */ +async function buildGlobalFileIndex(): Promise> { + const GITHUB_API_BASE = + 'https://api.github.com/repos/MicrosoftDocs/azure-databases-docs/contents/articles/documentdb/operators'; + + type GithubEntry = { name: string; type: string }; + const index = new Map(); + + const rootResult = await fetchJson(GITHUB_API_BASE); + if (!rootResult.data) { + console.log( + ` โš  Could not fetch directory listing from GitHub API โ€” skipping global index (${rootResult.failReason})`, + ); + return index; + } + + const dirs = rootResult.data.filter((d) => d.type === 'dir' && d.name !== 'includes'); + + for (const dir of dirs) { + await sleep(300); // Rate limit GitHub API + + const dirResult = await fetchJson(`${GITHUB_API_BASE}/${dir.name}`); + if (!dirResult.data) continue; + + const files = dirResult.data.filter((f) => f.name.endsWith('.md')); + const subdirs = dirResult.data.filter((f) => f.type === 'dir'); + + for (const file of files) { + index.set(file.name.toLowerCase(), dir.name); + } + + // Also check subdirectories (e.g., aggregation/type-expression/) + for (const sub of subdirs) { + await sleep(300); + + const subResult = await fetchJson(`${GITHUB_API_BASE}/${dir.name}/${sub.name}`); + if (!subResult.data) continue; + + for (const file of subResult.data.filter((f) => f.name.endsWith('.md'))) { + index.set(file.name.toLowerCase(), `${dir.name}/${sub.name}`); + } + } + } + + return index; +} + +interface FetchOperatorDocsResult { + failureDetails: { operator: string; category: string; reason: string }[]; +} + +async function fetchOperatorDocs(operators: OperatorInfo[]): Promise { + // Build a global index of all doc files to use as fallback + console.log(' Building global file index from GitHub API...'); + const globalIndex = await buildGlobalFileIndex(); + console.log(` Global index: ${globalIndex.size} files found across all directories`); + console.log(''); + + // Only fetch for listed operators that have a doc directory or are in global index + const fetchable = operators.filter((op) => { + if (!op.listed) return false; + const dir = getCategoryDir(op.category); + // Skip operators whose category maps to empty string (e.g. system variables) + if (dir === '') return false; + // Include if we have a directory mapping OR if the file exists in the global index + const opFileName = op.operator.toLowerCase() + '.md'; + return dir !== undefined || globalIndex.has(opFileName); + }); + const total = fetchable.length; + let fetched = 0; + let succeeded = 0; + let failed = 0; + const skipped = operators.filter((op) => op.listed).length - total; + + const failureDetails: { operator: string; category: string; reason: string }[] = []; + + console.log(` Phase 2: Fetching per-operator doc pages (${total} operators, ${skipped} skipped)...`); + console.log(''); + + // Process in batches + for (let i = 0; i < fetchable.length; i += BATCH_SIZE) { + const batch = fetchable.slice(i, i + BATCH_SIZE); + + const promises = batch.map(async (op) => { + const primaryDir = getCategoryDir(op.category); + const opNameLower = op.operator.toLowerCase(); + const opNameOriginal = op.operator; + const opFileName = opNameLower + '.md'; + + // Strategy: + // 1. Try primary directory (lowercase filename) + // 2. Try primary directory (original casing) + // 3. Try global index fallback directory (lowercase filename) + // 4. Try global index fallback directory (original casing) + let content: string | null = null; + let resolvedDir: string | undefined; + let lastFailReason: string | undefined; + + if (primaryDir) { + const result = await fetchText(`${OPERATOR_DOC_BASE}/${primaryDir}/${opNameLower}.md`); + if (result.content) { + content = result.content; + resolvedDir = primaryDir; + } else { + lastFailReason = result.failReason; + if (opNameLower !== opNameOriginal) { + const result2 = await fetchText(`${OPERATOR_DOC_BASE}/${primaryDir}/${opNameOriginal}.md`); + if (result2.content) { + content = result2.content; + resolvedDir = primaryDir; + } else { + lastFailReason = result2.failReason; + } + } + } + } + + // Fallback: check global index for a different directory + if (!content && globalIndex.has(opFileName)) { + const fallbackDir = globalIndex.get(opFileName)!; + if (fallbackDir !== primaryDir) { + const result3 = await fetchText(`${OPERATOR_DOC_BASE}/${fallbackDir}/${opFileName}`); + if (result3.content) { + content = result3.content; + resolvedDir = fallbackDir; + } else { + lastFailReason = result3.failReason; + } + } + } + + if (content) { + op.description = extractDescription(content); + op.syntax = extractSyntax(content); + + if (primaryDir && resolvedDir !== primaryDir) { + // Doc page found in a different directory โ€” emit 'none' + // so the generator can cross-reference alternative URLs. + // Description/syntax were still scraped from the fallback page. + op.docLink = 'none'; + op.scraperComment = + `Doc page not found in expected directory '${primaryDir}/'. ` + + `Content scraped from '${resolvedDir}/'.`; + } else { + op.docLink = `${DOC_LINK_BASE}/${resolvedDir}/${opNameLower}`; + } + succeeded++; + } else { + failureDetails.push({ + operator: op.operator, + category: op.category, + reason: lastFailReason ?? 'Unknown', + }); + failed++; + } + fetched++; + }); + + await Promise.all(promises); + + // Progress output + const pct = ((fetched / total) * 100).toFixed(0); + process.stdout.write(`\r Progress: ${fetched}/${total} (${pct}%) โ€” ${succeeded} succeeded, ${failed} failed`); + + // Rate limiting between batches + if (i + BATCH_SIZE < fetchable.length) { + await sleep(BATCH_DELAY_MS); + } + } + + console.log(''); // newline after progress + console.log(` Phase 2 complete: ${succeeded}/${total} docs fetched successfully`); + if (failed > 0) { + console.log(` โš  ${failed} operators could not be fetched (will have empty descriptions)`); + console.log(''); + + // Group failures by reason for a clear summary + const byReason = new Map(); + for (const f of failureDetails) { + const list = byReason.get(f.reason) ?? []; + list.push(f); + byReason.set(f.reason, list); + } + + for (const [reason, ops] of byReason) { + console.log(` [${reason}] (${ops.length} operators):`); + for (const f of ops) { + const dir = getCategoryDir(f.category) || '???'; + const fallback = globalIndex.get(f.operator.toLowerCase() + '.md'); + const extra = fallback && fallback !== dir ? ` (also tried ${fallback})` : ''; + console.log(` - ${f.operator} (${f.category} โ†’ ${dir}${extra})`); + } + console.log(''); + } + } + + return { failureDetails }; +} + +// --------------------------------------------------------------------------- +// Phase 3: Dump generation +// --------------------------------------------------------------------------- + +function generateDump(operators: OperatorInfo[]): string { + const now = new Date().toISOString().split('T')[0]; + const lines: string[] = []; + + lines.push('# DocumentDB Operator Reference'); + lines.push(''); + lines.push(''); + lines.push(``); + lines.push(''); + lines.push(''); + + // Summary table (compact โ€” stays as a table) + const categories = new Map(); + for (const op of operators) { + if (!categories.has(op.category)) { + categories.set(op.category, { listed: 0, notListed: 0 }); + } + const cat = categories.get(op.category)!; + if (op.listed) { + cat.listed++; + } else { + cat.notListed++; + } + } + + lines.push('## Summary'); + lines.push(''); + lines.push('| Category | Listed | Total |'); + lines.push('| --- | --- | --- |'); + let totalListed = 0; + let totalAll = 0; + for (const [cat, counts] of categories) { + const total = counts.listed + counts.notListed; + totalListed += counts.listed; + totalAll += total; + lines.push(`| ${escapeTableCell(cat)} | ${counts.listed} | ${total} |`); + } + lines.push(`| **Total** | **${totalListed}** | **${totalAll}** |`); + lines.push(''); + + // Per-category sections with structured operator entries + const categoriesInOrder = [...categories.keys()]; + for (const cat of categoriesInOrder) { + const catOps = operators.filter((op) => op.category === cat && op.listed); + if (catOps.length === 0) continue; + + lines.push(`## ${cat}`); + lines.push(''); + + for (const op of catOps) { + lines.push(`### ${op.operator}`); + lines.push(''); + if (op.description) { + lines.push(`- **Description:** ${op.description}`); + } + if (op.syntax) { + lines.push('- **Syntax:**'); + lines.push(''); + lines.push('```javascript'); + lines.push(op.syntax); + lines.push('```'); + lines.push(''); + } + if (op.docLink) { + lines.push(`- **Doc Link:** ${op.docLink}`); + } + if (op.scraperComment) { + lines.push(`- **Scraper Comment:** ${op.scraperComment}`); + } + lines.push(''); + } + } + + // Not-listed operators section + const notListed = operators.filter((op) => !op.listed); + if (notListed.length > 0) { + lines.push('## Not Listed'); + lines.push(''); + lines.push('Operators below are present on the compatibility page but are not in scope'); + lines.push('for this package (deprecated or not available in DocumentDB).'); + lines.push(''); + for (const op of notListed) { + lines.push(`- **${op.operator}** (${op.category}) โ€” ${op.notListedReason || 'Not in scope'}`); + } + lines.push(''); + } + + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main(): Promise { + console.log('DocumentDB Operator Documentation Scraper'); + console.log('========================================='); + console.log(''); + + // Phase 0: Verification + const verification = await runVerification(); + if (!verification.passed) { + console.error('Aborting due to verification failure.'); + console.error('If the documentation structure has changed, update the scraper accordingly.'); + process.exit(1); + } + + // Phase 1: Fetch and parse compatibility page + console.log(' Phase 1: Fetching compatibility page...'); + const compatResult = await fetchText(COMPAT_PAGE_URL); + if (!compatResult.content) { + console.error(`ERROR: Could not fetch compatibility page (${compatResult.failReason})`); + process.exit(1); + } + console.log(` Fetched ${(compatResult.content.length / 1024).toFixed(1)} KB`); + + const operators = parseCompatibilityTables(compatResult.content); + const listed = operators.filter((op) => op.listed); + const notListed = operators.filter((op) => !op.listed); + console.log(` Parsed ${operators.length} operators (${listed.length} listed, ${notListed.length} not listed)`); + console.log(''); + + // Phase 2: Fetch per-operator docs + const { failureDetails } = await fetchOperatorDocs(operators); + console.log(''); + + // Fail immediately on network errors (transient connectivity problems that + // exhaust all retries). 404s are expected for operators without dedicated + // doc pages and do not abort the run. + const networkFailures = failureDetails.filter((f) => f.reason.startsWith('NetworkError:')); + if (networkFailures.length > 0) { + console.error(`ERROR: ${networkFailures.length} operator(s) failed due to network errors (not 404). Aborting.`); + for (const f of networkFailures) { + console.error(` - ${f.operator} (${f.category}): ${f.reason}`); + } + process.exit(1); + } + + // Phase 3: Generate dump + console.log(' Phase 3: Generating scraped/operator-reference.md...'); + const dump = generateDump(operators); + + const outputDir = path.join(__dirname, '..', 'resources', 'scraped'); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const outputPath = path.join(outputDir, 'operator-reference.md'); + fs.writeFileSync(outputPath, dump, 'utf-8'); + + console.log(` Written to: ${outputPath}`); + console.log(` File size: ${(dump.length / 1024).toFixed(1)} KB`); + console.log(''); + console.log('Done! Review the generated file and commit it to the repo.'); +} + +main().catch((err) => { + console.error('Scraper failed:', err); + process.exit(1); +}); diff --git a/packages/documentdb-constants/src/accumulators.ts b/packages/documentdb-constants/src/accumulators.ts new file mode 100644 index 000000000..c2d4d97d0 --- /dev/null +++ b/packages/documentdb-constants/src/accumulators.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED โ€” DO NOT EDIT BY HAND +// +// Generated by: npm run generate (scripts/generate-from-reference.ts) +// Sources: resources/scraped/operator-reference.md +// resources/overrides/operator-overrides.md +// resources/overrides/operator-snippets.md +// +// To change operator data, edit the overrides/snippets files and re-run the generator. + +import { getDocLink } from './docLinks'; +import { registerOperators } from './getFilteredCompletions'; +import { META_ACCUMULATOR } from './metaTags'; +import { type OperatorEntry } from './types'; + +// --------------------------------------------------------------------------- +// Accumulators ($group, $bucket, $bucketAuto, $setWindowFields) +// --------------------------------------------------------------------------- + +const groupAccumulators: readonly OperatorEntry[] = [ + { + value: '$addToSet', + meta: META_ACCUMULATOR, + description: + "The addToSet operator adds elements to an array if they don't already exist, while ensuring uniqueness of elements within the set.", + snippet: '{ $addToSet: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$addtoset', // inferred from another category + }, + { + value: '$avg', + meta: META_ACCUMULATOR, + description: 'Computes the average of numeric values for documents in a group, bucket, or window.', + snippet: '{ $avg: "${1:\\$field}" }', + link: getDocLink('$avg', META_ACCUMULATOR), + }, + { + value: '$bottom', + meta: META_ACCUMULATOR, + description: + "The $bottom operator returns the last document from the query's result set sorted by one or more fields", + snippet: '{ $bottom: { sortBy: { ${1:field}: ${2:1} }, output: "${3:\\$field}" } }', + link: getDocLink('$bottom', META_ACCUMULATOR), + }, + { + value: '$bottomN', + meta: META_ACCUMULATOR, + description: 'The $bottomN operator returns the last N documents from the result sorted by one or more fields', + snippet: '{ $bottomN: { n: ${1:number}, sortBy: { ${2:field}: ${3:1} }, output: "${4:\\$field}" } }', + link: getDocLink('$bottomN', META_ACCUMULATOR), + }, + { + value: '$count', + meta: META_ACCUMULATOR, + description: + 'The `$count` operator is used to count the number of documents that match a query filtering criteria.', + snippet: '{ $count: {} }', + link: getDocLink('$count', META_ACCUMULATOR), + }, + { + value: '$first', + meta: META_ACCUMULATOR, + description: "The $first operator returns the first value in a group according to the group's sorting order.", + snippet: '{ $first: "${1:\\$field}" }', + link: getDocLink('$first', META_ACCUMULATOR), + }, + { + value: '$firstN', + meta: META_ACCUMULATOR, + description: + 'The $firstN operator sorts documents on one or more fields specified by the query and returns the first N document matching the filtering criteria', + snippet: '{ $firstN: { input: "${1:\\$field}", n: ${2:number} } }', + link: getDocLink('$firstN', META_ACCUMULATOR), + }, + { + value: '$last', + meta: META_ACCUMULATOR, + description: 'The $last operator returns the last document from the result sorted by one or more fields', + snippet: '{ $last: "${1:\\$field}" }', + link: getDocLink('$last', META_ACCUMULATOR), + }, + { + value: '$lastN', + meta: META_ACCUMULATOR, + description: 'The $lastN accumulator operator returns the last N values in a group of documents.', + snippet: '{ $lastN: { input: "${1:\\$field}", n: ${2:number} } }', + link: getDocLink('$lastN', META_ACCUMULATOR), + }, + { + value: '$max', + meta: META_ACCUMULATOR, + description: 'The $max operator returns the maximum value from a set of input values.', + snippet: '{ $max: "${1:\\$field}" }', + link: getDocLink('$max', META_ACCUMULATOR), + }, + { + value: '$maxN', + meta: META_ACCUMULATOR, + description: 'Retrieves the top N values based on a specified filtering criteria', + snippet: '{ $maxN: { input: "${1:\\$field}", n: ${2:number} } }', + link: getDocLink('$maxN', META_ACCUMULATOR), + }, + { + value: '$median', + meta: META_ACCUMULATOR, + description: 'The $median operator calculates the median value of a numeric field in a group of documents.', + snippet: '{ $median: { input: "${1:\\$field}", method: "approximate" } }', + link: getDocLink('$median', META_ACCUMULATOR), + }, + { + value: '$mergeObjects', + meta: META_ACCUMULATOR, + description: 'The $mergeObjects operator merges multiple documents into a single document', + snippet: '{ $mergeObjects: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/object-expression/$mergeobjects', // inferred from another category + }, + { + value: '$min', + meta: META_ACCUMULATOR, + description: 'Retrieves the minimum value for a specified field', + snippet: '{ $min: "${1:\\$field}" }', + link: getDocLink('$min', META_ACCUMULATOR), + }, + { + value: '$percentile', + meta: META_ACCUMULATOR, + description: + 'The $percentile operator calculates the percentile of numerical values that match a filtering criteria', + snippet: '{ $percentile: { input: "${1:\\$field}", p: [${2:0.5}], method: "approximate" } }', + link: getDocLink('$percentile', META_ACCUMULATOR), + }, + { + value: '$push', + meta: META_ACCUMULATOR, + description: 'The $push operator adds a specified value to an array within a document.', + snippet: '{ $push: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$push', // inferred from another category + }, + { + value: '$stdDevPop', + meta: META_ACCUMULATOR, + description: 'The $stddevpop operator calculates the standard deviation of the specified values', + snippet: '{ $stdDevPop: "${1:\\$field}" }', + link: getDocLink('$stdDevPop', META_ACCUMULATOR), + }, + { + value: '$stdDevSamp', + meta: META_ACCUMULATOR, + description: + 'The $stddevsamp operator calculates the standard deviation of a specified sample of values and not the entire population', + snippet: '{ $stdDevSamp: "${1:\\$field}" }', + link: getDocLink('$stdDevSamp', META_ACCUMULATOR), + }, + { + value: '$sum', + meta: META_ACCUMULATOR, + description: 'The $sum operator calculates the sum of the values of a field based on a filtering criteria', + snippet: '{ $sum: "${1:\\$field}" }', + link: getDocLink('$sum', META_ACCUMULATOR), + }, + { + value: '$top', + meta: META_ACCUMULATOR, + description: 'The $top operator returns the first document from the result set sorted by one or more fields', + snippet: '{ $top: { sortBy: { ${1:field}: ${2:1} }, output: "${3:\\$field}" } }', + link: getDocLink('$top', META_ACCUMULATOR), + }, + { + value: '$topN', + meta: META_ACCUMULATOR, + description: 'The $topN operator returns the first N documents from the result sorted by one or more fields', + snippet: '{ $topN: { n: ${1:number}, sortBy: { ${2:field}: ${3:1} }, output: "${4:\\$field}" } }', + link: getDocLink('$topN', META_ACCUMULATOR), + }, +]; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +export function loadAccumulators(): void { + registerOperators([...groupAccumulators]); +} diff --git a/packages/documentdb-constants/src/bsonConstructors.ts b/packages/documentdb-constants/src/bsonConstructors.ts new file mode 100644 index 000000000..5e08a22d7 --- /dev/null +++ b/packages/documentdb-constants/src/bsonConstructors.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerOperators } from './getFilteredCompletions'; +import { META_BSON } from './metaTags'; +import { type OperatorEntry } from './types'; + +// --------------------------------------------------------------------------- +// BSON Constructor Functions +// --------------------------------------------------------------------------- + +const bsonConstructors: readonly OperatorEntry[] = [ + { + value: 'ObjectId', + meta: META_BSON, + description: 'Creates a new ObjectId value, a 12-byte unique identifier.', + snippet: 'ObjectId("${1:hex}")', + }, + { + value: 'ISODate', + meta: META_BSON, + description: 'Creates a date object from an ISO 8601 date string.', + snippet: 'ISODate("${1:2025-01-01T00:00:00Z}")', + }, + { + value: 'NumberLong', + meta: META_BSON, + description: 'Creates a 64-bit integer (long) value.', + snippet: 'NumberLong(${1:value})', + }, + { + value: 'NumberInt', + meta: META_BSON, + description: 'Creates a 32-bit integer value.', + snippet: 'NumberInt(${1:value})', + }, + { + value: 'NumberDecimal', + meta: META_BSON, + description: 'Creates a 128-bit decimal value for high-precision calculations.', + snippet: 'NumberDecimal("${1:value}")', + }, + { + value: 'BinData', + meta: META_BSON, + description: 'Creates a binary data value with a specified subtype.', + snippet: 'BinData(${1:subtype}, "${2:base64}")', + }, + { + value: 'UUID', + meta: META_BSON, + description: 'Creates a UUID (Universally Unique Identifier) value.', + snippet: 'UUID("${1:uuid}")', + }, + { + value: 'Timestamp', + meta: META_BSON, + description: 'Creates a BSON timestamp value for internal replication use.', + snippet: 'Timestamp(${1:seconds}, ${2:increment})', + }, + { + value: 'MinKey', + meta: META_BSON, + description: 'Represents the lowest possible BSON value, comparing less than all other types.', + snippet: 'MinKey()', + }, + { + value: 'MaxKey', + meta: META_BSON, + description: 'Represents the highest possible BSON value, comparing greater than all other types.', + snippet: 'MaxKey()', + }, +]; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +export function loadBsonConstructors(): void { + registerOperators(bsonConstructors); +} diff --git a/packages/documentdb-constants/src/docLinks.test.ts b/packages/documentdb-constants/src/docLinks.test.ts new file mode 100644 index 000000000..c79a53da9 --- /dev/null +++ b/packages/documentdb-constants/src/docLinks.test.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Unit tests for docLinks.ts โ€” URL generation for DocumentDB operator docs. + */ + +import { getDocBase, getDocLink } from './index'; + +describe('docLinks', () => { + test('getDocBase returns the expected base URL', () => { + expect(getDocBase()).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators'); + }); + + describe('getDocLink', () => { + test('generates correct URL for comparison query operator', () => { + const link = getDocLink('$eq', 'query:comparison'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$eq'); + }); + + test('generates correct URL for aggregation stage', () => { + const link = getDocLink('$match', 'stage'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$match'); + }); + + test('generates correct URL for accumulator', () => { + const link = getDocLink('$sum', 'accumulator'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$sum'); + }); + + test('generates correct URL for field update operator', () => { + const link = getDocLink('$set', 'update:field'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/field-update/$set'); + }); + + test('generates correct URL for array expression operator', () => { + const link = getDocLink('$filter', 'expr:array'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$filter'); + }); + + test('generates correct URL for type expression operator (nested dir)', () => { + const link = getDocLink('$convert', 'expr:type'); + expect(link).toBe( + 'https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$convert', + ); + }); + + test('generates correct URL for window operator', () => { + const link = getDocLink('$rank', 'window'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/window-operators/$rank'); + }); + + test('lowercases operator names in URLs', () => { + const link = getDocLink('$AddFields', 'stage'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$addfields'); + }); + + test('returns undefined for unknown meta tag', () => { + expect(getDocLink('$eq', 'unknown:tag')).toBeUndefined(); + }); + + test('returns undefined for BSON meta tag (no docs directory)', () => { + expect(getDocLink('ObjectId', 'bson')).toBeUndefined(); + }); + + test('returns undefined for variable meta tag (no docs directory)', () => { + expect(getDocLink('$$NOW', 'variable')).toBeUndefined(); + }); + + test('generates correct URL for boolean expression operator', () => { + const link = getDocLink('$and', 'expr:bool'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/boolean-expression/$and'); + }); + + test('generates correct URL for comparison expression operator', () => { + const link = getDocLink('$eq', 'expr:comparison'); + expect(link).toBe('https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-expression/$eq'); + }); + }); +}); diff --git a/packages/documentdb-constants/src/docLinks.ts b/packages/documentdb-constants/src/docLinks.ts new file mode 100644 index 000000000..460112548 --- /dev/null +++ b/packages/documentdb-constants/src/docLinks.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * URL generation helpers for DocumentDB documentation pages. + * + * Each operator has a documentation page at: + * https://learn.microsoft.com/en-us/azure/documentdb/operators/{category}/{operatorName} + */ + +const DOC_BASE = 'https://learn.microsoft.com/en-us/azure/documentdb/operators'; + +/** + * Maps meta tag prefixes to the docs directory name used in the + * DocumentDB documentation URL path. + */ +const META_TO_DOC_DIR: Record = { + 'query:comparison': 'comparison-query', + 'query:logical': 'logical-query', + 'query:element': 'element-query', + 'query:evaluation': 'evaluation-query', + 'query:array': 'array-query', + 'query:bitwise': 'bitwise-query', + 'query:geospatial': 'geospatial', + 'query:projection': 'projection', + 'query:misc': 'miscellaneous-query', + 'update:field': 'field-update', + 'update:array': 'array-update', + 'update:bitwise': 'bitwise-update', + stage: 'aggregation', + accumulator: 'accumulators', + 'expr:arith': 'arithmetic-expression', + 'expr:array': 'array-expression', + 'expr:bool': 'boolean-expression', + 'expr:comparison': 'comparison-expression', + 'expr:conditional': 'conditional-expression', + 'expr:date': 'date-expression', + 'expr:object': 'object-expression', + 'expr:set': 'set-expression', + 'expr:string': 'string-expression', + 'expr:trig': 'trigonometry-expression', + 'expr:type': 'aggregation/type-expression', + 'expr:datasize': 'data-size', + 'expr:timestamp': 'timestamp-expression', + 'expr:bitwise': 'bitwise', + 'expr:literal': 'literal-expression', + 'expr:misc': 'miscellaneous', + 'expr:variable': 'variable-expression', + window: 'window-operators', +}; + +/** + * Generates a documentation URL for a DocumentDB operator. + * + * @param operatorValue - the operator name, e.g. "$bucket", "$gt" + * @param meta - the meta tag, e.g. "stage", "query:comparison" + * @returns URL string or undefined if no mapping exists for the meta tag + */ +export function getDocLink(operatorValue: string, meta: string): string | undefined { + const dir = META_TO_DOC_DIR[meta]; + if (!dir) { + return undefined; + } + + // Operator names in URLs keep their $ prefix and are lowercased + const name = operatorValue.toLowerCase(); + return `${DOC_BASE}/${dir}/${name}`; +} + +/** + * Returns the base URL for the DocumentDB operators documentation. + */ +export function getDocBase(): string { + return DOC_BASE; +} diff --git a/packages/documentdb-constants/src/expressionOperators.ts b/packages/documentdb-constants/src/expressionOperators.ts new file mode 100644 index 000000000..a75905738 --- /dev/null +++ b/packages/documentdb-constants/src/expressionOperators.ts @@ -0,0 +1,1181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED โ€” DO NOT EDIT BY HAND +// +// Generated by: npm run generate (scripts/generate-from-reference.ts) +// Sources: resources/scraped/operator-reference.md +// resources/overrides/operator-overrides.md +// resources/overrides/operator-snippets.md +// +// To change operator data, edit the overrides/snippets files and re-run the generator. + +import { getDocLink } from './docLinks'; +import { registerOperators } from './getFilteredCompletions'; +import { + META_EXPR_ARITH, + META_EXPR_ARRAY, + META_EXPR_BITWISE, + META_EXPR_BOOL, + META_EXPR_COMPARISON, + META_EXPR_CONDITIONAL, + META_EXPR_DATASIZE, + META_EXPR_DATE, + META_EXPR_LITERAL, + META_EXPR_MISC, + META_EXPR_OBJECT, + META_EXPR_SET, + META_EXPR_STRING, + META_EXPR_TIMESTAMP, + META_EXPR_TRIG, + META_EXPR_TYPE, + META_EXPR_VARIABLE, +} from './metaTags'; +import { type OperatorEntry } from './types'; + +// --------------------------------------------------------------------------- +// Arithmetic Expression Operators +// --------------------------------------------------------------------------- + +const arithmeticExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$abs', + meta: META_EXPR_ARITH, + description: 'The $abs operator returns the absolute value of a number.', + snippet: '{ $abs: "${1:\\$field}" }', + link: getDocLink('$abs', META_EXPR_ARITH), + }, + { + value: '$add', + meta: META_EXPR_ARITH, + description: 'The $add operator returns the sum of two numbers or the sum of a date and numbers.', + snippet: '{ $add: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: getDocLink('$add', META_EXPR_ARITH), + }, + { + value: '$ceil', + meta: META_EXPR_ARITH, + description: 'The $ceil operator returns the smallest integer greater than or equal to the specified number.', + snippet: '{ $ceil: "${1:\\$field}" }', + link: getDocLink('$ceil', META_EXPR_ARITH), + }, + { + value: '$divide', + meta: META_EXPR_ARITH, + description: 'The $divide operator divides two numbers and returns the quotient.', + snippet: '{ $divide: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: getDocLink('$divide', META_EXPR_ARITH), + }, + { + value: '$exp', + meta: META_EXPR_ARITH, + description: 'The $exp operator raises e to the specified exponent and returns the result', + snippet: '{ $exp: "${1:\\$field}" }', + link: getDocLink('$exp', META_EXPR_ARITH), + }, + { + value: '$floor', + meta: META_EXPR_ARITH, + description: 'The $floor operator returns the largest integer less than or equal to the specified number', + snippet: '{ $floor: "${1:\\$field}" }', + link: getDocLink('$floor', META_EXPR_ARITH), + }, + { + value: '$ln', + meta: META_EXPR_ARITH, + description: 'The $ln operator calculates the natural logarithm of the input', + snippet: '{ $ln: "${1:\\$field}" }', + link: getDocLink('$ln', META_EXPR_ARITH), + }, + { + value: '$log', + meta: META_EXPR_ARITH, + description: 'The $log operator calculates the logarithm of a number in the specified base', + snippet: '{ $log: ["${1:\\$number}", ${2:base}] }', + link: getDocLink('$log', META_EXPR_ARITH), + }, + { + value: '$log10', + meta: META_EXPR_ARITH, + description: 'The $log10 operator calculates the log of a specified number in base 10', + snippet: '{ $log10: "${1:\\$field}" }', + link: getDocLink('$log10', META_EXPR_ARITH), + }, + { + value: '$mod', + meta: META_EXPR_ARITH, + description: + 'The $mod operator performs a modulo operation on the value of a field and selects documents with a specified result.', + snippet: '{ $mod: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/evaluation-query/$mod', // inferred from another category + }, + { + value: '$multiply', + meta: META_EXPR_ARITH, + description: 'The $multiply operator multiplies the input numerical values', + snippet: '{ $multiply: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: getDocLink('$multiply', META_EXPR_ARITH), + }, + { + value: '$pow', + meta: META_EXPR_ARITH, + description: + 'The `$pow` operator calculates the value of a numerical value raised to the power of a specified exponent.', + snippet: '{ $pow: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: getDocLink('$pow', META_EXPR_ARITH), + }, + { + value: '$round', + meta: META_EXPR_ARITH, + description: 'The $round operator rounds a number to a specified decimal place.', + snippet: '{ $round: ["${1:\\$field}", ${2:place}] }', + link: getDocLink('$round', META_EXPR_ARITH), + }, + { + value: '$sqrt', + meta: META_EXPR_ARITH, + description: 'The $sqrt operator calculates and returns the square root of an input number', + snippet: '{ $sqrt: "${1:\\$field}" }', + link: getDocLink('$sqrt', META_EXPR_ARITH), + }, + { + value: '$subtract', + meta: META_EXPR_ARITH, + description: 'The $subtract operator subtracts two numbers and returns the result.', + snippet: '{ $subtract: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: getDocLink('$subtract', META_EXPR_ARITH), + }, + { + value: '$trunc', + meta: META_EXPR_ARITH, + description: 'The $trunc operator truncates a number to a specified decimal place.', + snippet: '{ $trunc: "${1:\\$field}" }', + link: getDocLink('$trunc', META_EXPR_ARITH), + }, +]; + +// --------------------------------------------------------------------------- +// Array Expression Operators +// --------------------------------------------------------------------------- + +const arrayExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$arrayElemAt', + meta: META_EXPR_ARRAY, + description: 'The $arrayElemAt returns the element at the specified array index.', + snippet: '{ $arrayElemAt: ["${1:\\$array}", ${2:index}] }', + link: getDocLink('$arrayElemAt', META_EXPR_ARRAY), + }, + { + value: '$arrayToObject', + meta: META_EXPR_ARRAY, + description: 'The $arrayToObject allows converting an array into a single document.', + snippet: '{ $arrayToObject: "${1:\\$array}" }', + link: getDocLink('$arrayToObject', META_EXPR_ARRAY), + }, + { + value: '$concatArrays', + meta: META_EXPR_ARRAY, + description: 'The $concatArrays is used to combine multiple arrays into a single array.', + snippet: '{ $concatArrays: ["${1:\\$array1}", "${2:\\$array2}"] }', + link: getDocLink('$concatArrays', META_EXPR_ARRAY), + }, + { + value: '$filter', + meta: META_EXPR_ARRAY, + description: 'The $filter operator filters for elements from an array based on a specified condition.', + snippet: '{ $filter: { input: "${1:\\$array}", as: "${2:item}", cond: { ${3:expression} } } }', + link: getDocLink('$filter', META_EXPR_ARRAY), + }, + { + value: '$firstN', + meta: META_EXPR_ARRAY, + description: + 'The $firstN operator sorts documents on one or more fields specified by the query and returns the first N document matching the filtering criteria', + snippet: '{ $firstN: { input: "${1:\\$array}", n: ${2:number} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$firstn', // inferred from another category + }, + { + value: '$in', + meta: META_EXPR_ARRAY, + description: 'The $in operator matches value of a field against an array of specified values', + snippet: '{ $in: ["${1:\\$field}", "${2:\\$array}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$in', // inferred from another category + }, + { + value: '$indexOfArray', + meta: META_EXPR_ARRAY, + description: + 'The $indexOfArray operator is used to search for an element in an array and return the index of the first occurrence of the element.', + snippet: '{ $indexOfArray: ["${1:\\$array}", "${2:value}"] }', + link: getDocLink('$indexOfArray', META_EXPR_ARRAY), + }, + { + value: '$isArray', + meta: META_EXPR_ARRAY, + description: 'The $isArray operator is used to determine if a specified value is an array.', + snippet: '{ $isArray: "${1:\\$field}" }', + link: getDocLink('$isArray', META_EXPR_ARRAY), + }, + { + value: '$lastN', + meta: META_EXPR_ARRAY, + description: 'The $lastN accumulator operator returns the last N values in a group of documents.', + snippet: '{ $lastN: { input: "${1:\\$array}", n: ${2:number} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$lastn', // inferred from another category + }, + { + value: '$map', + meta: META_EXPR_ARRAY, + description: 'The $map operator allows applying an expression to each element in an array.', + snippet: '{ $map: { input: "${1:\\$array}", as: "${2:item}", in: { ${3:expression} } } }', + link: getDocLink('$map', META_EXPR_ARRAY), + }, + { + value: '$maxN', + meta: META_EXPR_ARRAY, + description: 'Retrieves the top N values based on a specified filtering criteria', + snippet: '{ $maxN: { input: "${1:\\$array}", n: ${2:number} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$maxn', // inferred from another category + }, + { + value: '$minN', + meta: META_EXPR_ARRAY, + description: 'Retrieves the bottom N values based on a specified filtering criteria', + snippet: '{ $minN: { input: "${1:\\$array}", n: ${2:number} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$minn', + }, + { + value: '$objectToArray', + meta: META_EXPR_ARRAY, + description: 'Converts an object into an array of key-value pair documents.', + snippet: '{ $objectToArray: "${1:\\$object}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/object-expression/$objecttoarray', // inferred from another category + }, + { + value: '$range', + meta: META_EXPR_ARRAY, + description: 'The $range operator allows generating an array of sequential integers.', + snippet: '{ $range: [${1:start}, ${2:end}, ${3:step}] }', + link: getDocLink('$range', META_EXPR_ARRAY), + }, + { + value: '$reduce', + meta: META_EXPR_ARRAY, + description: + 'The $reduce operator applies an expression to each element in an array & accumulate result as single value.', + snippet: '{ $reduce: { input: "${1:\\$array}", initialValue: ${2:0}, in: { ${3:expression} } } }', + link: getDocLink('$reduce', META_EXPR_ARRAY), + }, + { + value: '$reverseArray', + meta: META_EXPR_ARRAY, + description: 'The $reverseArray operator is used to reverse the order of elements in an array.', + snippet: '{ $reverseArray: "${1:\\$array}" }', + link: getDocLink('$reverseArray', META_EXPR_ARRAY), + }, + { + value: '$size', + meta: META_EXPR_ARRAY, + description: + 'The $size operator is used to query documents where an array field has a specified number of elements.', + snippet: '{ $size: "${1:\\$array}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-query/$size', // inferred from another category + }, + { + value: '$slice', + meta: META_EXPR_ARRAY, + description: 'The $slice operator returns a subset of an array from any element onwards in the array.', + snippet: '{ $slice: ["${1:\\$array}", ${2:n}] }', + link: getDocLink('$slice', META_EXPR_ARRAY), + }, + { + value: '$sortArray', + meta: META_EXPR_ARRAY, + description: 'The $sortArray operator helps in sorting the elements in an array.', + snippet: '{ $sortArray: { input: "${1:\\$array}", sortBy: { ${2:field}: ${3:1} } } }', + link: getDocLink('$sortArray', META_EXPR_ARRAY), + }, + { + value: '$zip', + meta: META_EXPR_ARRAY, + description: 'The $zip operator allows merging two or more arrays element-wise into a single array or arrays.', + snippet: '{ $zip: { inputs: ["${1:\\$array1}", "${2:\\$array2}"] } }', + link: getDocLink('$zip', META_EXPR_ARRAY), + }, +]; + +// --------------------------------------------------------------------------- +// Bitwise Operators +// --------------------------------------------------------------------------- + +const bitwiseOperators: readonly OperatorEntry[] = [ + { + value: '$bitAnd', + meta: META_EXPR_BITWISE, + description: + 'The $bitAnd operator performs a bitwise AND operation on integer values and returns the result as an integer.', + snippet: '{ $bitAnd: [${1:value1}, ${2:value2}] }', + link: getDocLink('$bitAnd', META_EXPR_BITWISE), + }, + { + value: '$bitNot', + meta: META_EXPR_BITWISE, + description: + 'The $bitNot operator performs a bitwise NOT operation on integer values and returns the result as an integer.', + snippet: '{ $bitNot: "${1:\\$field}" }', + link: getDocLink('$bitNot', META_EXPR_BITWISE), + }, + { + value: '$bitOr', + meta: META_EXPR_BITWISE, + description: + 'The $bitOr operator performs a bitwise OR operation on integer values and returns the result as an integer.', + snippet: '{ $bitOr: [${1:value1}, ${2:value2}] }', + link: getDocLink('$bitOr', META_EXPR_BITWISE), + }, + { + value: '$bitXor', + meta: META_EXPR_BITWISE, + description: 'The $bitXor operator performs a bitwise XOR operation on integer values.', + snippet: '{ $bitXor: [${1:value1}, ${2:value2}] }', + link: getDocLink('$bitXor', META_EXPR_BITWISE), + }, +]; + +// --------------------------------------------------------------------------- +// Boolean Expression Operators +// --------------------------------------------------------------------------- + +const booleanExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$and', + meta: META_EXPR_BOOL, + description: + 'The $and operator joins multiple query clauses and returns documents that match all specified conditions.', + snippet: '{ $and: ["${1:expression1}", "${2:expression2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/logical-query/$and', // inferred from another category + }, + { + value: '$not', + meta: META_EXPR_BOOL, + description: + "The $not operator performs a logical NOT operation on a specified expression, selecting documents that don't match the expression.", + snippet: '{ $not: ["${1:expression}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/logical-query/$not', // inferred from another category + }, + { + value: '$or', + meta: META_EXPR_BOOL, + description: + 'The $or operator joins query clauses with a logical OR and returns documents that match at least one of the specified conditions.', + snippet: '{ $or: ["${1:expression1}", "${2:expression2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/logical-query/$or', // inferred from another category + }, +]; + +// --------------------------------------------------------------------------- +// Comparison Expression Operators +// --------------------------------------------------------------------------- + +const comparisonExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$cmp', + meta: META_EXPR_COMPARISON, + description: 'The $cmp operator compares two values', + snippet: '{ $cmp: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$cmp', + }, + { + value: '$eq', + meta: META_EXPR_COMPARISON, + description: 'The $eq query operator compares the value of a field to a specified value', + snippet: '{ $eq: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$eq', // inferred from another category + }, + { + value: '$gt', + meta: META_EXPR_COMPARISON, + description: + 'The $gt query operator retrieves documents where the value of a field is greater than a specified value', + snippet: '{ $gt: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$gt', // inferred from another category + }, + { + value: '$gte', + meta: META_EXPR_COMPARISON, + description: + 'The $gte operator retrieves documents where the value of a field is greater than or equal to a specified value', + snippet: '{ $gte: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$gte', // inferred from another category + }, + { + value: '$lt', + meta: META_EXPR_COMPARISON, + description: 'The $lt operator retrieves documents where the value of field is less than a specified value', + snippet: '{ $lt: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$lt', // inferred from another category + }, + { + value: '$lte', + meta: META_EXPR_COMPARISON, + description: + 'The $lte operator retrieves documents where the value of a field is less than or equal to a specified value', + snippet: '{ $lte: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$lte', // inferred from another category + }, + { + value: '$ne', + meta: META_EXPR_COMPARISON, + description: "The $ne operator retrieves documents where the value of a field doesn't equal a specified value", + snippet: '{ $ne: ["${1:\\$field1}", "${2:\\$field2}"] }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/comparison-query/$ne', // inferred from another category + }, +]; + +// --------------------------------------------------------------------------- +// Data Size Operators +// --------------------------------------------------------------------------- + +const dataSizeOperators: readonly OperatorEntry[] = [ + { + value: '$bsonSize', + meta: META_EXPR_DATASIZE, + description: 'The $bsonSize operator returns the size of a document in bytes when encoded as BSON.', + snippet: '{ $bsonSize: "${1:\\$field}" }', + link: getDocLink('$bsonSize', META_EXPR_DATASIZE), + }, + { + value: '$binarySize', + meta: META_EXPR_DATASIZE, + description: 'The $binarySize operator is used to return the size of a binary data field.', + snippet: '{ $binarySize: "${1:\\$field}" }', + link: getDocLink('$binarySize', META_EXPR_DATASIZE), + }, +]; + +// --------------------------------------------------------------------------- +// Date Expression Operators +// --------------------------------------------------------------------------- + +const dateExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$dateAdd', + meta: META_EXPR_DATE, + description: 'The $dateAdd operator adds a specified number of time units (day, hour, month etc) to a date.', + snippet: '{ $dateAdd: { startDate: "${1:\\$dateField}", unit: "${2:day}", amount: ${3:1} } }', + link: getDocLink('$dateAdd', META_EXPR_DATE), + }, + { + value: '$dateDiff', + meta: META_EXPR_DATE, + description: + 'The $dateDiff operator calculates the difference between two dates in various units such as years, months, days, etc.', + snippet: '{ $dateDiff: { startDate: "${1:\\$startDate}", endDate: "${2:\\$endDate}", unit: "${3:day}" } }', + link: getDocLink('$dateDiff', META_EXPR_DATE), + }, + { + value: '$dateFromParts', + meta: META_EXPR_DATE, + description: 'The $dateFromParts operator constructs a date from individual components.', + snippet: '{ $dateFromParts: { year: ${1:2024}, month: ${2:1}, day: ${3:1} } }', + link: getDocLink('$dateFromParts', META_EXPR_DATE), + }, + { + value: '$dateFromString', + meta: META_EXPR_DATE, + description: 'The $dateDiff operator converts a date/time string to a date object.', + snippet: '{ $dateFromString: { dateString: "${1:dateString}" } }', + link: getDocLink('$dateFromString', META_EXPR_DATE), + }, + { + value: '$dateSubtract', + meta: META_EXPR_DATE, + description: 'The $dateSubtract operator subtracts a specified amount of time from a date.', + snippet: '{ $dateSubtract: { startDate: "${1:\\$dateField}", unit: "${2:day}", amount: ${3:1} } }', + link: getDocLink('$dateSubtract', META_EXPR_DATE), + }, + { + value: '$dateToParts', + meta: META_EXPR_DATE, + description: + 'The $dateToParts operator decomposes a date into its individual parts such as year, month, day, and more.', + snippet: '{ $dateToParts: { date: "${1:\\$dateField}" } }', + link: getDocLink('$dateToParts', META_EXPR_DATE), + }, + { + value: '$dateToString', + meta: META_EXPR_DATE, + description: 'The $dateToString operator converts a date object into a formatted string.', + snippet: '{ $dateToString: { format: "${1:%Y-%m-%d}", date: "${2:\\$dateField}" } }', + link: getDocLink('$dateToString', META_EXPR_DATE), + }, + { + value: '$dateTrunc', + meta: META_EXPR_DATE, + description: 'The $dateTrunc operator truncates a date to a specified unit.', + snippet: '{ $dateTrunc: { date: "${1:\\$dateField}", unit: "${2:day}" } }', + link: getDocLink('$dateTrunc', META_EXPR_DATE), + }, + { + value: '$dayOfMonth', + meta: META_EXPR_DATE, + description: 'The $dayOfMonth operator extracts the day of the month from a date.', + snippet: '{ $dayOfMonth: "${1:\\$dateField}" }', + link: getDocLink('$dayOfMonth', META_EXPR_DATE), + }, + { + value: '$dayOfWeek', + meta: META_EXPR_DATE, + description: 'The $dayOfWeek operator extracts the day of the week from a date.', + snippet: '{ $dayOfWeek: "${1:\\$dateField}" }', + link: getDocLink('$dayOfWeek', META_EXPR_DATE), + }, + { + value: '$dayOfYear', + meta: META_EXPR_DATE, + description: 'The $dayOfYear operator extracts the day of the year from a date.', + snippet: '{ $dayOfYear: "${1:\\$dateField}" }', + link: getDocLink('$dayOfYear', META_EXPR_DATE), + }, + { + value: '$hour', + meta: META_EXPR_DATE, + description: 'The $hour operator returns the hour portion of a date as a number between 0 and 23.', + snippet: '{ $hour: "${1:\\$dateField}" }', + link: getDocLink('$hour', META_EXPR_DATE), + }, + { + value: '$isoDayOfWeek', + meta: META_EXPR_DATE, + description: + 'The $isoDayOfWeek operator returns the weekday number in ISO 8601 format, ranging from 1 (Monday) to 7 (Sunday).', + snippet: '{ $isoDayOfWeek: "${1:\\$dateField}" }', + link: getDocLink('$isoDayOfWeek', META_EXPR_DATE), + }, + { + value: '$isoWeek', + meta: META_EXPR_DATE, + description: + 'The $isoWeek operator returns the week number of the year in ISO 8601 format, ranging from 1 to 53.', + snippet: '{ $isoWeek: "${1:\\$dateField}" }', + link: getDocLink('$isoWeek', META_EXPR_DATE), + }, + { + value: '$isoWeekYear', + meta: META_EXPR_DATE, + description: + 'The $isoWeekYear operator returns the year number in ISO 8601 format, which can differ from the calendar year for dates at the beginning or end of the year.', + snippet: '{ $isoWeekYear: "${1:\\$dateField}" }', + link: getDocLink('$isoWeekYear', META_EXPR_DATE), + }, + { + value: '$millisecond', + meta: META_EXPR_DATE, + description: 'The $millisecond operator extracts the milliseconds portion from a date value.', + snippet: '{ $millisecond: "${1:\\$dateField}" }', + link: getDocLink('$millisecond', META_EXPR_DATE), + }, + { + value: '$minute', + meta: META_EXPR_DATE, + description: 'The $minute operator extracts the minute portion from a date value.', + snippet: '{ $minute: "${1:\\$dateField}" }', + link: getDocLink('$minute', META_EXPR_DATE), + }, + { + value: '$month', + meta: META_EXPR_DATE, + description: 'The $month operator extracts the month portion from a date value.', + snippet: '{ $month: "${1:\\$dateField}" }', + link: getDocLink('$month', META_EXPR_DATE), + }, + { + value: '$second', + meta: META_EXPR_DATE, + description: 'The $second operator extracts the seconds portion from a date value.', + snippet: '{ $second: "${1:\\$dateField}" }', + link: getDocLink('$second', META_EXPR_DATE), + }, + { + value: '$toDate', + meta: META_EXPR_DATE, + description: 'The $toDate operator converts supported types to a proper Date object.', + snippet: '{ $toDate: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$todate', // inferred from another category + }, + { + value: '$week', + meta: META_EXPR_DATE, + description: 'The $week operator returns the week number for a date as a value between 0 and 53.', + snippet: '{ $week: "${1:\\$dateField}" }', + link: getDocLink('$week', META_EXPR_DATE), + }, + { + value: '$year', + meta: META_EXPR_DATE, + description: 'The $year operator returns the year for a date as a four-digit number.', + snippet: '{ $year: "${1:\\$dateField}" }', + link: getDocLink('$year', META_EXPR_DATE), + }, +]; + +// --------------------------------------------------------------------------- +// Literal Expression Operator +// --------------------------------------------------------------------------- + +const literalExpressionOperator: readonly OperatorEntry[] = [ + { + value: '$literal', + meta: META_EXPR_LITERAL, + description: + 'The $literal operator returns the specified value without parsing it as an expression, allowing literal values to be used in aggregation pipelines.', + snippet: '{ $literal: ${1:value} }', + link: getDocLink('$literal', META_EXPR_LITERAL), + }, +]; + +// --------------------------------------------------------------------------- +// Miscellaneous Operators +// --------------------------------------------------------------------------- + +const miscellaneousOperators: readonly OperatorEntry[] = [ + { + value: '$getField', + meta: META_EXPR_MISC, + description: 'The $getField operator allows retrieving the value of a specified field from a document.', + snippet: '{ $getField: { field: "${1:fieldName}", input: "${2:\\$object}" } }', + link: getDocLink('$getField', META_EXPR_MISC), + }, + { + value: '$rand', + meta: META_EXPR_MISC, + description: 'The $rand operator generates a random float value between 0 and 1.', + snippet: '{ $rand: {} }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/miscellaneous-query/$rand', // inferred from another category + }, + { + value: '$sampleRate', + meta: META_EXPR_MISC, + description: + 'The $sampleRate operator randomly samples documents from a collection based on a specified probability rate, useful for statistical analysis and testing.', + snippet: '{ $sampleRate: ${1:0.5} }', + link: getDocLink('$sampleRate', META_EXPR_MISC), + }, +]; + +// --------------------------------------------------------------------------- +// Object Expression Operators +// --------------------------------------------------------------------------- + +const objectExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$mergeObjects', + meta: META_EXPR_OBJECT, + description: 'The $mergeObjects operator merges multiple documents into a single document', + snippet: '{ $mergeObjects: ["${1:\\$object1}", "${2:\\$object2}"] }', + link: getDocLink('$mergeObjects', META_EXPR_OBJECT), + }, + { + value: '$objectToArray', + meta: META_EXPR_OBJECT, + description: + 'The objectToArray command is used to transform a document (object) into an array of key-value pairs.', + snippet: '{ $objectToArray: "${1:\\$object}" }', + link: getDocLink('$objectToArray', META_EXPR_OBJECT), + }, + { + value: '$setField', + meta: META_EXPR_OBJECT, + description: 'The setField command is used to add, update, or remove fields in embedded documents.', + snippet: '{ $setField: { field: "${1:fieldName}", input: "${2:\\$object}", value: ${3:value} } }', + link: getDocLink('$setField', META_EXPR_OBJECT), + }, +]; + +// --------------------------------------------------------------------------- +// Set Expression Operators +// --------------------------------------------------------------------------- + +const setExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$allElementsTrue', + meta: META_EXPR_SET, + description: 'The $allElementsTrue operator returns true if all elements in an array evaluate to true.', + snippet: '{ $allElementsTrue: ["${1:\\$array}"] }', + link: getDocLink('$allElementsTrue', META_EXPR_SET), + }, + { + value: '$anyElementTrue', + meta: META_EXPR_SET, + description: + 'The $anyElementTrue operator returns true if any element in an array evaluates to a value of true.', + snippet: '{ $anyElementTrue: ["${1:\\$array}"] }', + link: getDocLink('$anyElementTrue', META_EXPR_SET), + }, + { + value: '$setDifference', + meta: META_EXPR_SET, + description: + 'The $setDifference operator returns a set with elements that exist in one set but not in a second set.', + snippet: '{ $setDifference: ["${1:\\$set1}", "${2:\\$set2}"] }', + link: getDocLink('$setDifference', META_EXPR_SET), + }, + { + value: '$setEquals', + meta: META_EXPR_SET, + description: 'The $setEquals operator returns true if two sets have the same distinct elements.', + snippet: '{ $setEquals: ["${1:\\$set1}", "${2:\\$set2}"] }', + link: getDocLink('$setEquals', META_EXPR_SET), + }, + { + value: '$setIntersection', + meta: META_EXPR_SET, + description: 'The $setIntersection operator returns the common elements that appear in all input arrays.', + snippet: '{ $setIntersection: ["${1:\\$set1}", "${2:\\$set2}"] }', + link: getDocLink('$setIntersection', META_EXPR_SET), + }, + { + value: '$setIsSubset', + meta: META_EXPR_SET, + description: 'The $setIsSubset operator determines if one array is a subset of a second array.', + snippet: '{ $setIsSubset: ["${1:\\$set1}", "${2:\\$set2}"] }', + link: getDocLink('$setIsSubset', META_EXPR_SET), + }, + { + value: '$setUnion', + meta: META_EXPR_SET, + description: + 'The $setUnion operator returns an array that contains all the unique elements from the input arrays.', + snippet: '{ $setUnion: ["${1:\\$set1}", "${2:\\$set2}"] }', + link: getDocLink('$setUnion', META_EXPR_SET), + }, +]; + +// --------------------------------------------------------------------------- +// String Expression Operators +// --------------------------------------------------------------------------- + +const stringExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$concat', + meta: META_EXPR_STRING, + description: 'Concatenates two or more strings and returns the resulting string.', + snippet: '{ $concat: ["${1:\\$string1}", "${2:\\$string2}"] }', + }, + { + value: '$dateFromString', + meta: META_EXPR_STRING, + description: 'The $dateDiff operator converts a date/time string to a date object.', + snippet: '{ $dateFromString: "${1:\\$string}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datefromstring', // inferred from another category + }, + { + value: '$dateToString', + meta: META_EXPR_STRING, + description: 'The $dateToString operator converts a date object into a formatted string.', + snippet: '{ $dateToString: "${1:\\$string}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/date-expression/$datetostring', // inferred from another category + }, + { + value: '$indexOfBytes', + meta: META_EXPR_STRING, + description: 'Returns the byte index of the first occurrence of a substring within a string.', + snippet: '{ $indexOfBytes: ["${1:\\$string}", "${2:substring}"] }', + }, + { + value: '$indexOfCP', + meta: META_EXPR_STRING, + description: 'Returns the code point index of the first occurrence of a substring within a string.', + snippet: '{ $indexOfCP: ["${1:\\$string}", "${2:substring}"] }', + }, + { + value: '$ltrim', + meta: META_EXPR_STRING, + description: 'Removes whitespace or specified characters from the beginning of a string.', + snippet: '{ $ltrim: { input: "${1:\\$string}" } }', + }, + { + value: '$regexFind', + meta: META_EXPR_STRING, + description: 'Applies a regular expression to a string and returns the first match.', + snippet: '{ $regexFind: { input: "${1:\\$string}", regex: "${2:pattern}" } }', + }, + { + value: '$regexFindAll', + meta: META_EXPR_STRING, + description: 'Applies a regular expression to a string and returns all matches as an array.', + snippet: '{ $regexFindAll: { input: "${1:\\$string}", regex: "${2:pattern}" } }', + }, + { + value: '$regexMatch', + meta: META_EXPR_STRING, + description: 'Applies a regular expression to a string and returns a boolean indicating if a match was found.', + snippet: '{ $regexMatch: { input: "${1:\\$string}", regex: "${2:pattern}" } }', + }, + { + value: '$replaceOne', + meta: META_EXPR_STRING, + description: 'Replaces the first occurrence of a search string with a replacement string.', + snippet: '{ $replaceOne: { input: "${1:\\$string}", find: "${2:find}", replacement: "${3:replacement}" } }', + }, + { + value: '$replaceAll', + meta: META_EXPR_STRING, + description: 'Replaces all occurrences of a search string with a replacement string.', + snippet: '{ $replaceAll: { input: "${1:\\$string}", find: "${2:find}", replacement: "${3:replacement}" } }', + }, + { + value: '$rtrim', + meta: META_EXPR_STRING, + description: 'Removes whitespace or specified characters from the end of a string.', + snippet: '{ $rtrim: { input: "${1:\\$string}" } }', + }, + { + value: '$split', + meta: META_EXPR_STRING, + description: 'Splits a string by a delimiter and returns an array of substrings.', + snippet: '{ $split: ["${1:\\$string}", "${2:delimiter}"] }', + }, + { + value: '$strLenBytes', + meta: META_EXPR_STRING, + description: 'Returns the number of UTF-8 encoded bytes in the specified string.', + snippet: '{ $strLenBytes: "${1:\\$string}" }', + }, + { + value: '$strLenCP', + meta: META_EXPR_STRING, + description: 'Returns the number of UTF-8 code points in the specified string.', + snippet: '{ $strLenCP: "${1:\\$string}" }', + }, + { + value: '$strcasecmp', + meta: META_EXPR_STRING, + description: 'Performs a case-insensitive comparison of two strings and returns an integer.', + snippet: '{ $strcasecmp: ["${1:\\$string1}", "${2:\\$string2}"] }', + }, + { + value: '$substr', + meta: META_EXPR_STRING, + description: + 'Returns a substring of a string, starting at a specified index for a specified length. Deprecated โ€” use $substrBytes or $substrCP.', + snippet: '{ $substr: ["${1:\\$string}", ${2:start}, ${3:length}] }', + }, + { + value: '$substrBytes', + meta: META_EXPR_STRING, + description: + 'Returns a substring of a string by byte index, starting at a specified index for a specified number of bytes.', + snippet: '{ $substrBytes: ["${1:\\$string}", ${2:start}, ${3:length}] }', + }, + { + value: '$substrCP', + meta: META_EXPR_STRING, + description: + 'Returns a substring of a string by code point index, starting at a specified index for a specified number of code points.', + snippet: '{ $substrCP: ["${1:\\$string}", ${2:start}, ${3:length}] }', + }, + { + value: '$toLower', + meta: META_EXPR_STRING, + description: 'Converts a string to lowercase and returns the result.', + snippet: '{ $toLower: "${1:\\$string}" }', + }, + { + value: '$toString', + meta: META_EXPR_STRING, + description: 'The $toString operator converts an expression into a String', + snippet: '{ $toString: "${1:\\$string}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/type-expression/$tostring', // inferred from another category + }, + { + value: '$trim', + meta: META_EXPR_STRING, + description: 'Removes whitespace or specified characters from both ends of a string.', + snippet: '{ $trim: { input: "${1:\\$string}" } }', + }, + { + value: '$toUpper', + meta: META_EXPR_STRING, + description: 'Converts a string to uppercase and returns the result.', + snippet: '{ $toUpper: "${1:\\$string}" }', + }, +]; + +// --------------------------------------------------------------------------- +// Timestamp Expression Operators +// --------------------------------------------------------------------------- + +const timestampExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$tsIncrement', + meta: META_EXPR_TIMESTAMP, + description: 'The $tsIncrement operator extracts the increment portion from a timestamp value.', + snippet: '{ $tsIncrement: "${1:\\$timestampField}" }', + link: getDocLink('$tsIncrement', META_EXPR_TIMESTAMP), + }, + { + value: '$tsSecond', + meta: META_EXPR_TIMESTAMP, + description: 'The $tsSecond operator extracts the seconds portion from a timestamp value.', + snippet: '{ $tsSecond: "${1:\\$timestampField}" }', + link: getDocLink('$tsSecond', META_EXPR_TIMESTAMP), + }, +]; + +// --------------------------------------------------------------------------- +// Trigonometry Expression Operators +// --------------------------------------------------------------------------- + +const trigonometryExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$sin', + meta: META_EXPR_TRIG, + description: 'Returns the sine of a value measured in radians.', + snippet: '{ $sin: "${1:\\$value}" }', + }, + { + value: '$cos', + meta: META_EXPR_TRIG, + description: 'Returns the cosine of a value measured in radians.', + snippet: '{ $cos: "${1:\\$value}" }', + }, + { + value: '$tan', + meta: META_EXPR_TRIG, + description: 'Returns the tangent of a value measured in radians.', + snippet: '{ $tan: "${1:\\$value}" }', + }, + { + value: '$asin', + meta: META_EXPR_TRIG, + description: 'Returns the arcsine (inverse sine) of a value in radians.', + snippet: '{ $asin: "${1:\\$value}" }', + }, + { + value: '$acos', + meta: META_EXPR_TRIG, + description: 'Returns the arccosine (inverse cosine) of a value in radians.', + snippet: '{ $acos: "${1:\\$value}" }', + }, + { + value: '$atan', + meta: META_EXPR_TRIG, + description: 'Returns the arctangent (inverse tangent) of a value in radians.', + snippet: '{ $atan: "${1:\\$value}" }', + }, + { + value: '$atan2', + meta: META_EXPR_TRIG, + description: 'Returns the arctangent of the quotient of two values, using the signs to determine the quadrant.', + snippet: '{ $atan2: "${1:\\$value}" }', + }, + { + value: '$asinh', + meta: META_EXPR_TRIG, + description: 'Returns the inverse hyperbolic sine of a value.', + snippet: '{ $asinh: "${1:\\$value}" }', + }, + { + value: '$acosh', + meta: META_EXPR_TRIG, + description: 'Returns the inverse hyperbolic cosine of a value.', + snippet: '{ $acosh: "${1:\\$value}" }', + }, + { + value: '$atanh', + meta: META_EXPR_TRIG, + description: 'Returns the inverse hyperbolic tangent of a value.', + snippet: '{ $atanh: "${1:\\$value}" }', + }, + { + value: '$sinh', + meta: META_EXPR_TRIG, + description: 'Returns the hyperbolic sine of a value.', + snippet: '{ $sinh: "${1:\\$value}" }', + }, + { + value: '$cosh', + meta: META_EXPR_TRIG, + description: 'Returns the hyperbolic cosine of a value.', + snippet: '{ $cosh: "${1:\\$value}" }', + }, + { + value: '$tanh', + meta: META_EXPR_TRIG, + description: 'Returns the hyperbolic tangent of a value.', + snippet: '{ $tanh: "${1:\\$value}" }', + }, + { + value: '$degreesToRadians', + meta: META_EXPR_TRIG, + description: 'Converts a value from degrees to radians.', + snippet: '{ $degreesToRadians: "${1:\\$angle}" }', + }, + { + value: '$radiansToDegrees', + meta: META_EXPR_TRIG, + description: 'Converts a value from radians to degrees.', + snippet: '{ $radiansToDegrees: "${1:\\$angle}" }', + }, +]; + +// --------------------------------------------------------------------------- +// Type Expression Operators +// --------------------------------------------------------------------------- + +const typeExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$convert', + meta: META_EXPR_TYPE, + description: 'The $convert operator converts an expression into the specified type', + snippet: '{ $convert: { input: "${1:\\$field}", to: "${2:type}" } }', + link: getDocLink('$convert', META_EXPR_TYPE), + }, + { + value: '$isNumber', + meta: META_EXPR_TYPE, + description: 'The $isNumber operator checks if a specified expression is a numerical type', + snippet: '{ $isNumber: "${1:\\$field}" }', + link: getDocLink('$isNumber', META_EXPR_TYPE), + }, + { + value: '$toBool', + meta: META_EXPR_TYPE, + description: 'The $toBool operator converts an expression into a Boolean type', + snippet: '{ $toBool: "${1:\\$field}" }', + link: getDocLink('$toBool', META_EXPR_TYPE), + }, + { + value: '$toDate', + meta: META_EXPR_TYPE, + description: 'The $toDate operator converts supported types to a proper Date object.', + snippet: '{ $toDate: "${1:\\$field}" }', + link: getDocLink('$toDate', META_EXPR_TYPE), + }, + { + value: '$toDecimal', + meta: META_EXPR_TYPE, + description: 'The $toDecimal operator converts an expression into a Decimal type', + snippet: '{ $toDecimal: "${1:\\$field}" }', + link: getDocLink('$toDecimal', META_EXPR_TYPE), + }, + { + value: '$toDouble', + meta: META_EXPR_TYPE, + description: 'The $toDouble operator converts an expression into a Double value', + snippet: '{ $toDouble: "${1:\\$field}" }', + link: getDocLink('$toDouble', META_EXPR_TYPE), + }, + { + value: '$toInt', + meta: META_EXPR_TYPE, + description: 'The $toInt operator converts an expression into an Integer', + snippet: '{ $toInt: "${1:\\$field}" }', + link: getDocLink('$toInt', META_EXPR_TYPE), + }, + { + value: '$toLong', + meta: META_EXPR_TYPE, + description: 'The $toLong operator converts an expression into a Long value', + snippet: '{ $toLong: "${1:\\$field}" }', + link: getDocLink('$toLong', META_EXPR_TYPE), + }, + { + value: '$toObjectId', + meta: META_EXPR_TYPE, + description: 'The $toObjectId operator converts an expression into an ObjectId', + snippet: '{ $toObjectId: "${1:\\$field}" }', + link: getDocLink('$toObjectId', META_EXPR_TYPE), + }, + { + value: '$toString', + meta: META_EXPR_TYPE, + description: 'The $toString operator converts an expression into a String', + snippet: '{ $toString: "${1:\\$field}" }', + link: getDocLink('$toString', META_EXPR_TYPE), + }, + { + value: '$type', + meta: META_EXPR_TYPE, + description: 'The $type operator retrieves documents if the chosen field is of the specified type.', + snippet: '{ $type: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/element-query/$type', // inferred from another category + }, +]; + +// --------------------------------------------------------------------------- +// Variable Expression Operators +// --------------------------------------------------------------------------- + +const variableExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$let', + meta: META_EXPR_VARIABLE, + description: + 'The $let operator allows defining variables for use in a specified expression, enabling complex calculations and reducing code repetition.', + snippet: '{ $let: { vars: { ${1:var}: ${2:expression} }, in: ${3:expression} } }', + link: getDocLink('$let', META_EXPR_VARIABLE), + }, +]; + +// --------------------------------------------------------------------------- +// Conditional Expression Operators +// --------------------------------------------------------------------------- + +const conditionalExpressionOperators: readonly OperatorEntry[] = [ + { + value: '$cond', + meta: META_EXPR_CONDITIONAL, + description: + 'The $cond operator is used to evaluate a condition and return one of two expressions based on the result.', + snippet: '{ $cond: { if: { ${1:expression} }, then: ${2:trueValue}, else: ${3:falseValue} } }', + link: getDocLink('$cond', META_EXPR_CONDITIONAL), + }, + { + value: '$ifNull', + meta: META_EXPR_CONDITIONAL, + description: + 'The $ifNull operator is used to evaluate an expression and return a specified value if the expression resolves to null.', + snippet: '{ $ifNull: ["${1:\\$field}", ${2:replacement}] }', + link: getDocLink('$ifNull', META_EXPR_CONDITIONAL), + }, + { + value: '$switch', + meta: META_EXPR_CONDITIONAL, + description: + 'The $switch operator is used to evaluate a series of conditions and return a value based on the first condition that evaluates to true.', + snippet: + '{ $switch: { branches: [{ case: { ${1:expression} }, then: ${2:value} }], default: ${3:defaultValue} } }', + link: getDocLink('$switch', META_EXPR_CONDITIONAL), + }, +]; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +export function loadExpressionOperators(): void { + registerOperators([ + ...arithmeticExpressionOperators, + ...arrayExpressionOperators, + ...bitwiseOperators, + ...booleanExpressionOperators, + ...comparisonExpressionOperators, + ...dataSizeOperators, + ...dateExpressionOperators, + ...literalExpressionOperator, + ...miscellaneousOperators, + ...objectExpressionOperators, + ...setExpressionOperators, + ...stringExpressionOperators, + ...timestampExpressionOperators, + ...trigonometryExpressionOperators, + ...typeExpressionOperators, + ...variableExpressionOperators, + ...conditionalExpressionOperators, + ]); +} diff --git a/packages/documentdb-constants/src/getFilteredCompletions.test.ts b/packages/documentdb-constants/src/getFilteredCompletions.test.ts new file mode 100644 index 000000000..02e683e5d --- /dev/null +++ b/packages/documentdb-constants/src/getFilteredCompletions.test.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Unit tests for getFilteredCompletions and completion presets. + */ + +import { + EXPRESSION_COMPLETION_META, + FILTER_COMPLETION_META, + GROUP_EXPRESSION_COMPLETION_META, + PROJECTION_COMPLETION_META, + STAGE_COMPLETION_META, + UPDATE_COMPLETION_META, + WINDOW_COMPLETION_META, + getAllCompletions, + getFilteredCompletions, + loadOperators, +} from './index'; + +describe('getFilteredCompletions', () => { + test('returns all operators when filtering by all top-level meta prefixes', () => { + const all = getAllCompletions(); + expect(all.length).toBeGreaterThan(0); + }); + + test('filtering by "query" returns only query operators', () => { + const results = getFilteredCompletions({ meta: ['query'] }); + expect(results.length).toBeGreaterThan(0); + for (const r of results) { + expect(r.meta).toMatch(/^query/); + } + }); + + test('filtering by "query:comparison" returns only comparison operators', () => { + const results = getFilteredCompletions({ meta: ['query:comparison'] }); + expect(results.length).toBe(8); // $eq, $gt, $gte, $in, $lt, $lte, $ne, $nin + for (const r of results) { + expect(r.meta).toBe('query:comparison'); + } + }); + + test('filtering by "stage" returns aggregation pipeline stages', () => { + const results = getFilteredCompletions({ meta: ['stage'] }); + expect(results.length).toBe(35); + for (const r of results) { + expect(r.meta).toBe('stage'); + } + }); + + test('filtering by "update" returns all update operators', () => { + const results = getFilteredCompletions({ meta: ['update'] }); + expect(results.length).toBe(22); + for (const r of results) { + expect(r.meta).toMatch(/^update/); + } + }); + + test('filtering by "accumulator" returns accumulator operators', () => { + const results = getFilteredCompletions({ meta: ['accumulator'] }); + expect(results.length).toBe(21); + for (const r of results) { + expect(r.meta).toBe('accumulator'); + } + }); + + test('filtering by "expr" returns all expression operators', () => { + const results = getFilteredCompletions({ meta: ['expr'] }); + expect(results.length).toBeGreaterThan(100); + for (const r of results) { + expect(r.meta).toMatch(/^expr:/); + } + }); + + test('filtering by "window" returns window operators', () => { + const results = getFilteredCompletions({ meta: ['window'] }); + expect(results.length).toBe(27); + for (const r of results) { + expect(r.meta).toBe('window'); + } + }); + + test('filtering by "bson" returns BSON constructors', () => { + const results = getFilteredCompletions({ meta: ['bson'] }); + expect(results.length).toBe(10); + for (const r of results) { + expect(r.meta).toBe('bson'); + } + }); + + test('filtering by "variable" returns system variables', () => { + const results = getFilteredCompletions({ meta: ['variable'] }); + expect(results.length).toBe(7); + for (const r of results) { + expect(r.meta).toBe('variable'); + } + }); + + test('filtering by multiple meta tags combines results', () => { + const queryOnly = getFilteredCompletions({ meta: ['query'] }); + const stageOnly = getFilteredCompletions({ meta: ['stage'] }); + const combined = getFilteredCompletions({ meta: ['query', 'stage'] }); + expect(combined.length).toBe(queryOnly.length + stageOnly.length); + }); + + test('empty meta array returns no results', () => { + const results = getFilteredCompletions({ meta: [] }); + expect(results.length).toBe(0); + }); + + test('unknown meta tag returns no results', () => { + const results = getFilteredCompletions({ meta: ['nonexistent'] }); + expect(results.length).toBe(0); + }); + + describe('BSON type filtering', () => { + test('filtering by bsonTypes narrows type-specific operators', () => { + const allQuery = getFilteredCompletions({ meta: ['query'] }); + const stringOnly = getFilteredCompletions({ + meta: ['query'], + bsonTypes: ['string'], + }); + // String-only should have fewer or equal operators (universal + string-specific) + expect(stringOnly.length).toBeLessThanOrEqual(allQuery.length); + expect(stringOnly.length).toBeGreaterThan(0); + }); + + test('universal operators (no applicableBsonTypes) always pass BSON filter', () => { + const withBsonFilter = getFilteredCompletions({ + meta: ['query:comparison'], + bsonTypes: ['string'], + }); + // All comparison operators are universal + expect(withBsonFilter.length).toBe(8); + }); + + test('type-specific operators are excluded when BSON type does not match', () => { + const stringOps = getFilteredCompletions({ + meta: ['query'], + bsonTypes: ['number'], + }); + // $regex should NOT be included (it's string-only) + const hasRegex = stringOps.some((op) => op.value === '$regex'); + expect(hasRegex).toBe(false); + }); + + test('type-specific operators are included when BSON type matches', () => { + const stringOps = getFilteredCompletions({ + meta: ['query'], + bsonTypes: ['string'], + }); + // $regex should be included for string type + const hasRegex = stringOps.some((op) => op.value === '$regex'); + expect(hasRegex).toBe(true); + }); + }); +}); + +describe('completion context presets', () => { + test('FILTER_COMPLETION_META returns query + bson + variable', () => { + const results = getFilteredCompletions({ meta: FILTER_COMPLETION_META }); + const metas = new Set(results.map((r) => r.meta.split(':')[0])); + expect(metas).toContain('query'); + expect(metas).toContain('bson'); + expect(metas).toContain('variable'); + expect(metas).not.toContain('stage'); + expect(metas).not.toContain('update'); + }); + + test('STAGE_COMPLETION_META returns only stages', () => { + const results = getFilteredCompletions({ meta: STAGE_COMPLETION_META }); + expect(results.length).toBe(35); + for (const r of results) { + expect(r.meta).toBe('stage'); + } + }); + + test('UPDATE_COMPLETION_META returns only update operators', () => { + const results = getFilteredCompletions({ meta: UPDATE_COMPLETION_META }); + expect(results.length).toBe(22); + for (const r of results) { + expect(r.meta).toMatch(/^update/); + } + }); + + test('GROUP_EXPRESSION_COMPLETION_META returns expr + accumulator + bson + variable', () => { + const results = getFilteredCompletions({ meta: GROUP_EXPRESSION_COMPLETION_META }); + const metaPrefixes = new Set(results.map((r) => r.meta.split(':')[0])); + expect(metaPrefixes).toContain('expr'); + expect(metaPrefixes).toContain('accumulator'); + expect(metaPrefixes).toContain('bson'); + expect(metaPrefixes).toContain('variable'); + expect(metaPrefixes).not.toContain('query'); + expect(metaPrefixes).not.toContain('stage'); + }); + + test('EXPRESSION_COMPLETION_META returns expr + bson + variable (no accumulators)', () => { + const results = getFilteredCompletions({ meta: EXPRESSION_COMPLETION_META }); + const metaPrefixes = new Set(results.map((r) => r.meta.split(':')[0])); + expect(metaPrefixes).toContain('expr'); + expect(metaPrefixes).toContain('bson'); + expect(metaPrefixes).toContain('variable'); + expect(metaPrefixes).not.toContain('accumulator'); + }); + + test('WINDOW_COMPLETION_META returns window + accumulator + expr + bson + variable', () => { + const results = getFilteredCompletions({ meta: WINDOW_COMPLETION_META }); + const metaPrefixes = new Set(results.map((r) => r.meta.split(':')[0])); + expect(metaPrefixes).toContain('window'); + expect(metaPrefixes).toContain('accumulator'); + expect(metaPrefixes).toContain('expr'); + expect(metaPrefixes).toContain('bson'); + expect(metaPrefixes).toContain('variable'); + }); + + test('PROJECTION_COMPLETION_META returns projection operators + BSON constructors', () => { + const results = getFilteredCompletions({ meta: PROJECTION_COMPLETION_META }); + // field:identifier entries are injected at runtime, not statically registered + // But projection operators ($, $elemMatch, $slice) and BSON constructors are static + expect(results.length).toBeGreaterThan(0); + const metas = [...new Set(results.map((r) => r.meta))]; + expect(metas).toContain('query:projection'); + expect(metas).toContain('bson'); + }); +}); + +describe('registry idempotency', () => { + test('calling loadOperators() twice does not duplicate entries', () => { + const countBefore = getAllCompletions().length; + // loadOperators is re-exported from index + loadOperators(); + const countAfter = getAllCompletions().length; + expect(countAfter).toBe(countBefore); + }); +}); diff --git a/packages/documentdb-constants/src/getFilteredCompletions.ts b/packages/documentdb-constants/src/getFilteredCompletions.ts new file mode 100644 index 000000000..170353f78 --- /dev/null +++ b/packages/documentdb-constants/src/getFilteredCompletions.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Primary consumer API for the documentdb-constants package. + * + * Provides filtered access to the operator entries based on meta tags + * and optional BSON type constraints. + */ + +import { type CompletionFilter, type OperatorEntry } from './types'; + +/** + * Internal registry of all operator entries. Populated by the + * individual operator module files (queryOperators, stages, etc.) + * via {@link registerOperators}. + */ +const allOperatorsSet = new Set(); +const allOperators: OperatorEntry[] = []; + +/** + * Registers operator entries into the global registry. + * Duplicate entries (same value + meta key) are silently skipped, + * making repeated calls idempotent. + * + * Called by each operator module during module initialization. + * + * @param entries - array of OperatorEntry objects to register + */ +export function registerOperators(entries: readonly OperatorEntry[]): void { + for (const entry of entries) { + const key = `${entry.value}|${entry.meta}`; + if (!allOperatorsSet.has(key)) { + allOperatorsSet.add(key); + allOperators.push(entry); + } + } +} + +/** + * Clears all registered operator entries. + * Intended for internal/testing use only. + */ +export function clearOperators(): void { + allOperators.length = 0; + allOperatorsSet.clear(); +} + +/** + * Returns operator entries matching the given filter. + * + * Meta tag matching uses **prefix matching**: a filter meta of 'query' + * matches 'query', 'query:comparison', 'query:logical', etc. + * A filter meta of 'expr' matches all 'expr:*' entries. + * + * BSON type filtering is applied as an intersection: if `filter.bsonTypes` + * is provided, only operators whose `applicableBsonTypes` includes at least + * one of the requested types are returned. Operators without + * `applicableBsonTypes` (universal operators) are always included. + * + * @param filter - the filtering criteria + * @returns matching operator entries as a new array โ€” `Array.prototype.filter` + * always allocates a fresh array, so callers cannot mutate the internal registry + * through this return value. + */ +export function getFilteredCompletions(filter: CompletionFilter): readonly OperatorEntry[] { + return allOperators.filter((entry) => { + // Meta tag prefix matching + const metaMatch = filter.meta.some((prefix) => entry.meta === prefix || entry.meta.startsWith(prefix + ':')); + if (!metaMatch) { + return false; + } + + // BSON type filtering (if specified) + if (filter.bsonTypes && filter.bsonTypes.length > 0) { + // Universal operators (no applicableBsonTypes) always pass + if (entry.applicableBsonTypes && entry.applicableBsonTypes.length > 0) { + const hasMatch = entry.applicableBsonTypes.some((t) => filter.bsonTypes!.includes(t)); + if (!hasMatch) { + return false; + } + } + } + + return true; + }); +} + +/** + * Returns all operator entries (unfiltered). + * Useful for validation, testing, and diagnostics. + * + * Returns a shallow copy so callers cannot mutate the internal registry. + */ +export function getAllCompletions(): readonly OperatorEntry[] { + return [...allOperators]; +} diff --git a/packages/documentdb-constants/src/index.ts b/packages/documentdb-constants/src/index.ts new file mode 100644 index 000000000..d888fcf6b --- /dev/null +++ b/packages/documentdb-constants/src/index.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * @vscode-documentdb/documentdb-constants + * + * Static operator metadata for DocumentDB-supported operators, stages, + * accumulators, update operators, BSON constructors, and system variables. + */ + +// -- Core types -- +export type { CompletionFilter, MetaTag, OperatorEntry } from './types'; + +// -- Meta tag constants and presets -- +export { + ALL_META_TAGS, + EXPRESSION_COMPLETION_META, + // Completion context presets + FILTER_COMPLETION_META, + GROUP_EXPRESSION_COMPLETION_META, + META_ACCUMULATOR, + META_BSON, + META_EXPR_ARITH, + META_EXPR_ARRAY, + META_EXPR_BITWISE, + META_EXPR_BOOL, + META_EXPR_COMPARISON, + META_EXPR_CONDITIONAL, + META_EXPR_DATASIZE, + META_EXPR_DATE, + META_EXPR_LITERAL, + META_EXPR_MISC, + META_EXPR_OBJECT, + META_EXPR_SET, + META_EXPR_STRING, + META_EXPR_TIMESTAMP, + META_EXPR_TRIG, + META_EXPR_TYPE, + META_EXPR_VARIABLE, + META_FIELD_IDENTIFIER, + // Individual meta tags + META_QUERY, + META_QUERY_ARRAY, + META_QUERY_BITWISE, + META_QUERY_COMPARISON, + META_QUERY_ELEMENT, + META_QUERY_EVALUATION, + META_QUERY_GEOSPATIAL, + META_QUERY_LOGICAL, + META_QUERY_MISC, + META_QUERY_PROJECTION, + META_STAGE, + META_UPDATE, + META_UPDATE_ARRAY, + META_UPDATE_BITWISE, + META_UPDATE_FIELD, + META_VARIABLE, + META_WINDOW, + PROJECTION_COMPLETION_META, + STAGE_COMPLETION_META, + UPDATE_COMPLETION_META, + WINDOW_COMPLETION_META, +} from './metaTags'; + +// -- Consumer API -- +export { getAllCompletions, getFilteredCompletions } from './getFilteredCompletions'; + +// -- Documentation URL helpers -- +export { getDocBase, getDocLink } from './docLinks'; + +// -- Operator data modules -- +import { loadAccumulators } from './accumulators'; +import { loadBsonConstructors } from './bsonConstructors'; +import { loadExpressionOperators } from './expressionOperators'; +import { loadQueryOperators } from './queryOperators'; +import { loadStages } from './stages'; +import { loadSystemVariables } from './systemVariables'; +import { loadUpdateOperators } from './updateOperators'; +import { loadWindowOperators } from './windowOperators'; + +/** + * Loads all built-in operator data into the registry. + * + * Called automatically at module import time so that consumers using + * `import { getFilteredCompletions } from '@vscode-documentdb/documentdb-constants'` + * get all operators without any additional setup. + * + * Can also be called explicitly (e.g. in workers or tests) โ€” the call is + * idempotent when combined with {@link clearOperators}. + */ +export function loadOperators(): void { + loadAccumulators(); + loadBsonConstructors(); + loadExpressionOperators(); + loadQueryOperators(); + loadStages(); + loadSystemVariables(); + loadUpdateOperators(); + loadWindowOperators(); +} + +// Auto-load on module import so the public API works out of the box. +loadOperators(); diff --git a/packages/documentdb-constants/src/metaTags.ts b/packages/documentdb-constants/src/metaTags.ts new file mode 100644 index 000000000..7a4dd7add --- /dev/null +++ b/packages/documentdb-constants/src/metaTags.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Meta tag constants for categorizing operators in the DocumentDB constants package. + * + * Tags use a hierarchical scheme where prefix matching is supported: + * filtering by 'query' matches 'query', 'query:comparison', 'query:logical', etc. + */ + +// -- Query operators -- +export const META_QUERY = 'query' as const; +export const META_QUERY_COMPARISON = 'query:comparison' as const; +export const META_QUERY_LOGICAL = 'query:logical' as const; +export const META_QUERY_ELEMENT = 'query:element' as const; +export const META_QUERY_EVALUATION = 'query:evaluation' as const; +export const META_QUERY_ARRAY = 'query:array' as const; +export const META_QUERY_BITWISE = 'query:bitwise' as const; +export const META_QUERY_GEOSPATIAL = 'query:geospatial' as const; +export const META_QUERY_PROJECTION = 'query:projection' as const; +export const META_QUERY_MISC = 'query:misc' as const; + +// -- Update operators -- +export const META_UPDATE = 'update' as const; +export const META_UPDATE_FIELD = 'update:field' as const; +export const META_UPDATE_ARRAY = 'update:array' as const; +export const META_UPDATE_BITWISE = 'update:bitwise' as const; + +// -- Aggregation pipeline -- +export const META_STAGE = 'stage' as const; +export const META_ACCUMULATOR = 'accumulator' as const; + +// -- Expression operators -- +export const META_EXPR_ARITH = 'expr:arith' as const; +export const META_EXPR_ARRAY = 'expr:array' as const; +export const META_EXPR_BOOL = 'expr:bool' as const; +export const META_EXPR_COMPARISON = 'expr:comparison' as const; +export const META_EXPR_CONDITIONAL = 'expr:conditional' as const; +export const META_EXPR_DATE = 'expr:date' as const; +export const META_EXPR_OBJECT = 'expr:object' as const; +export const META_EXPR_SET = 'expr:set' as const; +export const META_EXPR_STRING = 'expr:string' as const; +export const META_EXPR_TRIG = 'expr:trig' as const; +export const META_EXPR_TYPE = 'expr:type' as const; +export const META_EXPR_DATASIZE = 'expr:datasize' as const; +export const META_EXPR_TIMESTAMP = 'expr:timestamp' as const; +export const META_EXPR_BITWISE = 'expr:bitwise' as const; +export const META_EXPR_LITERAL = 'expr:literal' as const; +export const META_EXPR_MISC = 'expr:misc' as const; +export const META_EXPR_VARIABLE = 'expr:variable' as const; + +// -- Window operators -- +export const META_WINDOW = 'window' as const; + +// -- BSON constructors -- +export const META_BSON = 'bson' as const; + +// -- System variables -- +export const META_VARIABLE = 'variable' as const; + +// -- Schema-injected field names (not static โ€” provided at runtime) -- +export const META_FIELD_IDENTIFIER = 'field:identifier' as const; + +/** + * All known meta tag values for validation purposes. + */ +export const ALL_META_TAGS = [ + META_QUERY, + META_QUERY_COMPARISON, + META_QUERY_LOGICAL, + META_QUERY_ELEMENT, + META_QUERY_EVALUATION, + META_QUERY_ARRAY, + META_QUERY_BITWISE, + META_QUERY_GEOSPATIAL, + META_QUERY_PROJECTION, + META_QUERY_MISC, + META_UPDATE, + META_UPDATE_FIELD, + META_UPDATE_ARRAY, + META_UPDATE_BITWISE, + META_STAGE, + META_ACCUMULATOR, + META_EXPR_ARITH, + META_EXPR_ARRAY, + META_EXPR_BOOL, + META_EXPR_COMPARISON, + META_EXPR_CONDITIONAL, + META_EXPR_DATE, + META_EXPR_OBJECT, + META_EXPR_SET, + META_EXPR_STRING, + META_EXPR_TRIG, + META_EXPR_TYPE, + META_EXPR_DATASIZE, + META_EXPR_TIMESTAMP, + META_EXPR_BITWISE, + META_EXPR_LITERAL, + META_EXPR_MISC, + META_EXPR_VARIABLE, + META_WINDOW, + META_BSON, + META_VARIABLE, + META_FIELD_IDENTIFIER, +] as const; + +// -- Completion context presets -- + +/** Query filter contexts: find filter bar, $match stage body */ +export const FILTER_COMPLETION_META: readonly string[] = ['query', 'bson', 'variable']; + +/** Projection/sort contexts: field names + projection operators */ +export const PROJECTION_COMPLETION_META: readonly string[] = ['field:identifier', 'query:projection', 'bson']; + +/** $group/$project/$addFields stage body: expressions + accumulators */ +export const GROUP_EXPRESSION_COMPLETION_META: readonly string[] = ['expr', 'accumulator', 'bson', 'variable']; + +/** Other stage bodies: expressions only (no accumulators) */ +export const EXPRESSION_COMPLETION_META: readonly string[] = ['expr', 'bson', 'variable']; + +/** Update operations: update operators */ +export const UPDATE_COMPLETION_META: readonly string[] = ['update']; + +/** Top-level aggregation pipeline: stage names */ +export const STAGE_COMPLETION_META: readonly string[] = ['stage']; + +/** Window fields: window operators + accumulators + expressions */ +export const WINDOW_COMPLETION_META: readonly string[] = ['window', 'accumulator', 'expr', 'bson', 'variable']; diff --git a/packages/documentdb-constants/src/operatorReference.test.ts b/packages/documentdb-constants/src/operatorReference.test.ts new file mode 100644 index 000000000..4d4a8d853 --- /dev/null +++ b/packages/documentdb-constants/src/operatorReference.test.ts @@ -0,0 +1,359 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Dump-vs-implementation verification test. + * + * Ensures the TypeScript operator implementation always matches the + * resource dump (scraped/operator-reference.md). This test is the + * enforcing contract between "what does DocumentDB support?" (the dump) + * and "what does our code provide?" (the implementation). + * + * See ยง2.3.3 of docs/plan/03-documentdb-constants.md for design rationale. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { getAllCompletions } from './index'; +import { parseOperatorReference, type ReferenceOperator } from './parseOperatorReference'; + +const dumpPath = path.join(__dirname, '..', 'resources', 'scraped', 'operator-reference.md'); +const dumpContent = fs.readFileSync(dumpPath, 'utf-8'); +const parsed = parseOperatorReference(dumpContent); +const referenceOperators = parsed.operators; +const notListedOperators = parsed.notListed; +const implementedOperators = getAllCompletions(); + +/** + * Category-to-meta mapping. Maps dump category names to the meta tags + * used in the implementation. Some dump categories map to the same meta + * tag (e.g., both accumulator categories map to 'accumulator'). + */ +const CATEGORY_TO_META: Record = { + 'Comparison Query Operators': 'query:comparison', + 'Logical Query Operators': 'query:logical', + 'Element Query Operators': 'query:element', + 'Evaluation Query Operators': 'query:evaluation', + 'Geospatial Operators': 'query:geospatial', + 'Array Query Operators': 'query:array', + 'Bitwise Query Operators': 'query:bitwise', + 'Projection Operators': 'query:projection', + 'Miscellaneous Query Operators': 'query:misc', + 'Field Update Operators': 'update:field', + 'Array Update Operators': 'update:array', + 'Bitwise Update Operators': 'update:bitwise', + 'Arithmetic Expression Operators': 'expr:arith', + 'Array Expression Operators': 'expr:array', + 'Bitwise Operators': 'expr:bitwise', + 'Boolean Expression Operators': 'expr:bool', + 'Comparison Expression Operators': 'expr:comparison', + 'Data Size Operators': 'expr:datasize', + 'Date Expression Operators': 'expr:date', + 'Literal Expression Operator': 'expr:literal', + 'Miscellaneous Operators': 'expr:misc', + 'Object Expression Operators': 'expr:object', + 'Set Expression Operators': 'expr:set', + 'String Expression Operators': 'expr:string', + 'Timestamp Expression Operators': 'expr:timestamp', + 'Trigonometry Expression Operators': 'expr:trig', + 'Type Expression Operators': 'expr:type', + 'Accumulators ($group, $bucket, $bucketAuto, $setWindowFields)': 'accumulator', + 'Accumulators (in Other Stages)': 'accumulator', + 'Variable Expression Operators': 'expr:variable', + 'Window Operators': 'window', + 'Conditional Expression Operators': 'expr:conditional', + 'Aggregation Pipeline Stages': 'stage', + 'Variables in Aggregation Expressions': 'variable', +}; + +describe('operator reference verification', () => { + test('dump file exists and is parseable', () => { + expect(dumpContent.length).toBeGreaterThan(1000); + expect(referenceOperators.length).toBeGreaterThan(250); + }); + + test('every listed operator in the dump has an implementation entry', () => { + const implementedValues = new Set(implementedOperators.map((op) => op.value)); + const missing: string[] = []; + + for (const ref of referenceOperators) { + // Some operators appear in multiple dump categories (e.g., $objectToArray + // in both "Array Expression" and "Object Expression"). The implementation + // only needs one entry per (value, meta) pair โ€” check by value. + if (!implementedValues.has(ref.operator)) { + missing.push(`${ref.operator} (${ref.category})`); + } + } + + expect(missing).toEqual([]); + }); + + test('no extra operators in implementation beyond the dump (excluding BSON/variables)', () => { + // Build a set of all operator values from the dump + const dumpValues = new Set(referenceOperators.map((r) => r.operator)); + + // Filter implementation entries: exclude BSON constructors and system variables + // (these are hand-authored, not from the compatibility page dump) + const extras = implementedOperators.filter( + (op) => !op.meta.startsWith('bson') && !op.meta.startsWith('variable') && !dumpValues.has(op.value), + ); + + expect(extras.map((e) => `${e.value} (${e.meta})`)).toEqual([]); + }); + + test('descriptions match the dump (detect drift)', () => { + const mismatches: string[] = []; + + for (const ref of referenceOperators) { + if (!ref.description) { + continue; // some operators have empty descriptions (missing upstream docs) + } + + // Find implementation entry matching this operator + category's meta + const expectedMeta = CATEGORY_TO_META[ref.category]; + if (!expectedMeta) { + continue; // unknown category + } + + const impl = implementedOperators.find((op) => op.value === ref.operator && op.meta === expectedMeta); + + if (impl && impl.description !== ref.description) { + mismatches.push( + `${ref.operator} (${ref.category}): expected "${ref.description}", got "${impl.description}"`, + ); + } + } + + expect(mismatches).toEqual([]); + }); + + test('not-listed operators are NOT in the implementation', () => { + const leaked: string[] = []; + + for (const nl of notListedOperators) { + // Check the exact meta category from the dump + const expectedMeta = CATEGORY_TO_META[nl.category]; + if (!expectedMeta) { + continue; + } + + const found = implementedOperators.find((op) => op.value === nl.operator && op.meta === expectedMeta); + + if (found) { + leaked.push(`${nl.operator} (${nl.category}) โ€” ${nl.reason}`); + } + } + + expect(leaked).toEqual([]); + }); + + test('all dump categories have a known meta mapping', () => { + const categories = new Set(referenceOperators.map((r) => r.category)); + const unmapped = [...categories].filter((c) => !CATEGORY_TO_META[c]); + expect(unmapped).toEqual([]); + }); + + test('reference parser found the expected number of not-listed operators', () => { + // The plan lists 16 not-listed operators (ยง2.1) + expect(notListedOperators.length).toBeGreaterThanOrEqual(14); + expect(notListedOperators.length).toBeLessThanOrEqual(20); + }); +}); + +// --------------------------------------------------------------------------- +// Merged dump + overrides verification +// +// The generator (scripts/generate-from-reference.ts) merges the scraped dump +// with manual overrides. These tests verify the implementation matches the +// MERGED result โ€” catching scenarios where: +// - Someone hand-edits a generated .ts file instead of using overrides +// - Someone adds an override but forgets to run `npm run generate` +// - Someone runs `npm run scrape` but forgets `npm run generate` +// - The override file is accidentally truncated +// --------------------------------------------------------------------------- + +const overridesPath = path.join(__dirname, '..', 'resources', 'overrides', 'operator-overrides.md'); +const overridesContent = fs.readFileSync(overridesPath, 'utf-8'); +const parsedOverrides = parseOperatorReference(overridesContent); +const overrideOperators = parsedOverrides.operators; + +/** + * Merges dump and override operators. For each (operator, category) pair, + * the override description wins if non-empty; otherwise the dump description + * is used. This mirrors what the generator does. + */ +function getMergedOperators(): readonly ReferenceOperator[] { + // Build a lookup: "operator|category" โ†’ override entry + const overrideLookup = new Map(); + for (const ov of overrideOperators) { + overrideLookup.set(`${ov.operator}|${ov.category}`, ov); + } + + return referenceOperators.map((ref) => { + const override = overrideLookup.get(`${ref.operator}|${ref.category}`); + if (!override) { + return ref; + } + return { + operator: ref.operator, + category: ref.category, + description: override.description || ref.description, + docLink: override.docLink || ref.docLink, + }; + }); +} + +const mergedOperators = getMergedOperators(); + +describe('merged dump + overrides verification', () => { + test('overrides file exists and has entries', () => { + expect(overridesContent.length).toBeGreaterThan(100); + expect(overrideOperators.length).toBeGreaterThan(0); + }); + + test('override count is within expected range (detect truncation)', () => { + // Currently 56 overrides. Allow some flex for additions/removals, + // but catch catastrophic truncation (e.g., file emptied to <10). + expect(overrideOperators.length).toBeGreaterThanOrEqual(40); + expect(overrideOperators.length).toBeLessThanOrEqual(80); + }); + + test('every override targets an operator that exists in the dump', () => { + const dumpKeys = new Set(referenceOperators.map((r) => `${r.operator}|${r.category}`)); + const orphans: string[] = []; + + for (const ov of overrideOperators) { + if (!dumpKeys.has(`${ov.operator}|${ov.category}`)) { + orphans.push(`${ov.operator} (${ov.category})`); + } + } + + expect(orphans).toEqual([]); + }); + + test('descriptions match the merged dump+overrides (detect hand-edits and stale generates)', () => { + const mismatches: string[] = []; + + for (const merged of mergedOperators) { + if (!merged.description) { + continue; // operator with no description in either dump or override + } + + const expectedMeta = CATEGORY_TO_META[merged.category]; + if (!expectedMeta) { + continue; + } + + const impl = implementedOperators.find((op) => op.value === merged.operator && op.meta === expectedMeta); + + if (impl && impl.description !== merged.description) { + mismatches.push( + `${merged.operator} (${merged.category}): ` + + `expected "${merged.description}", got "${impl.description}"`, + ); + } + } + + expect(mismatches).toEqual([]); + }); + + test('doc links from dump match implementation links for single-category operators', () => { + // Many operators appear in multiple dump categories (e.g., $eq in both + // "Comparison Query" and "Comparison Expression"). The scraper finds the + // doc page under whichever category directory it tries first, while the + // implementation generates URLs from each operator's meta tag. For + // cross-category operators, the dump link and impl link will point to + // different (but both valid) doc directories. + // + // This test only compares links for operators where the dump category + // maps to a unique operator โ€” no cross-category ambiguity. + + // Known scraper mismatches: the scraper's global index fallback found + // these operators' doc pages under a different directory than their + // category implies. The implementation link is correct; the dump link is + // a scraper artifact. Update this set when refreshing the dump. + // + // NOTE: After fixing META_TO_DOC_DIR in docLinks.ts (expr:bool โ†’ logical-query, + // expr:comparison โ†’ comparison-query) and adding smart link emission in the + // generator (hardcoded URLs for cross-category fallbacks), this set should + // remain empty unless new scraper mismatches are discovered. + const KNOWN_SCRAPER_MISMATCHES = new Set([]); + + // Build a set of operators that appear in more than one dump category + const operatorCategories = new Map>(); + for (const ref of referenceOperators) { + const cats = operatorCategories.get(ref.operator) ?? new Set(); + cats.add(ref.category); + operatorCategories.set(ref.operator, cats); + } + + const mismatches: string[] = []; + + for (const ref of referenceOperators) { + if (!ref.docLink) { + continue; + } + + // Skip cross-category operators โ€” their dump link may come from + // a different category than the implementation's meta tag + const cats = operatorCategories.get(ref.operator); + if (cats && cats.size > 1) { + continue; + } + + // Skip known scraper mismatches (documented above) + if (KNOWN_SCRAPER_MISMATCHES.has(ref.operator)) { + continue; + } + + const expectedMeta = CATEGORY_TO_META[ref.category]; + if (!expectedMeta) { + continue; + } + + const impl = implementedOperators.find((op) => op.value === ref.operator && op.meta === expectedMeta); + + if (!impl || !impl.link) { + continue; + } + + const dumpLink = ref.docLink.toLowerCase(); + const implLink = impl.link.toLowerCase(); + + if (dumpLink !== implLink) { + mismatches.push(`${ref.operator} (${ref.category}): ` + `dump="${ref.docLink}", impl="${impl.link}"`); + } + } + + expect(mismatches).toEqual([]); + }); + + test('every override with a description was applied (not silently ignored)', () => { + const unapplied: string[] = []; + + for (const ov of overrideOperators) { + if (!ov.description) { + continue; + } + + const expectedMeta = CATEGORY_TO_META[ov.category]; + if (!expectedMeta) { + continue; + } + + const impl = implementedOperators.find((op) => op.value === ov.operator && op.meta === expectedMeta); + + if (!impl) { + unapplied.push(`${ov.operator} (${ov.category}): no implementation entry found`); + } else if (impl.description !== ov.description) { + unapplied.push( + `${ov.operator} (${ov.category}): override="${ov.description}", ` + `impl="${impl.description}"`, + ); + } + } + + expect(unapplied).toEqual([]); + }); +}); diff --git a/packages/documentdb-constants/src/parseOperatorReference.test.ts b/packages/documentdb-constants/src/parseOperatorReference.test.ts new file mode 100644 index 000000000..4ebf5138a --- /dev/null +++ b/packages/documentdb-constants/src/parseOperatorReference.test.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Unit tests for the parseOperatorReference helper. + */ + +import { parseOperatorReference } from './parseOperatorReference'; + +describe('parseOperatorReference', () => { + test('parses a minimal dump with one category and one operator', () => { + const content = `# DocumentDB Operator Reference + +## Summary + +| Category | Listed | Total | +| --- | --- | --- | +| Test Category | 1 | 1 | + +## Test Category + +### $testOp + +- **Description:** A test operator +- **Doc Link:** https://example.com/test + +## Not Listed + +- **$excluded** (Test Category) โ€” Not supported +`; + const result = parseOperatorReference(content); + expect(result.operators).toHaveLength(1); + expect(result.operators[0]).toEqual({ + operator: '$testOp', + category: 'Test Category', + description: 'A test operator', + docLink: 'https://example.com/test', + }); + expect(result.notListed).toHaveLength(1); + expect(result.notListed[0]).toEqual({ + operator: '$excluded', + category: 'Test Category', + reason: 'Not supported', + }); + }); + + test('handles operators with empty description and doc link', () => { + const content = `## Variables + +### $$NOW + +### $$ROOT +`; + const result = parseOperatorReference(content); + expect(result.operators).toHaveLength(2); + expect(result.operators[0]).toEqual({ + operator: '$$NOW', + category: 'Variables', + description: '', + docLink: '', + }); + expect(result.operators[1]).toEqual({ + operator: '$$ROOT', + category: 'Variables', + description: '', + docLink: '', + }); + }); + + test('handles operators with syntax blocks (ignores syntax)', () => { + const content = `## Comparison Query Operators + +### $eq + +- **Description:** Matches values equal to a specified value +- **Syntax:** + +\`\`\`javascript +{ field: { $eq: value } } +\`\`\` + +- **Doc Link:** https://example.com/$eq + +### $gt + +- **Description:** Matches values greater than a specified value +- **Doc Link:** https://example.com/$gt +`; + const result = parseOperatorReference(content); + expect(result.operators).toHaveLength(2); + expect(result.operators[0].operator).toBe('$eq'); + expect(result.operators[0].description).toBe('Matches values equal to a specified value'); + expect(result.operators[1].operator).toBe('$gt'); + }); + + test('skips operators in the Summary section', () => { + const content = `## Summary + +| Category | Listed | Total | +| --- | --- | --- | +| Test | 2 | 3 | + +## Test Category + +### $realOp + +- **Description:** I am real +`; + const result = parseOperatorReference(content); + expect(result.operators).toHaveLength(1); + expect(result.operators[0].operator).toBe('$realOp'); + }); + + test('multiple not-listed entries are parsed correctly', () => { + const content = `## Not Listed + +Operators below are not in scope. + +- **$where** (Evaluation Query) โ€” Deprecated in Mongo version 8.0 +- **$meta** (Projection) โ€” Not in scope +- **$accumulator** (Custom Aggregation) โ€” Deprecated in Mongo version 8.0 +`; + const result = parseOperatorReference(content); + expect(result.notListed).toHaveLength(3); + expect(result.notListed[0].operator).toBe('$where'); + expect(result.notListed[0].reason).toBe('Deprecated in Mongo version 8.0'); + expect(result.notListed[1].operator).toBe('$meta'); + expect(result.notListed[2].operator).toBe('$accumulator'); + }); + + test('handles multiple categories', () => { + const content = `## Cat A + +### $a1 + +- **Description:** Operator a1 + +### $a2 + +- **Description:** Operator a2 + +## Cat B + +### $b1 + +- **Description:** Operator b1 +`; + const result = parseOperatorReference(content); + expect(result.operators).toHaveLength(3); + expect(result.operators[0].category).toBe('Cat A'); + expect(result.operators[1].category).toBe('Cat A'); + expect(result.operators[2].category).toBe('Cat B'); + }); +}); diff --git a/packages/documentdb-constants/src/parseOperatorReference.ts b/packages/documentdb-constants/src/parseOperatorReference.ts new file mode 100644 index 000000000..e1179c336 --- /dev/null +++ b/packages/documentdb-constants/src/parseOperatorReference.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Parses the scraped/operator-reference.md dump file into structured data + * for use in the operatorReference verification test. + * + * The dump format uses structured headings: + * ## Category Name โ€” category section + * ### $operatorName โ€” operator heading + * - **Description:** text โ€” operator description + * - **Doc Link:** url โ€” documentation URL + * + * ## Not Listed โ€” excluded operators section + * - **$operator** (Category) โ€” Reason + */ + +/** + * Represents a single operator entry parsed from the reference dump. + */ +export interface ReferenceOperator { + /** Operator name, e.g. "$eq", "$$NOW" */ + readonly operator: string; + /** Category from the dump, e.g. "Comparison Query Operators" */ + readonly category: string; + /** Description from the dump (may be empty) */ + readonly description: string; + /** Documentation URL from the dump (may be empty) */ + readonly docLink: string; +} + +/** + * Represents an operator excluded from the package scope. + */ +export interface NotListedOperator { + /** Operator name, e.g. "$where", "$meta" */ + readonly operator: string; + /** Category from the dump */ + readonly category: string; + /** Reason for exclusion */ + readonly reason: string; +} + +/** + * Complete parsed result from the reference dump. + */ +export interface ParsedReference { + /** All listed (in-scope) operators */ + readonly operators: readonly ReferenceOperator[]; + /** All not-listed (excluded) operators */ + readonly notListed: readonly NotListedOperator[]; +} + +/** + * Parses the scraped/operator-reference.md content into structured data. + * + * @param content - the full Markdown content of the dump file + * @returns parsed reference data + */ +export function parseOperatorReference(content: string): ParsedReference { + const lines = content.split('\n'); + const operators: ReferenceOperator[] = []; + const notListed: NotListedOperator[] = []; + + let currentCategory = ''; + let inNotListed = false; + let inSummary = false; + + // Temp state for building current operator + let currentOperator = ''; + let currentDescription = ''; + let currentDocLink = ''; + + function flushOperator(): void { + if (currentOperator && currentCategory && !inNotListed && !inSummary) { + operators.push({ + operator: currentOperator, + category: currentCategory, + description: currentDescription, + docLink: currentDocLink, + }); + } + currentOperator = ''; + currentDescription = ''; + currentDocLink = ''; + } + + for (const line of lines) { + const trimmed = line.trim(); + + // Detect ## headings (category sections) + const h2Match = trimmed.match(/^## (.+)$/); + if (h2Match) { + flushOperator(); + const heading = h2Match[1].trim(); + if (heading === 'Summary') { + inSummary = true; + inNotListed = false; + currentCategory = ''; + } else if (heading === 'Not Listed') { + inNotListed = true; + inSummary = false; + currentCategory = ''; + } else { + currentCategory = heading; + inNotListed = false; + inSummary = false; + } + continue; + } + + // Skip summary section + if (inSummary) { + continue; + } + + // Parse "Not Listed" entries: - **$operator** (Category) โ€” Reason + if (inNotListed) { + const notListedMatch = trimmed.match(/^- \*\*(.+?)\*\* \((.+?)\) โ€” (.+)$/); + if (notListedMatch) { + notListed.push({ + operator: notListedMatch[1], + category: notListedMatch[2], + reason: notListedMatch[3], + }); + } + continue; + } + + // Detect ### headings (operator entries) + const h3Match = trimmed.match(/^### (.+)$/); + if (h3Match) { + flushOperator(); + currentOperator = h3Match[1].trim(); + continue; + } + + // Parse description: - **Description:** text + const descMatch = trimmed.match(/^- \*\*Description:\*\* (.+)$/); + if (descMatch && currentOperator) { + currentDescription = descMatch[1].trim(); + continue; + } + + // Parse doc link: - **Doc Link:** url ('none' means no page at expected location) + const linkMatch = trimmed.match(/^- \*\*Doc Link:\*\* (.+)$/); + if (linkMatch && currentOperator) { + const rawLink = linkMatch[1].trim(); + currentDocLink = rawLink === 'none' ? '' : rawLink; + continue; + } + } + + // Flush last operator + flushOperator(); + + return { operators, notListed }; +} diff --git a/packages/documentdb-constants/src/queryOperators.ts b/packages/documentdb-constants/src/queryOperators.ts new file mode 100644 index 000000000..8390356a6 --- /dev/null +++ b/packages/documentdb-constants/src/queryOperators.ts @@ -0,0 +1,458 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED โ€” DO NOT EDIT BY HAND +// +// Generated by: npm run generate (scripts/generate-from-reference.ts) +// Sources: resources/scraped/operator-reference.md +// resources/overrides/operator-overrides.md +// resources/overrides/operator-snippets.md +// +// To change operator data, edit the overrides/snippets files and re-run the generator. + +import { getDocLink } from './docLinks'; +import { registerOperators } from './getFilteredCompletions'; +import { + META_QUERY_ARRAY, + META_QUERY_BITWISE, + META_QUERY_COMPARISON, + META_QUERY_ELEMENT, + META_QUERY_EVALUATION, + META_QUERY_GEOSPATIAL, + META_QUERY_LOGICAL, + META_QUERY_MISC, + META_QUERY_PROJECTION, +} from './metaTags'; +import { type OperatorEntry } from './types'; + +// --------------------------------------------------------------------------- +// Comparison Query Operators +// --------------------------------------------------------------------------- + +const comparisonQueryOperators: readonly OperatorEntry[] = [ + { + value: '$eq', + meta: META_QUERY_COMPARISON, + description: 'The $eq query operator compares the value of a field to a specified value', + snippet: '{ $eq: ${1:value} }', + link: getDocLink('$eq', META_QUERY_COMPARISON), + }, + { + value: '$gt', + meta: META_QUERY_COMPARISON, + description: + 'The $gt query operator retrieves documents where the value of a field is greater than a specified value', + snippet: '{ $gt: ${1:value} }', + link: getDocLink('$gt', META_QUERY_COMPARISON), + }, + { + value: '$gte', + meta: META_QUERY_COMPARISON, + description: + 'The $gte operator retrieves documents where the value of a field is greater than or equal to a specified value', + snippet: '{ $gte: ${1:value} }', + link: getDocLink('$gte', META_QUERY_COMPARISON), + }, + { + value: '$in', + meta: META_QUERY_COMPARISON, + description: 'The $in operator matches value of a field against an array of specified values', + snippet: '{ $in: [${1:value}] }', + link: getDocLink('$in', META_QUERY_COMPARISON), + }, + { + value: '$lt', + meta: META_QUERY_COMPARISON, + description: 'The $lt operator retrieves documents where the value of field is less than a specified value', + snippet: '{ $lt: ${1:value} }', + link: getDocLink('$lt', META_QUERY_COMPARISON), + }, + { + value: '$lte', + meta: META_QUERY_COMPARISON, + description: + 'The $lte operator retrieves documents where the value of a field is less than or equal to a specified value', + snippet: '{ $lte: ${1:value} }', + link: getDocLink('$lte', META_QUERY_COMPARISON), + }, + { + value: '$ne', + meta: META_QUERY_COMPARISON, + description: "The $ne operator retrieves documents where the value of a field doesn't equal a specified value", + snippet: '{ $ne: ${1:value} }', + link: getDocLink('$ne', META_QUERY_COMPARISON), + }, + { + value: '$nin', + meta: META_QUERY_COMPARISON, + description: "The $nin operator retrieves documents where the value of a field doesn't match a list of values", + snippet: '{ $nin: [${1:value}] }', + link: getDocLink('$nin', META_QUERY_COMPARISON), + }, +]; + +// --------------------------------------------------------------------------- +// Logical Query Operators +// --------------------------------------------------------------------------- + +const logicalQueryOperators: readonly OperatorEntry[] = [ + { + value: '$and', + meta: META_QUERY_LOGICAL, + description: + 'The $and operator joins multiple query clauses and returns documents that match all specified conditions.', + snippet: '{ $and: [{ ${1:expression} }] }', + link: getDocLink('$and', META_QUERY_LOGICAL), + }, + { + value: '$not', + meta: META_QUERY_LOGICAL, + description: + "The $not operator performs a logical NOT operation on a specified expression, selecting documents that don't match the expression.", + snippet: '{ $not: { ${1:expression} } }', + link: getDocLink('$not', META_QUERY_LOGICAL), + }, + { + value: '$nor', + meta: META_QUERY_LOGICAL, + description: + 'The $nor operator performs a logical NOR on an array of expressions and retrieves documents that fail all the conditions.', + snippet: '{ $nor: [{ ${1:expression} }] }', + link: getDocLink('$nor', META_QUERY_LOGICAL), + }, + { + value: '$or', + meta: META_QUERY_LOGICAL, + description: + 'The $or operator joins query clauses with a logical OR and returns documents that match at least one of the specified conditions.', + snippet: '{ $or: [{ ${1:expression} }] }', + link: getDocLink('$or', META_QUERY_LOGICAL), + }, +]; + +// --------------------------------------------------------------------------- +// Element Query Operators +// --------------------------------------------------------------------------- + +const elementQueryOperators: readonly OperatorEntry[] = [ + { + value: '$exists', + meta: META_QUERY_ELEMENT, + description: + 'The $exists operator retrieves documents that contain the specified field in their document structure.', + snippet: '{ $exists: ${1:true} }', + link: getDocLink('$exists', META_QUERY_ELEMENT), + }, + { + value: '$type', + meta: META_QUERY_ELEMENT, + description: 'The $type operator retrieves documents if the chosen field is of the specified type.', + snippet: '{ $type: "${1:type}" }', + link: getDocLink('$type', META_QUERY_ELEMENT), + }, +]; + +// --------------------------------------------------------------------------- +// Evaluation Query Operators +// --------------------------------------------------------------------------- + +const evaluationQueryOperators: readonly OperatorEntry[] = [ + { + value: '$expr', + meta: META_QUERY_EVALUATION, + description: + 'The $expr operator allows the use of aggregation expressions within the query language, enabling complex field comparisons and calculations.', + snippet: '{ $expr: { ${1:expression} } }', + link: getDocLink('$expr', META_QUERY_EVALUATION), + }, + { + value: '$jsonSchema', + meta: META_QUERY_EVALUATION, + description: + 'The $jsonSchema operator validates documents against a JSON Schema definition for data validation and structure enforcement. Discover supported features and limitations.', + snippet: '{ $jsonSchema: { bsonType: "${1:object}" } }', + link: getDocLink('$jsonSchema', META_QUERY_EVALUATION), + }, + { + value: '$mod', + meta: META_QUERY_EVALUATION, + description: + 'The $mod operator performs a modulo operation on the value of a field and selects documents with a specified result.', + snippet: '{ $mod: [${1:divisor}, ${2:remainder}] }', + link: getDocLink('$mod', META_QUERY_EVALUATION), + }, + { + value: '$regex', + meta: META_QUERY_EVALUATION, + description: + 'The $regex operator provides regular expression capabilities for pattern matching in queries, allowing flexible string matching and searching.', + snippet: '{ $regex: /${1:pattern}/ }', + link: getDocLink('$regex', META_QUERY_EVALUATION), + applicableBsonTypes: ['string'], + }, + { + value: '$text', + meta: META_QUERY_EVALUATION, + description: + 'The $text operator performs text search on the content of indexed string fields, enabling full-text search capabilities.', + snippet: '{ $text: { \\$search: "${1:text}" } }', + link: getDocLink('$text', META_QUERY_EVALUATION), + applicableBsonTypes: ['string'], + }, +]; + +// --------------------------------------------------------------------------- +// Geospatial Operators +// --------------------------------------------------------------------------- + +const geospatialOperators: readonly OperatorEntry[] = [ + { + value: '$geoIntersects', + meta: META_QUERY_GEOSPATIAL, + description: + 'The $geoIntersects operator selects documents whose location field intersects with a specified GeoJSON object.', + snippet: '{ $geoIntersects: { \\$geometry: { type: "${1:GeoJSON type}", coordinates: ${2:coordinates} } } }', + link: getDocLink('$geoIntersects', META_QUERY_GEOSPATIAL), + }, + { + value: '$geoWithin', + meta: META_QUERY_GEOSPATIAL, + description: + 'The $geoWithin operator selects documents whose location field is completely within a specified geometry.', + snippet: '{ $geoWithin: { \\$geometry: { type: "${1:GeoJSON type}", coordinates: ${2:coordinates} } } }', + link: getDocLink('$geoWithin', META_QUERY_GEOSPATIAL), + }, + { + value: '$box', + meta: META_QUERY_GEOSPATIAL, + description: 'The $box operator defines a rectangular area for geospatial queries using coordinate pairs.', + snippet: '[[${1:bottomLeftX}, ${2:bottomLeftY}], [${3:upperRightX}, ${4:upperRightY}]]', + link: getDocLink('$box', META_QUERY_GEOSPATIAL), + standalone: false, + }, + { + value: '$center', + meta: META_QUERY_GEOSPATIAL, + description: 'The $center operator specifies a circle using legacy coordinate pairs for $geoWithin queries.', + snippet: '[[${1:x}, ${2:y}], ${3:radius}]', + link: getDocLink('$center', META_QUERY_GEOSPATIAL), + standalone: false, + }, + { + value: '$centerSphere', + meta: META_QUERY_GEOSPATIAL, + description: 'The $centerSphere operator specifies a circle using spherical geometry for $geoWithin queries.', + snippet: '[[${1:x}, ${2:y}], ${3:radiusInRadians}]', + link: getDocLink('$centerSphere', META_QUERY_GEOSPATIAL), + standalone: false, + }, + { + value: '$geometry', + meta: META_QUERY_GEOSPATIAL, + description: 'The $geometry operator specifies a GeoJSON geometry for geospatial queries.', + snippet: '{ type: "${1:Point}", coordinates: [${2:coordinates}] }', + link: getDocLink('$geometry', META_QUERY_GEOSPATIAL), + standalone: false, + }, + { + value: '$maxDistance', + meta: META_QUERY_GEOSPATIAL, + description: + 'The $maxDistance operator specifies the maximum distance that can exist between two points in a geospatial query.', + snippet: '${1:distance}', + link: getDocLink('$maxDistance', META_QUERY_GEOSPATIAL), + standalone: false, + }, + { + value: '$minDistance', + meta: META_QUERY_GEOSPATIAL, + description: + 'The $minDistance operator specifies the minimum distance that must exist between two points in a geospatial query.', + snippet: '${1:distance}', + link: getDocLink('$minDistance', META_QUERY_GEOSPATIAL), + standalone: false, + }, + { + value: '$polygon', + meta: META_QUERY_GEOSPATIAL, + description: + 'The $polygon operator defines a polygon for geospatial queries, allowing you to find locations within an irregular shape.', + snippet: '[[${1:x1}, ${2:y1}], [${3:x2}, ${4:y2}], [${5:x3}, ${6:y3}]]', + link: getDocLink('$polygon', META_QUERY_GEOSPATIAL), + standalone: false, + }, + { + value: '$near', + meta: META_QUERY_GEOSPATIAL, + description: + 'The $near operator returns documents with location fields that are near a specified point, sorted by distance.', + snippet: + '{ $near: { \\$geometry: { type: "Point", coordinates: [${1:lng}, ${2:lat}] }, \\$maxDistance: ${3:distance} } }', + link: getDocLink('$near', META_QUERY_GEOSPATIAL), + }, + { + value: '$nearSphere', + meta: META_QUERY_GEOSPATIAL, + description: + 'The $nearSphere operator returns documents whose location fields are near a specified point on a sphere, sorted by distance on a spherical surface.', + snippet: + '{ $nearSphere: { \\$geometry: { type: "Point", coordinates: [${1:lng}, ${2:lat}] }, \\$maxDistance: ${3:distance} } }', + link: getDocLink('$nearSphere', META_QUERY_GEOSPATIAL), + }, +]; + +// --------------------------------------------------------------------------- +// Array Query Operators +// --------------------------------------------------------------------------- + +const arrayQueryOperators: readonly OperatorEntry[] = [ + { + value: '$all', + meta: META_QUERY_ARRAY, + description: 'The $all operator helps finding array documents matching all the elements.', + snippet: '{ $all: [${1:value}] }', + link: getDocLink('$all', META_QUERY_ARRAY), + applicableBsonTypes: ['array'], + }, + { + value: '$elemMatch', + meta: META_QUERY_ARRAY, + description: + 'The $elemmatch operator returns complete array, qualifying criteria with at least one matching array element.', + snippet: '{ $elemMatch: { ${1:query} } }', + link: getDocLink('$elemMatch', META_QUERY_ARRAY), + applicableBsonTypes: ['array'], + }, + { + value: '$size', + meta: META_QUERY_ARRAY, + description: + 'The $size operator is used to query documents where an array field has a specified number of elements.', + snippet: '{ $size: ${1:number} }', + link: getDocLink('$size', META_QUERY_ARRAY), + applicableBsonTypes: ['array'], + }, +]; + +// --------------------------------------------------------------------------- +// Bitwise Query Operators +// --------------------------------------------------------------------------- + +const bitwiseQueryOperators: readonly OperatorEntry[] = [ + { + value: '$bitsAllClear', + meta: META_QUERY_BITWISE, + description: + 'The $bitsAllClear operator is used to match documents where all the bit positions specified in a bitmask are clear.', + snippet: '{ $bitsAllClear: ${1:bitmask} }', + link: getDocLink('$bitsAllClear', META_QUERY_BITWISE), + applicableBsonTypes: ['int32', 'long'], + }, + { + value: '$bitsAllSet', + meta: META_QUERY_BITWISE, + description: 'The bitsAllSet command is used to match documents where all the specified bit positions are set.', + snippet: '{ $bitsAllSet: ${1:bitmask} }', + link: getDocLink('$bitsAllSet', META_QUERY_BITWISE), + applicableBsonTypes: ['int32', 'long'], + }, + { + value: '$bitsAnyClear', + meta: META_QUERY_BITWISE, + description: + 'The $bitsAnyClear operator matches documents where any of the specified bit positions in a bitmask are clear.', + snippet: '{ $bitsAnyClear: ${1:bitmask} }', + link: getDocLink('$bitsAnyClear', META_QUERY_BITWISE), + applicableBsonTypes: ['int32', 'long'], + }, + { + value: '$bitsAnySet', + meta: META_QUERY_BITWISE, + description: + 'The $bitsAnySet operator returns documents where any of the specified bit positions are set to 1.', + snippet: '{ $bitsAnySet: ${1:bitmask} }', + link: getDocLink('$bitsAnySet', META_QUERY_BITWISE), + applicableBsonTypes: ['int32', 'long'], + }, +]; + +// --------------------------------------------------------------------------- +// Projection Operators +// --------------------------------------------------------------------------- + +const projectionOperators: readonly OperatorEntry[] = [ + { + value: '$', + meta: META_QUERY_PROJECTION, + description: + 'The $ positional operator identifies an element in an array to update without explicitly specifying the position of the element in the array.', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$', // inferred from another category + standalone: false, + }, + { + value: '$elemMatch', + meta: META_QUERY_PROJECTION, + description: + 'The $elemmatch operator returns complete array, qualifying criteria with at least one matching array element.', + snippet: '{ $elemMatch: { ${1:query} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-query/$elemmatch', // inferred from another category + }, + { + value: '$slice', + meta: META_QUERY_PROJECTION, + description: 'The $slice operator returns a subset of an array from any element onwards in the array.', + snippet: '{ $slice: ${1:number} }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$slice', // inferred from another category + }, +]; + +// --------------------------------------------------------------------------- +// Miscellaneous Query Operators +// --------------------------------------------------------------------------- + +const miscellaneousQueryOperators: readonly OperatorEntry[] = [ + { + value: '$comment', + meta: META_QUERY_MISC, + description: + 'The $comment operator adds a comment to a query to help identify the query in logs and profiler output.', + snippet: '{ $comment: "${1:comment}" }', + link: getDocLink('$comment', META_QUERY_MISC), + }, + { + value: '$rand', + meta: META_QUERY_MISC, + description: 'The $rand operator generates a random float value between 0 and 1.', + snippet: '{ $rand: {} }', + link: getDocLink('$rand', META_QUERY_MISC), + }, + { + value: '$natural', + meta: META_QUERY_MISC, + description: + 'The $natural operator forces the query to use the natural order of documents in a collection, providing control over document ordering and retrieval.', + snippet: '{ $natural: ${1:1} }', + link: getDocLink('$natural', META_QUERY_MISC), + standalone: false, + }, +]; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +export function loadQueryOperators(): void { + registerOperators([ + ...comparisonQueryOperators, + ...logicalQueryOperators, + ...elementQueryOperators, + ...evaluationQueryOperators, + ...geospatialOperators, + ...arrayQueryOperators, + ...bitwiseQueryOperators, + ...projectionOperators, + ...miscellaneousQueryOperators, + ]); +} diff --git a/packages/documentdb-constants/src/stages.ts b/packages/documentdb-constants/src/stages.ts new file mode 100644 index 000000000..0752d7734 --- /dev/null +++ b/packages/documentdb-constants/src/stages.ts @@ -0,0 +1,291 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED โ€” DO NOT EDIT BY HAND +// +// Generated by: npm run generate (scripts/generate-from-reference.ts) +// Sources: resources/scraped/operator-reference.md +// resources/overrides/operator-overrides.md +// resources/overrides/operator-snippets.md +// +// To change operator data, edit the overrides/snippets files and re-run the generator. + +import { getDocLink } from './docLinks'; +import { registerOperators } from './getFilteredCompletions'; +import { META_STAGE } from './metaTags'; +import { type OperatorEntry } from './types'; + +// --------------------------------------------------------------------------- +// Aggregation Pipeline Stages +// --------------------------------------------------------------------------- + +const aggregationPipelineStages: readonly OperatorEntry[] = [ + { + value: '$addFields', + meta: META_STAGE, + description: 'The $addFields stage in the aggregation pipeline is used to add new fields to documents.', + snippet: '{ $addFields: { ${1:newField}: ${2:expression} } }', + link: getDocLink('$addFields', META_STAGE), + }, + { + value: '$bucket', + meta: META_STAGE, + description: 'Groups input documents into buckets based on specified boundaries.', + snippet: '{ $bucket: { groupBy: "${1:\\$field}", boundaries: [${2:values}], default: "${3:Other}" } }', + link: getDocLink('$bucket', META_STAGE), + }, + { + value: '$bucketAuto', + meta: META_STAGE, + description: + 'Categorizes documents into a specified number of groups based on a given expression, automatically determining bucket boundaries.', + snippet: '{ $bucketAuto: { groupBy: "${1:\\$field}", buckets: ${2:number} } }', + }, + { + value: '$changeStream', + meta: META_STAGE, + description: 'The $changeStream stage opens a change stream cursor to track data changes in real-time.', + snippet: '{ $changeStream: {} }', + link: getDocLink('$changeStream', META_STAGE), + }, + { + value: '$collStats', + meta: META_STAGE, + description: + 'The $collStats stage in the aggregation pipeline is used to return statistics about a collection.', + snippet: '{ $collStats: { storageStats: {} } }', + link: getDocLink('$collStats', META_STAGE), + }, + { + value: '$count', + meta: META_STAGE, + description: + 'The `$count` operator is used to count the number of documents that match a query filtering criteria.', + snippet: '{ $count: "${1:countField}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$count', // inferred from another category + }, + { + value: '$densify', + meta: META_STAGE, + description: 'Adds missing data points in a sequence of values within an array or collection.', + snippet: '{ $densify: { field: "${1:field}", range: { step: ${2:1}, bounds: "full" } } }', + link: getDocLink('$densify', META_STAGE), + }, + { + value: '$documents', + meta: META_STAGE, + description: 'The $documents stage creates a pipeline from a set of provided documents.', + snippet: '{ $documents: [${1:documents}] }', + link: getDocLink('$documents', META_STAGE), + }, + { + value: '$facet', + meta: META_STAGE, + description: + 'The $facet allows for multiple parallel aggregations to be executed within a single pipeline stage.', + snippet: '{ $facet: { ${1:outputField}: [{ ${2:stage} }] } }', + link: getDocLink('$facet', META_STAGE), + }, + { + value: '$fill', + meta: META_STAGE, + description: + 'The $fill stage allows filling missing values in documents based on specified methods and criteria.', + snippet: '{ $fill: { output: { ${1:field}: { method: "${2:linear}" } } } }', + link: getDocLink('$fill', META_STAGE), + }, + { + value: '$geoNear', + meta: META_STAGE, + description: + 'The $geoNear operator finds and sorts documents by their proximity to a geospatial point, returning distance information for each document.', + snippet: + '{ $geoNear: { near: { type: "Point", coordinates: [${1:lng}, ${2:lat}] }, distanceField: "${3:distance}" } }', + link: getDocLink('$geoNear', META_STAGE), + }, + { + value: '$graphLookup', + meta: META_STAGE, + description: + 'Performs a recursive search on a collection to return documents connected by a specified field relationship.', + snippet: + '{ $graphLookup: { from: "${1:collection}", startWith: "${2:\\$field}", connectFromField: "${3:field}", connectToField: "${4:field}", as: "${5:result}" } }', + }, + { + value: '$group', + meta: META_STAGE, + description: + 'The $group stage groups documents by specified identifier expressions and applies accumulator expressions.', + snippet: '{ $group: { _id: "${1:\\$field}", ${2:accumulator}: { ${3:\\$sum}: 1 } } }', + link: getDocLink('$group', META_STAGE), + }, + { + value: '$indexStats', + meta: META_STAGE, + description: 'The $indexStats stage returns usage statistics for each index in the collection.', + snippet: '{ $indexStats: {} }', + link: getDocLink('$indexStats', META_STAGE), + }, + { + value: '$limit', + meta: META_STAGE, + description: 'Restricts the number of documents passed to the next stage in the pipeline.', + snippet: '{ $limit: ${1:number} }', + }, + { + value: '$lookup', + meta: META_STAGE, + description: + 'The $lookup stage in the Aggregation Framework is used to perform left outer joins with other collections.', + snippet: + '{ $lookup: { from: "${1:collection}", localField: "${2:field}", foreignField: "${3:field}", as: "${4:result}" } }', + link: getDocLink('$lookup', META_STAGE), + }, + { + value: '$match', + meta: META_STAGE, + description: + 'The $match stage in the aggregation pipeline is used to filter documents that match a specified condition.', + snippet: '{ $match: { ${1:query} } }', + link: getDocLink('$match', META_STAGE), + }, + { + value: '$merge', + meta: META_STAGE, + description: + 'The $merge stage in an aggregation pipeline writes the results of the aggregation to a specified collection.', + snippet: '{ $merge: { into: "${1:collection}" } }', + link: getDocLink('$merge', META_STAGE), + }, + { + value: '$out', + meta: META_STAGE, + description: + 'The `$out` stage in an aggregation pipeline writes the resulting documents to a specified collection.', + snippet: '{ $out: "${1:collection}" }', + link: getDocLink('$out', META_STAGE), + }, + { + value: '$project', + meta: META_STAGE, + description: 'Reshapes documents by including, excluding, or computing new fields.', + snippet: '{ $project: { ${1:field}: 1 } }', + }, + { + value: '$redact', + meta: META_STAGE, + description: 'Filters the content of the documents based on access rights.', + snippet: + '{ $redact: { \\$cond: { if: { ${1:expression} }, then: "${2:\\$\\$DESCEND}", else: "${3:\\$\\$PRUNE}" } } }', + link: getDocLink('$redact', META_STAGE), + }, + { + value: '$replaceRoot', + meta: META_STAGE, + description: 'Replaces the input document with a specified embedded document, promoting it to the top level.', + snippet: '{ $replaceRoot: { newRoot: "${1:\\$field}" } }', + }, + { + value: '$replaceWith', + meta: META_STAGE, + description: + 'The $replaceWith operator in Azure DocumentDB returns a document after replacing a document with the specified document', + snippet: '{ $replaceWith: "${1:\\$field}" }', + link: getDocLink('$replaceWith', META_STAGE), + }, + { + value: '$sample', + meta: META_STAGE, + description: 'The $sample operator in Azure DocumentDB returns a randomly selected number of documents', + snippet: '{ $sample: { size: ${1:number} } }', + link: getDocLink('$sample', META_STAGE), + }, + { + value: '$search', + meta: META_STAGE, + description: 'Performs full-text search on string fields using Atlas Search or compatible search indexes.', + snippet: '{ $search: { ${1} } }', + }, + { + value: '$searchMeta', + meta: META_STAGE, + description: 'Returns metadata about an Atlas Search query without returning the matching documents.', + snippet: '{ $searchMeta: { ${1} } }', + }, + { + value: '$set', + meta: META_STAGE, + description: 'The $set operator in Azure DocumentDB updates or creates a new field with a specified value', + snippet: '{ $set: { ${1:field}: ${2:expression} } }', + link: getDocLink('$set', META_STAGE), + }, + { + value: '$setWindowFields', + meta: META_STAGE, + description: + 'Adds computed fields to documents using window functions over a specified partition and sort order.', + snippet: + '{ $setWindowFields: { partitionBy: "${1:\\$field}", sortBy: { ${2:field}: ${3:1} }, output: { ${4:newField}: { ${5:windowFunc} } } } }', + }, + { + value: '$skip', + meta: META_STAGE, + description: + 'The $skip stage in the aggregation pipeline is used to skip a specified number of documents from the input and pass the remaining documents to the next stage in the pipeline.', + snippet: '{ $skip: ${1:number} }', + link: getDocLink('$skip', META_STAGE), + }, + { + value: '$sort', + meta: META_STAGE, + description: + 'The $sort stage in the aggregation pipeline is used to order the documents in the pipeline by a specified field or fields.', + snippet: '{ $sort: { ${1:field}: ${2:1} } }', + link: getDocLink('$sort', META_STAGE), + }, + { + value: '$sortByCount', + meta: META_STAGE, + description: + 'The $sortByCount stage in the aggregation pipeline is used to group documents by a specified expression and then sort the count of documents in each group in descending order.', + snippet: '{ $sortByCount: "${1:\\$field}" }', + link: getDocLink('$sortByCount', META_STAGE), + }, + { + value: '$unionWith', + meta: META_STAGE, + description: 'Combines the results of two collections into a single result set, similar to SQL UNION ALL.', + snippet: '{ $unionWith: { coll: "${1:collection}", pipeline: [${2}] } }', + }, + { + value: '$unset', + meta: META_STAGE, + description: 'The $unset stage in the aggregation pipeline is used to remove specified fields from documents.', + snippet: '{ $unset: "${1:field}" }', + link: getDocLink('$unset', META_STAGE), + }, + { + value: '$unwind', + meta: META_STAGE, + description: + 'The $unwind stage in the aggregation framework is used to deconstruct an array field from the input documents to output a document for each element.', + snippet: '{ $unwind: "${1:\\$arrayField}" }', + link: getDocLink('$unwind', META_STAGE), + }, + { + value: '$currentOp', + meta: META_STAGE, + description: 'Returns information on active and queued operations for the database instance.', + snippet: '{ $currentOp: { allUsers: true } }', + }, +]; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +export function loadStages(): void { + registerOperators([...aggregationPipelineStages]); +} diff --git a/packages/documentdb-constants/src/structuralInvariants.test.ts b/packages/documentdb-constants/src/structuralInvariants.test.ts new file mode 100644 index 000000000..953fc7831 --- /dev/null +++ b/packages/documentdb-constants/src/structuralInvariants.test.ts @@ -0,0 +1,242 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Structural invariant tests for all operator entries. + * + * Validates that every entry in getAllCompletions() has the correct shape, + * consistent meta tags, and reasonable values. + */ + +import { ALL_META_TAGS, getAllCompletions, type OperatorEntry } from './index'; + +const allOperators = getAllCompletions(); + +describe('structural invariants', () => { + test('total operator count is in the expected range', () => { + // 308 total (298 from dump + 10 BSON constructors) + expect(allOperators.length).toBeGreaterThanOrEqual(290); + expect(allOperators.length).toBeLessThanOrEqual(320); + }); + + test('every entry has required fields', () => { + const invalid: string[] = []; + for (const op of allOperators) { + if (!op.value) { + invalid.push('entry missing value'); + } + if (!op.meta) { + invalid.push(`${op.value} missing meta`); + } + if (!op.description) { + invalid.push(`${op.value} missing description`); + } + } + expect(invalid).toEqual([]); + }); + + test('operator values start with $ or $$ (except BSON constructors)', () => { + const invalid: string[] = []; + for (const op of allOperators) { + if (op.meta === 'bson') { + // BSON constructors: ObjectId, ISODate, etc. โ€” no $ prefix + expect(op.value).toMatch(/^[A-Z]/); + } else if (op.meta === 'variable') { + // System variables start with $$ + if (!op.value.startsWith('$$')) { + invalid.push(`${op.value} (variable) should start with $$`); + } + } else { + // All other operators start with $ + if (!op.value.startsWith('$')) { + invalid.push(`${op.value} (${op.meta}) should start with $`); + } + } + } + expect(invalid).toEqual([]); + }); + + test('every entry has a valid meta tag', () => { + const validMetas = new Set(ALL_META_TAGS); + const invalid: string[] = []; + for (const op of allOperators) { + if (!validMetas.has(op.meta)) { + invalid.push(`${op.value} has unknown meta: ${op.meta}`); + } + } + expect(invalid).toEqual([]); + }); + + test('descriptions are non-empty strings', () => { + const empty: string[] = []; + for (const op of allOperators) { + if (typeof op.description !== 'string' || op.description.trim().length === 0) { + empty.push(`${op.value} (${op.meta}) has empty description`); + } + } + expect(empty).toEqual([]); + }); + + test('snippets are strings when present', () => { + const invalid: string[] = []; + for (const op of allOperators) { + if (op.snippet !== undefined && typeof op.snippet !== 'string') { + invalid.push(`${op.value} (${op.meta}) has non-string snippet`); + } + } + expect(invalid).toEqual([]); + }); + + test('links are valid URLs when present', () => { + const invalid: string[] = []; + for (const op of allOperators) { + if (op.link !== undefined) { + if (typeof op.link !== 'string' || !op.link.startsWith('https://')) { + invalid.push(`${op.value} (${op.meta}) has invalid link: ${op.link}`); + } + } + } + expect(invalid).toEqual([]); + }); + + test('applicableBsonTypes is a string array when present', () => { + const invalid: string[] = []; + for (const op of allOperators) { + if (op.applicableBsonTypes !== undefined) { + if (!Array.isArray(op.applicableBsonTypes)) { + invalid.push(`${op.value} (${op.meta}) applicableBsonTypes is not an array`); + } else { + for (const t of op.applicableBsonTypes) { + if (typeof t !== 'string' || t.trim().length === 0) { + invalid.push(`${op.value} (${op.meta}) has empty BSON type`); + } + } + } + } + } + expect(invalid).toEqual([]); + }); + + test('no duplicate (value, meta) pairs', () => { + const seen = new Set(); + const duplicates: string[] = []; + for (const op of allOperators) { + const key = `${op.value}|${op.meta}`; + if (seen.has(key)) { + duplicates.push(key); + } + seen.add(key); + } + expect(duplicates).toEqual([]); + }); + + test('BSON constructors have expected entries', () => { + const bsonOps = allOperators.filter((op) => op.meta === 'bson'); + const bsonValues = bsonOps.map((op) => op.value).sort(); + expect(bsonValues).toEqual( + expect.arrayContaining([ + 'BinData', + 'ISODate', + 'MaxKey', + 'MinKey', + 'NumberDecimal', + 'NumberInt', + 'NumberLong', + 'ObjectId', + 'Timestamp', + 'UUID', + ]), + ); + }); + + test('system variables have expected entries', () => { + const varOps = allOperators.filter((op) => op.meta === 'variable'); + const varValues = varOps.map((op) => op.value).sort(); + expect(varValues).toEqual( + expect.arrayContaining(['$$CURRENT', '$$DESCEND', '$$KEEP', '$$NOW', '$$PRUNE', '$$REMOVE', '$$ROOT']), + ); + }); + + test('key operators are present', () => { + const values = new Set(allOperators.map((op) => op.value)); + + // Query operators + expect(values.has('$eq')).toBe(true); + expect(values.has('$gt')).toBe(true); + expect(values.has('$and')).toBe(true); + expect(values.has('$regex')).toBe(true); + expect(values.has('$exists')).toBe(true); + + // Stages + expect(values.has('$match')).toBe(true); + expect(values.has('$group')).toBe(true); + expect(values.has('$lookup')).toBe(true); + expect(values.has('$project')).toBe(true); + expect(values.has('$sort')).toBe(true); + + // Update operators + expect(values.has('$set')).toBe(true); + expect(values.has('$unset')).toBe(true); + expect(values.has('$inc')).toBe(true); + + // Accumulators + expect(values.has('$sum')).toBe(true); + expect(values.has('$avg')).toBe(true); + + // Expressions + expect(values.has('$add')).toBe(true); + expect(values.has('$concat')).toBe(true); + expect(values.has('$cond')).toBe(true); + }); + + test('excluded operators are NOT present with unsupported meta tags', () => { + // These should not be present (deprecated or not supported) + const opsByValueMeta = new Map(); + for (const op of allOperators) { + opsByValueMeta.set(`${op.value}|${op.meta}`, op); + } + + // $where is deprecated and should not be present as evaluation query + expect(opsByValueMeta.has('$where|query:evaluation')).toBe(false); + }); +}); + +describe('meta tag coverage', () => { + test('every meta tag in ALL_META_TAGS has at least one operator (except parent-only and runtime tags)', () => { + const metasWithOps = new Set(allOperators.map((op) => op.meta)); + // Parent-only tags: operators use subcategories (query:comparison, update:field), + // not the bare 'query' or 'update' tags. 'field:identifier' is runtime-injected. + const parentOnlyTags = new Set(['query', 'update', 'field:identifier']); + const missing: string[] = []; + for (const tag of ALL_META_TAGS) { + if (parentOnlyTags.has(tag)) { + continue; + } + if (!metasWithOps.has(tag)) { + missing.push(tag); + } + } + expect(missing).toEqual([]); + }); + + test('top-level meta categories have expected operator counts', () => { + const countByPrefix: Record = {}; + for (const op of allOperators) { + const prefix = op.meta.includes(':') ? op.meta.split(':')[0] : op.meta; + countByPrefix[prefix] = (countByPrefix[prefix] || 0) + 1; + } + + expect(countByPrefix['query']).toBe(43); + expect(countByPrefix['update']).toBe(22); + expect(countByPrefix['stage']).toBe(35); + expect(countByPrefix['accumulator']).toBe(21); + expect(countByPrefix['window']).toBe(27); + expect(countByPrefix['bson']).toBe(10); + expect(countByPrefix['variable']).toBe(7); + // Expression operators: ~143-144 + expect(countByPrefix['expr']).toBeGreaterThanOrEqual(140); + expect(countByPrefix['expr']).toBeLessThanOrEqual(150); + }); +}); diff --git a/packages/documentdb-constants/src/systemVariables.ts b/packages/documentdb-constants/src/systemVariables.ts new file mode 100644 index 000000000..219d04eb0 --- /dev/null +++ b/packages/documentdb-constants/src/systemVariables.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED โ€” DO NOT EDIT BY HAND +// +// Generated by: npm run generate (scripts/generate-from-reference.ts) +// Sources: resources/scraped/operator-reference.md +// resources/overrides/operator-overrides.md +// resources/overrides/operator-snippets.md +// +// To change operator data, edit the overrides/snippets files and re-run the generator. + +import { registerOperators } from './getFilteredCompletions'; +import { META_VARIABLE } from './metaTags'; +import { type OperatorEntry } from './types'; + +// --------------------------------------------------------------------------- +// Variables in Aggregation Expressions +// --------------------------------------------------------------------------- + +const systemVariables: readonly OperatorEntry[] = [ + { + value: '$$NOW', + meta: META_VARIABLE, + description: + 'Returns the current datetime as a Date object. Constant throughout a single aggregation pipeline.', + }, + { + value: '$$ROOT', + meta: META_VARIABLE, + description: + 'References the root document โ€” the top-level document currently being processed in the pipeline stage.', + }, + { + value: '$$REMOVE', + meta: META_VARIABLE, + description: + 'Removes a field from the output document. Used with $project or $addFields to conditionally exclude fields.', + }, + { + value: '$$CURRENT', + meta: META_VARIABLE, + description: + 'References the current document in the pipeline stage. Equivalent to $$ROOT at the start of the pipeline.', + }, + { + value: '$$DESCEND', + meta: META_VARIABLE, + description: + 'Used with $redact. Returns the document fields at the current level and continues descending into subdocuments.', + }, + { + value: '$$PRUNE', + meta: META_VARIABLE, + description: + 'Used with $redact. Excludes all fields at the current document level and stops descending into subdocuments.', + }, + { + value: '$$KEEP', + meta: META_VARIABLE, + description: + 'Used with $redact. Keeps all fields at the current document level without further descending into subdocuments.', + }, +]; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +export function loadSystemVariables(): void { + registerOperators([...systemVariables]); +} diff --git a/packages/documentdb-constants/src/types.ts b/packages/documentdb-constants/src/types.ts new file mode 100644 index 000000000..d08cac711 --- /dev/null +++ b/packages/documentdb-constants/src/types.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ALL_META_TAGS } from './metaTags'; + +/** + * Represents a single operator, stage, accumulator, or BSON constructor + * for use in autocomplete, hover docs, and diagnostics. + */ +export interface OperatorEntry { + /** The operator string, e.g. "$gt", "$match", "ObjectId" */ + readonly value: string; + + /** + * Category tag for filtering. Determines which contexts this entry + * appears in. See {@link MetaTag} for the full set. + * + * Examples: "query", "query:comparison", "stage", "accumulator", + * "expr:arith", "expr:date", "bson", "field:identifier" + */ + readonly meta: MetaTag; + + /** Human-readable one-line description. */ + readonly description: string; + + /** + * Monaco snippet with tab stops for insertion. + * Example: '{ \\$match: { ${1:field}: ${2:value} } }' + * If absent, `value` is inserted as-is. + */ + readonly snippet?: string; + + /** + * URL to the DocumentDB documentation page for this operator. + * Generated from `docLinks.ts` helpers. + */ + readonly link?: string; + + /** + * Applicable BSON types for type-aware filtering. + * If set, this operator only appears when the field's bsonType + * matches one of these values. If absent, the operator is universal. + * + * Example: $regex โ†’ ['string'], $size โ†’ ['array'] + */ + readonly applicableBsonTypes?: readonly string[]; + + /** + * Whether this operator is valid as a standalone completion at top-level + * positions (key, value, operator). Defaults to `true` when absent. + * + * Set to `false` for operators that are only valid inside another operator's + * value object โ€” e.g., geospatial shape specifiers (`$box`, `$geometry`) + * which are only valid inside `$geoWithin`/`$near`, or sort-only modifiers + * like `$natural`. + * + * Completion providers should filter out `standalone === false` entries + * from standard completion lists. These entries remain in the registry + * for hover documentation and future context-aware nested completions. + */ + readonly standalone?: boolean; + + /** + * @experimental Not yet populated by the generator; reserved for a future + * contextual-snippet feature. + * + * When populated, this field carries a hint about the type of value an operator + * produces or expects, enabling the CompletionItemProvider to tailor snippets + * and insert sensible placeholder values based on context. + * + * Planned values and their meanings: + * - `"number"` โ€” operator always produces a number + * (e.g. `$size` on an array field โ†’ insert a numeric comparand) + * - `"boolean"` โ€” operator produces true/false + * (e.g. `$and`, `$or` in expression context) + * - `"string"` โ€” operator produces a string + * (e.g. `$concat`, `$toLower`) + * - `"array"` โ€” operator produces an array + * (e.g. `$push` accumulator, `$concatArrays`) + * - `"date"` โ€” operator produces a date + * (e.g. `$dateAdd`, `$toDate`) + * - `"same"` โ€” operator produces the same type as its input + * (e.g. `$min`, `$max`, comparison operators like `$gt`) + * - `"object"` โ€” operator produces a document/object + * (e.g. `$mergeObjects`) + * - `"any"` โ€” return type is undetermined or context-dependent + * + * This field is intentionally absent from all current entries. The generator + * (`scripts/generate-from-reference.ts`) does not yet emit it. It will be + * populated in a follow-up pass once the `CompletionItemProvider` is ready + * to consume it. + */ + readonly returnType?: string; +} + +/** + * Filter configuration for {@link getFilteredCompletions}. + */ +export interface CompletionFilter { + /** + * Meta tag prefixes to include. Supports prefix matching: + * 'query' matches 'query', 'query:comparison', 'query:logical', etc. + * 'expr' matches all 'expr:*' entries. + */ + readonly meta: readonly string[]; + + /** Optional: only return operators applicable to these BSON types. */ + readonly bsonTypes?: readonly string[]; +} + +/** + * Meta tag constants. Tags use a hierarchical scheme: + * + * - 'query' โ€” top-level query operators (in find filter, $match) + * - 'query:comparison' โ€” comparison subset ($eq, $gt, etc.) + * - 'query:logical' โ€” logical ($and, $or, $not, $nor) + * - 'query:element' โ€” element ($exists, $type) + * - 'query:evaluation' โ€” evaluation ($expr, $regex, $mod, $text) + * - 'query:array' โ€” array ($all, $elemMatch, $size) + * - 'query:bitwise' โ€” bitwise ($bitsAllSet, etc.) + * - 'query:geospatial' โ€” geospatial ($geoWithin, $near, etc.) + * - 'query:projection' โ€” projection ($, $elemMatch, $slice) + * - 'query:misc' โ€” miscellaneous ($comment, $rand, $natural) + * - 'update' โ€” update operators ($set, $unset, $inc, etc.) + * - 'update:field' โ€” field update subset + * - 'update:array' โ€” array update subset ($push, $pull, etc.) + * - 'update:bitwise' โ€” bitwise update ($bit) + * - 'stage' โ€” aggregation pipeline stages ($match, $group, etc.) + * - 'accumulator' โ€” accumulators ($sum, $avg, $first, etc.) + * - 'expr:arith' โ€” arithmetic expressions ($add, $subtract, etc.) + * - 'expr:array' โ€” array expressions ($arrayElemAt, $filter, etc.) + * - 'expr:bool' โ€” boolean expressions ($and, $or, $not) + * - 'expr:comparison' โ€” comparison expressions ($cmp, $eq, etc.) + * - 'expr:conditional' โ€” conditional ($cond, $ifNull, $switch) + * - 'expr:date' โ€” date expressions ($dateAdd, $year, etc.) + * - 'expr:object' โ€” object expressions ($mergeObjects, etc.) + * - 'expr:set' โ€” set expressions ($setUnion, etc.) + * - 'expr:string' โ€” string expressions ($concat, $substr, etc.) + * - 'expr:trig' โ€” trigonometry ($sin, $cos, etc.) + * - 'expr:type' โ€” type conversion ($convert, $toInt, etc.) + * - 'expr:datasize' โ€” data size ($bsonSize, $binarySize) + * - 'expr:timestamp' โ€” timestamp ($tsIncrement, $tsSecond) + * - 'expr:bitwise' โ€” bitwise expressions ($bitAnd, $bitOr, etc.) + * - 'expr:literal' โ€” $literal + * - 'expr:misc' โ€” miscellaneous expressions ($getField, $rand, etc.) + * - 'expr:variable' โ€” variable expressions ($let) + * - 'window' โ€” window operators ($rank, $denseRank, etc.) + * - 'bson' โ€” BSON constructor functions (ObjectId, ISODate, etc.) + * - 'variable' โ€” system variables ($$NOW, $$ROOT, etc.) + * - 'field:identifier' โ€” injected field names from schema (not static) + */ +export type MetaTag = (typeof ALL_META_TAGS)[number] | (string & {}); diff --git a/packages/documentdb-constants/src/updateOperators.ts b/packages/documentdb-constants/src/updateOperators.ts new file mode 100644 index 000000000..a90f62fcd --- /dev/null +++ b/packages/documentdb-constants/src/updateOperators.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED โ€” DO NOT EDIT BY HAND +// +// Generated by: npm run generate (scripts/generate-from-reference.ts) +// Sources: resources/scraped/operator-reference.md +// resources/overrides/operator-overrides.md +// resources/overrides/operator-snippets.md +// +// To change operator data, edit the overrides/snippets files and re-run the generator. + +import { getDocLink } from './docLinks'; +import { registerOperators } from './getFilteredCompletions'; +import { META_UPDATE_ARRAY, META_UPDATE_BITWISE, META_UPDATE_FIELD } from './metaTags'; +import { type OperatorEntry } from './types'; + +// --------------------------------------------------------------------------- +// Field Update Operators +// --------------------------------------------------------------------------- + +const fieldUpdateOperators: readonly OperatorEntry[] = [ + { + value: '$currentDate', + meta: META_UPDATE_FIELD, + description: + 'The $currentDate operator sets the value of a field to the current date, either as a Date or a timestamp.', + snippet: '{ $currentDate: { "${1:field}": true } }', + link: getDocLink('$currentDate', META_UPDATE_FIELD), + }, + { + value: '$inc', + meta: META_UPDATE_FIELD, + description: 'The $inc operator increments the value of a field by a specified amount.', + snippet: '{ $inc: { "${1:field}": ${2:value} } }', + link: getDocLink('$inc', META_UPDATE_FIELD), + }, + { + value: '$min', + meta: META_UPDATE_FIELD, + description: 'Retrieves the minimum value for a specified field', + snippet: '{ $min: { "${1:field}": ${2:value} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$min', // inferred from another category + }, + { + value: '$max', + meta: META_UPDATE_FIELD, + description: 'The $max operator returns the maximum value from a set of input values.', + snippet: '{ $max: { "${1:field}": ${2:value} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$max', // inferred from another category + }, + { + value: '$mul', + meta: META_UPDATE_FIELD, + description: 'The $mul operator multiplies the value of a field by a specified number.', + snippet: '{ $mul: { "${1:field}": ${2:value} } }', + link: getDocLink('$mul', META_UPDATE_FIELD), + }, + { + value: '$rename', + meta: META_UPDATE_FIELD, + description: 'The $rename operator allows renaming fields in documents during update operations.', + snippet: '{ $rename: { "${1:oldField}": "${2:newField}" } }', + link: getDocLink('$rename', META_UPDATE_FIELD), + }, + { + value: '$set', + meta: META_UPDATE_FIELD, + description: 'The $set operator in Azure DocumentDB updates or creates a new field with a specified value', + snippet: '{ $set: { "${1:field}": ${2:value} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$set', // inferred from another category + }, + { + value: '$setOnInsert', + meta: META_UPDATE_FIELD, + description: + 'The $setOnInsert operator sets field values only when an upsert operation results in an insert of a new document.', + snippet: '{ $setOnInsert: { "${1:field}": ${2:value} } }', + link: getDocLink('$setOnInsert', META_UPDATE_FIELD), + }, + { + value: '$unset', + meta: META_UPDATE_FIELD, + description: 'The $unset stage in the aggregation pipeline is used to remove specified fields from documents.', + snippet: '{ $unset: { "${1:field}": ${2:value} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$unset', // inferred from another category + }, +]; + +// --------------------------------------------------------------------------- +// Array Update Operators +// --------------------------------------------------------------------------- + +const arrayUpdateOperators: readonly OperatorEntry[] = [ + { + value: '$', + meta: META_UPDATE_ARRAY, + description: + 'The $ positional operator identifies an element in an array to update without explicitly specifying the position of the element in the array.', + link: getDocLink('$', META_UPDATE_ARRAY), + }, + { + value: '$[]', + meta: META_UPDATE_ARRAY, + description: 'Positional all operator. Acts as a placeholder to update all elements in an array field.', + }, + { + value: '$[identifier]', + meta: META_UPDATE_ARRAY, + description: + 'Filtered positional operator. Acts as a placeholder to update elements that match an arrayFilters condition.', + }, + { + value: '$addToSet', + meta: META_UPDATE_ARRAY, + description: + "The addToSet operator adds elements to an array if they don't already exist, while ensuring uniqueness of elements within the set.", + snippet: '{ $addToSet: { "${1:field}": ${2:value} } }', + link: getDocLink('$addToSet', META_UPDATE_ARRAY), + }, + { + value: '$pop', + meta: META_UPDATE_ARRAY, + description: 'Removes the first or last element of an array.', + snippet: '{ $pop: { "${1:field}": ${2:1} } }', + link: getDocLink('$pop', META_UPDATE_ARRAY), + }, + { + value: '$pull', + meta: META_UPDATE_ARRAY, + description: 'Removes all instances of a value from an array.', + snippet: '{ $pull: { "${1:field}": ${2:condition} } }', + link: getDocLink('$pull', META_UPDATE_ARRAY), + }, + { + value: '$push', + meta: META_UPDATE_ARRAY, + description: 'The $push operator adds a specified value to an array within a document.', + snippet: '{ $push: { "${1:field}": ${2:value} } }', + link: getDocLink('$push', META_UPDATE_ARRAY), + }, + { + value: '$pullAll', + meta: META_UPDATE_ARRAY, + description: 'The $pullAll operator is used to remove all instances of the specified values from an array.', + snippet: '{ $pullAll: { "${1:field}": [${2:values}] } }', + link: getDocLink('$pullAll', META_UPDATE_ARRAY), + }, + { + value: '$each', + meta: META_UPDATE_ARRAY, + description: + 'The $each operator is used within an `$addToSet`or`$push` operation to add multiple elements to an array field in a single update operation.', + snippet: '{ $each: [${1:values}] }', + link: getDocLink('$each', META_UPDATE_ARRAY), + }, + { + value: '$position', + meta: META_UPDATE_ARRAY, + description: + 'Specifies the position in the array at which the $push operator inserts elements. Used with $each.', + snippet: '{ $position: ${1:index} }', + }, + { + value: '$slice', + meta: META_UPDATE_ARRAY, + description: 'The $slice operator returns a subset of an array from any element onwards in the array.', + snippet: '{ $slice: ${1:number} }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-expression/$slice', // inferred from another category + }, + { + value: '$sort', + meta: META_UPDATE_ARRAY, + description: + 'The $sort stage in the aggregation pipeline is used to order the documents in the pipeline by a specified field or fields.', + snippet: '{ $sort: { "${1:field}": ${2:1} } }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/aggregation/$sort', // inferred from another category + }, +]; + +// --------------------------------------------------------------------------- +// Bitwise Update Operators +// --------------------------------------------------------------------------- + +const bitwiseUpdateOperators: readonly OperatorEntry[] = [ + { + value: '$bit', + meta: META_UPDATE_BITWISE, + description: 'The `$bit` operator is used to perform bitwise operations on integer values.', + snippet: '{ $bit: { "${1:field}": { "${2:and|or|xor}": ${3:value} } } }', + link: getDocLink('$bit', META_UPDATE_BITWISE), + }, +]; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +export function loadUpdateOperators(): void { + registerOperators([...fieldUpdateOperators, ...arrayUpdateOperators, ...bitwiseUpdateOperators]); +} diff --git a/packages/documentdb-constants/src/windowOperators.ts b/packages/documentdb-constants/src/windowOperators.ts new file mode 100644 index 000000000..f15b412e1 --- /dev/null +++ b/packages/documentdb-constants/src/windowOperators.ts @@ -0,0 +1,233 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED โ€” DO NOT EDIT BY HAND +// +// Generated by: npm run generate (scripts/generate-from-reference.ts) +// Sources: resources/scraped/operator-reference.md +// resources/overrides/operator-overrides.md +// resources/overrides/operator-snippets.md +// +// To change operator data, edit the overrides/snippets files and re-run the generator. + +import { getDocLink } from './docLinks'; +import { registerOperators } from './getFilteredCompletions'; +import { META_WINDOW } from './metaTags'; +import { type OperatorEntry } from './types'; + +// --------------------------------------------------------------------------- +// Window Operators +// --------------------------------------------------------------------------- + +const windowOperators: readonly OperatorEntry[] = [ + { + value: '$sum', + meta: META_WINDOW, + description: 'The $sum operator calculates the sum of the values of a field based on a filtering criteria', + snippet: '{ $sum: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$sum', // inferred from another category + }, + { + value: '$push', + meta: META_WINDOW, + description: 'The $push operator adds a specified value to an array within a document.', + snippet: '{ $push: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$push', // inferred from another category + }, + { + value: '$addToSet', + meta: META_WINDOW, + description: + "The addToSet operator adds elements to an array if they don't already exist, while ensuring uniqueness of elements within the set.", + snippet: '{ $addToSet: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/array-update/$addtoset', // inferred from another category + }, + { + value: '$count', + meta: META_WINDOW, + description: + 'The `$count` operator is used to count the number of documents that match a query filtering criteria.', + snippet: '{ $count: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$count', // inferred from another category + }, + { + value: '$max', + meta: META_WINDOW, + description: 'The $max operator returns the maximum value from a set of input values.', + snippet: '{ $max: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$max', // inferred from another category + }, + { + value: '$min', + meta: META_WINDOW, + description: 'Retrieves the minimum value for a specified field', + snippet: '{ $min: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$min', // inferred from another category + }, + { + value: '$avg', + meta: META_WINDOW, + description: 'Computes the average of numeric values for documents in a group, bucket, or window.', + snippet: '{ $avg: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$avg', // inferred from another category + }, + { + value: '$stdDevPop', + meta: META_WINDOW, + description: 'The $stddevpop operator calculates the standard deviation of the specified values', + snippet: '{ $stdDevPop: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$stddevpop', // inferred from another category + }, + { + value: '$bottom', + meta: META_WINDOW, + description: + "The $bottom operator returns the last document from the query's result set sorted by one or more fields", + snippet: '{ $bottom: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$bottom', // inferred from another category + }, + { + value: '$bottomN', + meta: META_WINDOW, + description: 'The $bottomN operator returns the last N documents from the result sorted by one or more fields', + snippet: '{ $bottomN: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$bottomn', // inferred from another category + }, + { + value: '$covariancePop', + meta: META_WINDOW, + description: 'The $covariancePop operator returns the covariance of two numerical expressions', + snippet: '{ $covariancePop: "${1:\\$field}" }', + link: getDocLink('$covariancePop', META_WINDOW), + }, + { + value: '$covarianceSamp', + meta: META_WINDOW, + description: 'The $covarianceSamp operator returns the covariance of a sample of two numerical expressions', + snippet: '{ $covarianceSamp: "${1:\\$field}" }', + link: getDocLink('$covarianceSamp', META_WINDOW), + }, + { + value: '$denseRank', + meta: META_WINDOW, + description: + 'The $denseRank operator assigns and returns a positional ranking for each document within a partition based on a specified sort order', + snippet: '{ $denseRank: {} }', + link: getDocLink('$denseRank', META_WINDOW), + }, + { + value: '$derivative', + meta: META_WINDOW, + description: + 'The $derivative operator calculates the average rate of change of the value of a field within a specified window.', + snippet: '{ $derivative: { input: "${1:\\$field}", unit: "${2:hour}" } }', + link: getDocLink('$derivative', META_WINDOW), + }, + { + value: '$documentNumber', + meta: META_WINDOW, + description: + 'The $documentNumber operator assigns and returns a position for each document within a partition based on a specified sort order', + snippet: '{ $documentNumber: {} }', + link: getDocLink('$documentNumber', META_WINDOW), + }, + { + value: '$expMovingAvg', + meta: META_WINDOW, + description: + 'The $expMovingAvg operator calculates the moving average of a field based on the specified number of documents to hold the highest weight', + snippet: '{ $expMovingAvg: { input: "${1:\\$field}", N: ${2:number} } }', + link: getDocLink('$expMovingAvg', META_WINDOW), + }, + { + value: '$first', + meta: META_WINDOW, + description: "The $first operator returns the first value in a group according to the group's sorting order.", + snippet: '{ $first: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$first', // inferred from another category + }, + { + value: '$integral', + meta: META_WINDOW, + description: + 'The $integral operator calculates the area under a curve with the specified range of documents forming the adjacent documents for the calculation.', + snippet: '{ $integral: { input: "${1:\\$field}", unit: "${2:hour}" } }', + link: getDocLink('$integral', META_WINDOW), + }, + { + value: '$last', + meta: META_WINDOW, + description: 'The $last operator returns the last document from the result sorted by one or more fields', + snippet: '{ $last: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$last', // inferred from another category + }, + { + value: '$linearFill', + meta: META_WINDOW, + description: + 'The $linearFill operator interpolates missing values in a sequence of documents using linear interpolation.', + snippet: '{ $linearFill: "${1:\\$field}" }', + link: getDocLink('$linearFill', META_WINDOW), + }, + { + value: '$locf', + meta: META_WINDOW, + description: + 'The $locf operator propagates the last observed non-null value forward within a partition in a windowed query.', + snippet: '{ $locf: "${1:\\$field}" }', + link: getDocLink('$locf', META_WINDOW), + }, + { + value: '$minN', + meta: META_WINDOW, + description: 'Retrieves the bottom N values based on a specified filtering criteria', + snippet: '{ $minN: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$minn', + }, + { + value: '$rank', + meta: META_WINDOW, + description: 'The $rank operator ranks documents within a partition based on a specified sort order.', + snippet: '{ $rank: {} }', + link: getDocLink('$rank', META_WINDOW), + }, + { + value: '$shift', + meta: META_WINDOW, + description: 'A window operator that shifts values within a partition and returns the shifted value.', + snippet: '{ $shift: { output: "${1:\\$field}", by: ${2:1}, default: ${3:null} } }', + link: getDocLink('$shift', META_WINDOW), + }, + { + value: '$stdDevSamp', + meta: META_WINDOW, + description: + 'The $stddevsamp operator calculates the standard deviation of a specified sample of values and not the entire population', + snippet: '{ $stdDevSamp: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$stddevsamp', // inferred from another category + }, + { + value: '$top', + meta: META_WINDOW, + description: 'The $top operator returns the first document from the result set sorted by one or more fields', + snippet: '{ $top: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$top', // inferred from another category + }, + { + value: '$topN', + meta: META_WINDOW, + description: 'The $topN operator returns the first N documents from the result sorted by one or more fields', + snippet: '{ $topN: "${1:\\$field}" }', + link: 'https://learn.microsoft.com/en-us/azure/documentdb/operators/accumulators/$topn', // inferred from another category + }, +]; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +export function loadWindowOperators(): void { + registerOperators([...windowOperators]); +} diff --git a/packages/documentdb-constants/tsconfig.json b/packages/documentdb-constants/tsconfig.json new file mode 100644 index 000000000..8688f97ff --- /dev/null +++ b/packages/documentdb-constants/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "module": "commonjs", + "target": "ES2023", + "lib": ["ES2023"], + "rootDir": "./src", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/documentdb-constants/tsconfig.scripts.json b/packages/documentdb-constants/tsconfig.scripts.json new file mode 100644 index 000000000..841c83b0a --- /dev/null +++ b/packages/documentdb-constants/tsconfig.scripts.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "declaration": false, + "declarationMap": false, + "noEmit": true, + "rootDir": ".", + "types": ["node"] + }, + "include": ["scripts/**/*", "src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/packages/schema-analyzer/README.md b/packages/schema-analyzer/README.md new file mode 100644 index 000000000..1efa58f49 --- /dev/null +++ b/packages/schema-analyzer/README.md @@ -0,0 +1,52 @@ +# @vscode-documentdb/schema-analyzer + +Incremental JSON Schema analyzer for DocumentDB API and MongoDB API documents. Processes documents one at a time (or in batches) and produces an extended JSON Schema with statistical metadata โ€” field occurrence counts, BSON type distributions, min/max values, and array length stats. + +> **Monorepo package** โ€” this package is part of the `vscode-documentdb` workspace. +> Dev dependencies (Jest, ts-jest, Prettier, TypeScript, etc.) are provided by the +> root `package.json`. Always install from the repository root: +> +> ```bash +> cd +> npm install +> ``` +> +> **Note:** This package is not yet published to npm. We plan to publish it once the API stabilizes. For now, it is consumed internally via npm workspaces within the [vscode-documentdb](https://github.com/microsoft/vscode-documentdb) repository. + +## Overview + +The `SchemaAnalyzer` incrementally builds a JSON Schema by inspecting DocumentDB API / MongoDB API documents. It is designed for scenarios where documents arrive over time (streaming, pagination) and the schema needs to evolve as new documents are observed. + +Key capabilities: + +- **Incremental analysis** โ€” add documents one at a time or in batches; the schema updates in place. +- **BSON type awareness** โ€” recognizes BSON types defined by the MongoDB API (`ObjectId`, `Decimal128`, `Binary`, `UUID`, etc.) and annotates them with `x-bsonType`. +- **Statistical extensions** โ€” tracks field occurrence (`x-occurrence`), type frequency (`x-typeOccurrence`), min/max values, string lengths, array sizes, and document counts (`x-documentsInspected`). +- **Known fields extraction** โ€” derives a flat list of known field paths with their types and occurrence probabilities, useful for autocomplete and UI rendering. +- **Version tracking & caching** โ€” a monotonic version counter enables efficient cache invalidation for derived data like `getKnownFields()`. + +## Usage + +```typescript +import { SchemaAnalyzer } from '@vscode-documentdb/schema-analyzer'; + +// Create an analyzer and feed it documents +const analyzer = new SchemaAnalyzer(); +analyzer.addDocument(doc1); +analyzer.addDocuments([doc2, doc3, doc4]); + +// Get the JSON Schema with statistical extensions +const schema = analyzer.getSchema(); + +// Get a flat list of known fields (cached, version-aware) +const fields = analyzer.getKnownFields(); +``` + +## Requirements + +- **Node.js** โ‰ฅ 18 +- **mongodb** driver โ‰ฅ 6.0.0 (peer dependency) + +## License + +[MIT](../../LICENSE.md) diff --git a/packages/schema-analyzer/jest.config.js b/packages/schema-analyzer/jest.config.js new file mode 100644 index 000000000..6aecf39aa --- /dev/null +++ b/packages/schema-analyzer/jest.config.js @@ -0,0 +1,11 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + // Limit workers to avoid OOM kills on machines with many cores. + // Each ts-jest worker loads the TypeScript compiler and consumes ~500MB+. + maxWorkers: '50%', + testEnvironment: 'node', + testMatch: ['/test/**/*.test.ts'], + transform: { + '^.+\\.tsx?$': ['ts-jest', {}], + }, +}; diff --git a/packages/schema-analyzer/package.json b/packages/schema-analyzer/package.json new file mode 100644 index 000000000..3751cdba2 --- /dev/null +++ b/packages/schema-analyzer/package.json @@ -0,0 +1,27 @@ +{ + "name": "@vscode-documentdb/schema-analyzer", + "version": "1.0.0", + "description": "Incremental JSON Schema analyzer for DocumentDB API / MongoDB API documents with statistical extensions", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p .", + "clean": "rimraf dist tsconfig.tsbuildinfo", + "test": "jest --config jest.config.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode-documentdb", + "directory": "packages/schema-analyzer" + }, + "license": "MIT", + "peerDependencies": { + "mongodb": ">=6.0.0" + }, + "dependencies": { + "denque": "~2.1.0" + } +} diff --git a/packages/schema-analyzer/src/BSONTypes.ts b/packages/schema-analyzer/src/BSONTypes.ts new file mode 100644 index 000000000..b8fb92f16 --- /dev/null +++ b/packages/schema-analyzer/src/BSONTypes.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + Binary, + BSONSymbol, + Code, + DBRef, + Decimal128, + Double, + Int32, + Long, + MaxKey, + MinKey, + ObjectId, + Timestamp, + UUID, +} from 'mongodb'; + +/** + * Represents the different data types that can be stored in a DocumentDB API / MongoDB API document. + * The string representation is case-sensitive and should match the MongoDB API documentation. + * https://www.mongodb.com/docs/manual/reference/bson-types/ + */ +export enum BSONTypes { + String = 'string', + Number = 'number', + Int32 = 'int32', + Double = 'double', + Decimal128 = 'decimal128', + Long = 'long', + Boolean = 'boolean', + Object = 'object', + Array = 'array', + Null = 'null', + Undefined = 'undefined', + Date = 'date', + RegExp = 'regexp', + Binary = 'binary', + ObjectId = 'objectid', + Symbol = 'symbol', + Timestamp = 'timestamp', + UUID = 'uuid', + UUID_LEGACY = 'uuid-legacy', // old UUID subtype, used in some legacy data + MinKey = 'minkey', + MaxKey = 'maxkey', + DBRef = 'dbref', + Code = 'code', + CodeWithScope = 'codewithscope', + Map = 'map', + // Add any deprecated types if necessary + _UNKNOWN_ = '_unknown_', // Catch-all for unknown types +} + +export namespace BSONTypes { + const displayStringMap: Record = { + [BSONTypes.String]: 'String', + [BSONTypes.Number]: 'Number', + [BSONTypes.Int32]: 'Int32', + [BSONTypes.Double]: 'Double', + [BSONTypes.Decimal128]: 'Decimal128', + [BSONTypes.Long]: 'Long', + [BSONTypes.Boolean]: 'Boolean', + [BSONTypes.Object]: 'Object', + [BSONTypes.Array]: 'Array', + [BSONTypes.Null]: 'Null', + [BSONTypes.Undefined]: 'Undefined', + [BSONTypes.Date]: 'Date', + [BSONTypes.RegExp]: 'RegExp', + [BSONTypes.Binary]: 'Binary', + [BSONTypes.ObjectId]: 'ObjectId', + [BSONTypes.Symbol]: 'Symbol', + [BSONTypes.Timestamp]: 'Timestamp', + [BSONTypes.MinKey]: 'MinKey', + [BSONTypes.MaxKey]: 'MaxKey', + [BSONTypes.DBRef]: 'DBRef', + [BSONTypes.Code]: 'Code', + [BSONTypes.CodeWithScope]: 'CodeWithScope', + [BSONTypes.Map]: 'Map', + [BSONTypes._UNKNOWN_]: 'Unknown', + [BSONTypes.UUID]: 'UUID', + [BSONTypes.UUID_LEGACY]: 'UUID (Legacy)', + }; + + export function toDisplayString(type: BSONTypes): string { + return displayStringMap[type] || 'Unknown'; + } + + export function toString(type: BSONTypes): string { + return type; + } + + /** + * Converts a MongoDB API data type to a case-sensitive JSON data type + * @param type The MongoDB API data type + * @returns A corresponding JSON data type (please note: it's case sensitive) + */ + export function toJSONType(type: BSONTypes): string { + switch (type) { + case BSONTypes.String: + case BSONTypes.Symbol: + case BSONTypes.Date: + case BSONTypes.Timestamp: + case BSONTypes.ObjectId: + case BSONTypes.RegExp: + case BSONTypes.Binary: + case BSONTypes.Code: + case BSONTypes.UUID: + case BSONTypes.UUID_LEGACY: + return 'string'; + + case BSONTypes.Boolean: + return 'boolean'; + + case BSONTypes.Int32: + case BSONTypes.Long: + case BSONTypes.Double: + case BSONTypes.Decimal128: + return 'number'; + + case BSONTypes.Object: + case BSONTypes.Map: + case BSONTypes.DBRef: + case BSONTypes.CodeWithScope: + return 'object'; + + case BSONTypes.Array: + return 'array'; + + case BSONTypes.Null: + case BSONTypes.Undefined: + case BSONTypes.MinKey: + case BSONTypes.MaxKey: + return 'null'; + + default: + return 'string'; // Default to string for unknown types + } + } + + /** + * Accepts a value from a MongoDB API `Document` object and returns the inferred type. + * @param value The value of a field in a MongoDB API `Document` object + * @returns + */ + export function inferType(value: unknown): BSONTypes { + if (value === null) return BSONTypes.Null; + if (value === undefined) return BSONTypes.Undefined; + + switch (typeof value) { + case 'string': + return BSONTypes.String; + case 'number': + return BSONTypes.Double; // JavaScript numbers are doubles + case 'boolean': + return BSONTypes.Boolean; + case 'object': + if (Array.isArray(value)) { + return BSONTypes.Array; + } + + // Check for common BSON types first + if (value instanceof ObjectId) return BSONTypes.ObjectId; + if (value instanceof Int32) return BSONTypes.Int32; + if (value instanceof Double) return BSONTypes.Double; + if (value instanceof Date) return BSONTypes.Date; + if (value instanceof Timestamp) return BSONTypes.Timestamp; + + // Less common types + if (value instanceof Decimal128) return BSONTypes.Decimal128; + if (value instanceof Long) return BSONTypes.Long; + if (value instanceof MinKey) return BSONTypes.MinKey; + if (value instanceof MaxKey) return BSONTypes.MaxKey; + if (value instanceof BSONSymbol) return BSONTypes.Symbol; + if (value instanceof DBRef) return BSONTypes.DBRef; + if (value instanceof Map) return BSONTypes.Map; + if (value instanceof UUID && value.sub_type === Binary.SUBTYPE_UUID) return BSONTypes.UUID; + if (value instanceof UUID && value.sub_type === Binary.SUBTYPE_UUID_OLD) return BSONTypes.UUID_LEGACY; + if (value instanceof Buffer || value instanceof Binary) return BSONTypes.Binary; + if (value instanceof RegExp) return BSONTypes.RegExp; + if (value instanceof Code) { + if (value.scope) { + return BSONTypes.CodeWithScope; + } else { + return BSONTypes.Code; + } + } + + // Default to Object if none of the above match + return BSONTypes.Object; + default: + // This should never happen, but if it does, we'll catch it here + // TODO: add telemetry somewhere to know when it happens (not here, this could get hit too often) + return BSONTypes._UNKNOWN_; + } + } +} diff --git a/src/utils/json/JSONSchema.ts b/packages/schema-analyzer/src/JSONSchema.ts similarity index 80% rename from src/utils/json/JSONSchema.ts rename to packages/schema-analyzer/src/JSONSchema.ts index 467669ed5..3127932d6 100644 --- a/src/utils/json/JSONSchema.ts +++ b/packages/schema-analyzer/src/JSONSchema.ts @@ -24,16 +24,14 @@ export interface JSONSchema { $id?: string; $schema?: string; type?: string | string[]; - 'x-documentsInspected'?: number; - 'x-occurrence'?: number; - 'x-typeOccurrence'?: number; - 'x-bsonType'?: string; // Explicitly declare the key with a dash using quotes title?: string; + description?: string; definitions?: { [name: string]: JSONSchema; }; - description?: string; - properties?: JSONSchema; // changed from: JSONSchemaMap; + + // Structure + properties?: JSONSchemaMap; patternProperties?: JSONSchemaMap; additionalProperties?: JSONSchemaRef; minProperties?: number; @@ -44,7 +42,6 @@ export interface JSONSchema { [prop: string]: string[]; }; items?: JSONSchemaRef | JSONSchemaRef[]; - required?: string[]; $ref?: string; anyOf?: JSONSchemaRef[]; @@ -58,14 +55,35 @@ export interface JSONSchema { propertyNames?: JSONSchemaRef; examples?: undefined[]; $comment?: string; - $defs?: { [name: string]: JSONSchema; }; + + // Monaco extensions markdownEnumDescriptions?: string[]; markdownDescription?: string; doNotSuggest?: boolean; suggestSortText?: string; + + // SchemaAnalyzer extensions โ€” document/field level + 'x-documentsInspected'?: number; + 'x-occurrence'?: number; + + // SchemaAnalyzer extensions โ€” type entry level (on entries in anyOf) + 'x-bsonType'?: string; + 'x-typeOccurrence'?: number; + 'x-minValue'?: number; + 'x-maxValue'?: number; + 'x-minLength'?: number; + 'x-maxLength'?: number; + 'x-minDate'?: number; + 'x-maxDate'?: number; + 'x-trueCount'?: number; + 'x-falseCount'?: number; + 'x-minItems'?: number; + 'x-maxItems'?: number; + 'x-minProperties'?: number; + 'x-maxProperties'?: number; } export interface JSONSchemaMap { [name: string]: JSONSchemaRef; diff --git a/src/utils/json/mongo/SchemaAnalyzer.ts b/packages/schema-analyzer/src/SchemaAnalyzer.ts similarity index 56% rename from src/utils/json/mongo/SchemaAnalyzer.ts rename to packages/schema-analyzer/src/SchemaAnalyzer.ts index 278f51fc4..8f24d532a 100644 --- a/src/utils/json/mongo/SchemaAnalyzer.ts +++ b/packages/schema-analyzer/src/SchemaAnalyzer.ts @@ -3,66 +3,125 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import Denque from 'denque'; +import { type Document, type WithId } from 'mongodb'; +import assert from 'node:assert/strict'; +import { BSONTypes } from './BSONTypes'; +import { type JSONSchema, type JSONSchemaRef } from './JSONSchema'; +import { type FieldEntry, getKnownFields as getKnownFieldsFromSchema } from './getKnownFields'; + /** - * This is an example of a JSON Schema document that will be generated from MongoDB documents. - * It's optimized for the use-case of generating a schema for a table view, the monaco editor, and schema statistics. - * - * This is a 'work in progress' and will be updated as we progress with the project. - * - * Curent focus is: - * - discovery of the document structure - * - basic pre for future statistics work + * Incremental schema analyzer for documents from the MongoDB API / DocumentDB API. * - * Future tasks: - * - statistics aggregation - * - meaningful 'description' and 'markdownDescription' - * - add more properties to the schema, incl. properties like '$id', '$schema', and enable schema sharing/download + * Analyzes documents one at a time (or in batches) and builds a cumulative + * JSON Schema with statistical extensions (x-occurrence, x-bsonType, etc.). * + * The output schema follows JSON Schema draft-07 with custom x- extensions. + */ +export class SchemaAnalyzer { + private _schema: JSONSchema = {}; + private _version: number = 0; + private _knownFieldsCache: FieldEntry[] | null = null; + private _knownFieldsCacheVersion: number = -1; -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://example.com/sample.schema.json", - "title": "Sample Document Schema", - "type": "object", - "properties": { - "a-propert-root-level": { - "description": "a description as text", - "anyOf": [ // anyOf is used to indicate that the value can be of any of the types listed - { - "type": "string" - }, - { - "type": "string" + /** + * A monotonically increasing version counter. Incremented on every mutation + * (addDocument, addDocuments, reset). Adapters can store this value alongside + * their cached derived data and recompute only when it changes. + */ + get version(): number { + return this._version; + } + + /** + * Adds a single document to the accumulated schema. + * This is the primary incremental API โ€” call once per document. + */ + addDocument(document: WithId): void { + updateSchemaWithDocumentInternal(this._schema, document); + this._version++; + } + + /** + * Adds multiple documents to the accumulated schema. + * Convenience method equivalent to calling addDocument() for each. + * Increments version once for the entire batch โ€” not per document. + */ + addDocuments(documents: ReadonlyArray>): void { + for (const doc of documents) { + updateSchemaWithDocumentInternal(this._schema, doc); } - ] - }, - "isOpen": { - "description": "Indicates if the item is open", - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "number" + this._version++; + } + + /** + * Returns the current accumulated JSON Schema. + * The returned object is a live reference (not a copy) โ€” do not mutate externally. + */ + getSchema(): JSONSchema { + return this._schema; + } + + /** + * Returns the number of documents analyzed so far. + */ + getDocumentCount(): number { + return (this._schema['x-documentsInspected'] as number) ?? 0; + } + + /** + * Resets the analyzer to its initial empty state. + */ + reset(): void { + this._schema = {}; + this._version++; + } + + /** + * Creates a deep copy of this analyzer, including all accumulated schema data. + * Useful for aggregation stage branching where each stage needs its own schema state. + * The clone starts with version 0, independent from the original. + */ + clone(): SchemaAnalyzer { + const copy = new SchemaAnalyzer(); + copy._schema = structuredClone(this._schema); + return copy; + } + + /** + * Returns the cached list of known fields (all nesting levels, sorted). + * Recomputed only when the schema version has changed since the last call. + */ + getKnownFields(): FieldEntry[] { + if (this._knownFieldsCacheVersion !== this._version || this._knownFieldsCache === null) { + this._knownFieldsCache = getKnownFieldsFromSchema(this._schema); + this._knownFieldsCacheVersion = this._version; } - ] + return this._knownFieldsCache; } - }, - "required": ["isOpen"] -} - * - * - */ + /** + * Creates a SchemaAnalyzer from a single document. + * Equivalent to creating an instance and calling addDocument() once. + */ + static fromDocument(document: WithId): SchemaAnalyzer { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(document); + return analyzer; + } -import * as l10n from '@vscode/l10n'; -import { assert } from 'console'; -import Denque from 'denque'; -import { type Document, type WithId } from 'mongodb'; -import { type JSONSchema } from '../JSONSchema'; -import { MongoBSONTypes } from './MongoBSONTypes'; + /** + * Creates a SchemaAnalyzer from multiple documents. + * Equivalent to creating an instance and calling addDocuments(). + */ + static fromDocuments(documents: ReadonlyArray>): SchemaAnalyzer { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocuments(documents); + return analyzer; + } +} -export function updateSchemaWithDocument(schema: JSONSchema, document: WithId): void { +function updateSchemaWithDocumentInternal(schema: JSONSchema, document: WithId): void { // Initialize schema if it's empty if (!schema.properties) { schema.properties = {}; @@ -74,7 +133,7 @@ export function updateSchemaWithDocument(schema: JSONSchema, document: WithId; const objKeysCount = Object.keys(objValue).length; // Update min and max property counts updateMinMaxStats(item.propertySchema, 'x-minProperties', 'x-maxProperties', objKeysCount); + // Track how many object instances contributed to this sub-schema. + // This enables uniform probability computation at every nesting level: + // probability = property.x-occurrence / parentObject.x-documentsInspected + // + // Without this, array-embedded objects have no denominator for probability. + // Example: doc1.a=[], doc2.a=[{b:1},...,{b:100}] + // b.x-occurrence = 100, root.x-documentsInspected = 2 + // Naive: 100/2 = 5000% โ€” wrong! + // With fix: objectEntry.x-documentsInspected = 100, so 100/100 = 100% + item.propertySchema['x-documentsInspected'] = (item.propertySchema['x-documentsInspected'] ?? 0) + 1; + // Ensure 'properties' exists if (!item.propertySchema.properties) { item.propertySchema.properties = {}; @@ -158,7 +228,7 @@ export function updateSchemaWithDocument(schema: JSONSchema, document: WithId = new Map(); - // Iterate over the array elements for (const element of arrayValue) { - const elementMongoType = MongoBSONTypes.inferType(element); + const elementMongoType = BSONTypes.inferType(element); // Find or create the type entry in 'items.anyOf' let itemEntry = findTypeEntry(itemsSchema.anyOf as JSONSchema[], elementMongoType); + const isNewTypeEntry = !itemEntry; if (!itemEntry) { // Create a new type entry itemEntry = { - type: MongoBSONTypes.toJSONType(elementMongoType), + type: BSONTypes.toJSONType(elementMongoType), 'x-bsonType': elementMongoType, 'x-typeOccurrence': 0, }; @@ -249,18 +317,19 @@ export function updateSchemaWithDocument(schema: JSONSchema, document: WithId entry['x-bsonType'] === bsonType); } @@ -299,221 +368,69 @@ function findTypeEntry(anyOfArray: JSONSchema[], bsonType: MongoBSONTypes): JSON * Helper function to update min and max stats */ function updateMinMaxStats(schema: JSONSchema, minKey: string, maxKey: string, value: number): void { - if (schema[minKey] === undefined || value < schema[minKey]) { - schema[minKey] = value; + const record = schema as Record; + if (record[minKey] === undefined || value < (record[minKey] as number)) { + record[minKey] = value; } - if (schema[maxKey] === undefined || value > schema[maxKey]) { - schema[maxKey] = value; + if (record[maxKey] === undefined || value > (record[maxKey] as number)) { + record[maxKey] = value; } } -export function getSchemaFromDocument(document: WithId): JSONSchema { - const schema: JSONSchema = {}; - schema['x-documentsInspected'] = 1; // we're inspecting one document, this will make sense when we start aggregating stats - schema.properties = {}; - - type WorkItem = { - fieldName: string; - fieldMongoType: MongoBSONTypes; // the inferred BSON type - propertyTypeEntry: JSONSchema; // points to the entry within the 'anyOf' property of the schema - fieldValue: unknown; - pathSoFar: string; // used for debugging - }; - - // having some import/require issues with Denque atm - // prototype with an array - //const fifoQueue = new Denque(); - const fifoQueue: WorkItem[] = []; - - /** - * Push all elements from the root of the document into the queue - */ - for (const [name, value] of Object.entries(document)) { - const mongoDatatype = MongoBSONTypes.inferType(value); - - const typeEntry = { - type: MongoBSONTypes.toJSONType(mongoDatatype), - 'x-bsonType': mongoDatatype, - 'x-typeOccurrence': 1, - }; - - // please note (1/2): we're adding the type entry to the schema here - schema.properties[name] = { anyOf: [typeEntry], 'x-occurrence': 1 }; - - fifoQueue.push({ - fieldName: name, - fieldMongoType: mongoDatatype, - propertyTypeEntry: typeEntry, // please note (2/2): and we're keeping a reference to it here for further updates - fieldValue: value, - pathSoFar: name, - }); - } - - /** - * Work through the queue, adding elements to the schema as we go. - * This is a breadth-first search of the document, do note special - * handling on objects/arrays - */ - while (fifoQueue.length > 0) { - const item = fifoQueue.shift(); // todo, replace with a proper queue - if (item === undefined) { - // unexpected, but let's try to continue - continue; - } - - switch (item.fieldMongoType) { - case MongoBSONTypes.Object: { - const objKeys = Object.keys(item.fieldValue as object).length; - item.propertyTypeEntry['x-maxLength'] = objKeys; - item.propertyTypeEntry['x-minLength'] = objKeys; - - // prepare an entry for the object properties - item.propertyTypeEntry.properties = {}; - - for (const [name, value] of Object.entries(item.fieldValue as object)) { - const mongoDatatype = MongoBSONTypes.inferType(value); - - const typeEntry = { - type: MongoBSONTypes.toJSONType(mongoDatatype), - 'x-bsonType': mongoDatatype, - 'x-typeOccurrence': 1, - }; - - // please note (1/2): we're adding the entry to the main schema here - item.propertyTypeEntry.properties[name] = { anyOf: [typeEntry], 'x-occurrence': 1 }; - - fifoQueue.push({ - fieldName: name, - fieldMongoType: mongoDatatype, - propertyTypeEntry: typeEntry, // please note (2/2): and we're keeping a reference to it here for further updates to the schema - fieldValue: value, - pathSoFar: `${item.pathSoFar}.${item.fieldName}`, - }); - } - break; - } - case MongoBSONTypes.Array: { - const arrayLength = (item.fieldValue as unknown[]).length; - item.propertyTypeEntry['x-maxLength'] = arrayLength; - item.propertyTypeEntry['x-minLength'] = arrayLength; - - // preapare the array items entry (in two lines for ts not to compalin about the missing type later on) - item.propertyTypeEntry.items = {}; - item.propertyTypeEntry.items.anyOf = []; - - const encounteredMongoTypes: Map = new Map(); - - // iterate over the array and infer the type of each element - for (const element of item.fieldValue as unknown[]) { - const elementMongoType = MongoBSONTypes.inferType(element); - - let itemEntry: JSONSchema; - - if (!encounteredMongoTypes.has(elementMongoType)) { - itemEntry = { - type: MongoBSONTypes.toJSONType(elementMongoType), - 'x-bsonType': elementMongoType, - 'x-typeOccurrence': 1, // Initialize type occurrence counter - }; - item.propertyTypeEntry.items.anyOf.push(itemEntry); - encounteredMongoTypes.set(elementMongoType, itemEntry); - - initializeStatsForValue(element, elementMongoType, itemEntry); - } else { - // if we've already encountered this type, we'll just add the type to the existing entry - itemEntry = encounteredMongoTypes.get(elementMongoType) as JSONSchema; - - if (itemEntry === undefined) continue; // unexpected, but let's try to continue - - if (itemEntry['x-typeOccurrence'] !== undefined) { - itemEntry['x-typeOccurrence'] += 1; - } - - // Aggregate stats with the new value - aggregateStatsForValue(element, elementMongoType, itemEntry); - } - - // an imporant exception for arrays as we have to start adding them already now to the schema - // (if we want to avoid more iterations over the data) - if (elementMongoType === MongoBSONTypes.Object || elementMongoType === MongoBSONTypes.Array) { - fifoQueue.push({ - fieldName: '[]', // Array items don't have a field name - fieldMongoType: elementMongoType, - propertyTypeEntry: itemEntry, - fieldValue: element, - pathSoFar: `${item.pathSoFar}.${item.fieldName}.items`, - }); - } - } - - break; - } - - default: { - // For all other types, update stats for the value - initializeStatsForValue(item.fieldValue, item.fieldMongoType, item.propertyTypeEntry); - break; - } - } - } - - return schema; -} - /** * Helper function to compute stats for a value based on its MongoDB data type * Updates the provided propertyTypeEntry with the computed stats */ -function initializeStatsForValue(value: unknown, mongoType: MongoBSONTypes, propertyTypeEntry: JSONSchema): void { +function initializeStatsForValue(value: unknown, mongoType: BSONTypes, propertyTypeEntry: JSONSchema): void { switch (mongoType) { - case MongoBSONTypes.String: { + case BSONTypes.String: { const currentLength = (value as string).length; propertyTypeEntry['x-maxLength'] = currentLength; propertyTypeEntry['x-minLength'] = currentLength; break; } - case MongoBSONTypes.Number: - case MongoBSONTypes.Int32: - case MongoBSONTypes.Long: - case MongoBSONTypes.Double: - case MongoBSONTypes.Decimal128: { + case BSONTypes.Number: + case BSONTypes.Int32: + case BSONTypes.Long: + case BSONTypes.Double: + case BSONTypes.Decimal128: { const numericValue = Number(value); propertyTypeEntry['x-maxValue'] = numericValue; propertyTypeEntry['x-minValue'] = numericValue; break; } - case MongoBSONTypes.Boolean: { + case BSONTypes.Boolean: { const boolValue = value as boolean; propertyTypeEntry['x-trueCount'] = boolValue ? 1 : 0; propertyTypeEntry['x-falseCount'] = boolValue ? 0 : 1; break; } - case MongoBSONTypes.Date: { + case BSONTypes.Date: { const dateValue = (value as Date).getTime(); propertyTypeEntry['x-maxDate'] = dateValue; propertyTypeEntry['x-minDate'] = dateValue; break; } - case MongoBSONTypes.Binary: { + case BSONTypes.Binary: { const binaryLength = (value as Buffer).length; propertyTypeEntry['x-maxLength'] = binaryLength; propertyTypeEntry['x-minLength'] = binaryLength; break; } - case MongoBSONTypes.Null: - case MongoBSONTypes.RegExp: - case MongoBSONTypes.ObjectId: - case MongoBSONTypes.MinKey: - case MongoBSONTypes.MaxKey: - case MongoBSONTypes.Symbol: - case MongoBSONTypes.Timestamp: - case MongoBSONTypes.DBRef: - case MongoBSONTypes.Map: + case BSONTypes.Null: + case BSONTypes.RegExp: + case BSONTypes.ObjectId: + case BSONTypes.MinKey: + case BSONTypes.MaxKey: + case BSONTypes.Symbol: + case BSONTypes.Timestamp: + case BSONTypes.DBRef: + case BSONTypes.Map: // No stats computation for other types break; @@ -527,9 +444,9 @@ function initializeStatsForValue(value: unknown, mongoType: MongoBSONTypes, prop * Helper function to aggregate stats for a value based on its MongoDB data type * Used when processing multiple values (e.g., elements in arrays) */ -function aggregateStatsForValue(value: unknown, mongoType: MongoBSONTypes, propertyTypeEntry: JSONSchema): void { +function aggregateStatsForValue(value: unknown, mongoType: BSONTypes, propertyTypeEntry: JSONSchema): void { switch (mongoType) { - case MongoBSONTypes.String: { + case BSONTypes.String: { const currentLength = (value as string).length; // Update minLength @@ -544,11 +461,11 @@ function aggregateStatsForValue(value: unknown, mongoType: MongoBSONTypes, prope break; } - case MongoBSONTypes.Number: - case MongoBSONTypes.Int32: - case MongoBSONTypes.Long: - case MongoBSONTypes.Double: - case MongoBSONTypes.Decimal128: { + case BSONTypes.Number: + case BSONTypes.Int32: + case BSONTypes.Long: + case BSONTypes.Double: + case BSONTypes.Decimal128: { const numericValue = Number(value); // Update minValue @@ -563,7 +480,7 @@ function aggregateStatsForValue(value: unknown, mongoType: MongoBSONTypes, prope break; } - case MongoBSONTypes.Boolean: { + case BSONTypes.Boolean: { const boolValue = value as boolean; // Update trueCount and falseCount @@ -581,7 +498,7 @@ function aggregateStatsForValue(value: unknown, mongoType: MongoBSONTypes, prope break; } - case MongoBSONTypes.Date: { + case BSONTypes.Date: { const dateValue = (value as Date).getTime(); // Update minDate @@ -596,7 +513,7 @@ function aggregateStatsForValue(value: unknown, mongoType: MongoBSONTypes, prope break; } - case MongoBSONTypes.Binary: { + case BSONTypes.Binary: { const binaryLength = (value as Buffer).length; // Update minLength @@ -617,17 +534,12 @@ function aggregateStatsForValue(value: unknown, mongoType: MongoBSONTypes, prope } } -function getSchemaAtPath(schema: JSONSchema, path: string[]): JSONSchema { - let currentNode = schema; +function getSchemaAtPath(schema: JSONSchema, path: string[]): JSONSchema | undefined { + let currentNode: JSONSchema | undefined = schema; for (let i = 0; i < path.length; i++) { const key = path[i]; - // If the current node is an array, we should move to its `items` - // if (currentNode.type === 'array' && currentNode.items) { - // currentNode = currentNode.items; - // } - // Move to the next property in the schema if (currentNode && currentNode.properties && currentNode.properties[key]) { const nextNode: JSONSchema = currentNode.properties[key] as JSONSchema; @@ -636,13 +548,15 @@ function getSchemaAtPath(schema: JSONSchema, path: string[]): JSONSchema { * We're looking at the "Object"-one, because these have the properties we're interested in. */ if (nextNode.anyOf && nextNode.anyOf.length > 0) { - currentNode = nextNode.anyOf.find((entry: JSONSchema) => entry.type === 'object') as JSONSchema; + currentNode = nextNode.anyOf.find( + (entry: JSONSchemaRef): entry is JSONSchema => typeof entry === 'object' && entry.type === 'object', + ); } else { // we can't continue, as we're missing the next node, we abort at the last node we managed to extract return currentNode; } } else { - throw new Error(l10n.t('No properties found in the schema at path "{0}"', path.slice(0, i + 1).join('/'))); + throw new Error(`No properties found in the schema at path "${path.slice(0, i + 1).join('/')}"`); } } @@ -653,7 +567,7 @@ export function getPropertyNamesAtLevel(jsonSchema: JSONSchema, path: string[]): const headers = new Set(); // Explore the schema and apply the callback to collect headers at the specified path - const selectedSchema: JSONSchema = getSchemaAtPath(jsonSchema, path); + const selectedSchema = getSchemaAtPath(jsonSchema, path); if (selectedSchema && selectedSchema.properties) { Object.keys(selectedSchema.properties).forEach((key) => { diff --git a/src/utils/json/mongo/MongoValueFormatters.ts b/packages/schema-analyzer/src/ValueFormatters.ts similarity index 56% rename from src/utils/json/mongo/MongoValueFormatters.ts rename to packages/schema-analyzer/src/ValueFormatters.ts index 243ce2631..7f9e8e5fa 100644 --- a/src/utils/json/mongo/MongoValueFormatters.ts +++ b/packages/schema-analyzer/src/ValueFormatters.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { type Binary, type BSONRegExp, type ObjectId } from 'mongodb'; -import { MongoBSONTypes } from './MongoBSONTypes'; +import { BSONTypes } from './BSONTypes'; /** - * Converts a MongoDB value to its display string representation based on its type. + * Converts a MongoDB API value to its display string representation based on its type. * * @param value - The value to be converted to a display string. - * @param type - The MongoDB data type of the value. + * @param type - The MongoDB API data type of the value. * @returns The string representation of the value. * - * The function handles various MongoDB data types including: + * The function handles various MongoDB API data types including: * - String * - Number, Int32, Double, Decimal128, Long * - Boolean @@ -24,60 +24,60 @@ import { MongoBSONTypes } from './MongoBSONTypes'; * * For unsupported or unknown types, the function defaults to JSON stringification. */ -export function valueToDisplayString(value: unknown, type: MongoBSONTypes): string { +export function valueToDisplayString(value: unknown, type: BSONTypes): string { switch (type) { - case MongoBSONTypes.String: { + case BSONTypes.String: { return value as string; } - case MongoBSONTypes.Number: - case MongoBSONTypes.Int32: - case MongoBSONTypes.Double: - case MongoBSONTypes.Decimal128: - case MongoBSONTypes.Long: { + case BSONTypes.Number: + case BSONTypes.Int32: + case BSONTypes.Double: + case BSONTypes.Decimal128: + case BSONTypes.Long: { return (value as number).toString(); } - case MongoBSONTypes.Boolean: { + case BSONTypes.Boolean: { return (value as boolean).toString(); } - case MongoBSONTypes.Date: { + case BSONTypes.Date: { return (value as Date).toISOString(); } - case MongoBSONTypes.ObjectId: { + case BSONTypes.ObjectId: { return (value as ObjectId).toHexString(); } - case MongoBSONTypes.Null: { + case BSONTypes.Null: { return 'null'; } - case MongoBSONTypes.RegExp: { + case BSONTypes.RegExp: { const v = value as BSONRegExp; return `${v.pattern} ${v.options}`; } - case MongoBSONTypes.Binary: { + case BSONTypes.Binary: { return `Binary[${(value as Binary).length()}]`; } - case MongoBSONTypes.Symbol: { + case BSONTypes.Symbol: { return (value as symbol).toString(); } - case MongoBSONTypes.Timestamp: { + case BSONTypes.Timestamp: { return (value as { toString: () => string }).toString(); } - case MongoBSONTypes.MinKey: { + case BSONTypes.MinKey: { return 'MinKey'; } - case MongoBSONTypes.MaxKey: { + case BSONTypes.MaxKey: { return 'MaxKey'; } - case MongoBSONTypes.Code: - case MongoBSONTypes.CodeWithScope: { + case BSONTypes.Code: + case BSONTypes.CodeWithScope: { return JSON.stringify(value); } - case MongoBSONTypes.Array: - case MongoBSONTypes.Object: - case MongoBSONTypes.Map: - case MongoBSONTypes.DBRef: - case MongoBSONTypes.Undefined: - case MongoBSONTypes._UNKNOWN_: + case BSONTypes.Array: + case BSONTypes.Object: + case BSONTypes.Map: + case BSONTypes.DBRef: + case BSONTypes.Undefined: + case BSONTypes._UNKNOWN_: default: { return JSON.stringify(value); } diff --git a/packages/schema-analyzer/src/getKnownFields.ts b/packages/schema-analyzer/src/getKnownFields.ts new file mode 100644 index 000000000..f5da314b6 --- /dev/null +++ b/packages/schema-analyzer/src/getKnownFields.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Denque from 'denque'; +import { type JSONSchema } from './JSONSchema'; + +export interface FieldEntry { + /** Dot-notated path (e.g., "user.profile.name") */ + path: string; + /** JSON type of the dominant type entry ("string", "number", "object", "array", etc.) */ + type: string; + /** Dominant BSON type from x-bsonType on the most common type entry ("date", "objectid", "int32", etc.) */ + bsonType: string; + /** All observed BSON types for this field (for polymorphic fields) */ + bsonTypes?: string[]; + /** + * True if this field was not present in every inspected document + * (x-occurrence < parent x-documentsInspected). + * + * This is a statistical observation, not a schema constraint โ€” in the MongoDB API / DocumentDB API, + * all fields are implicitly optional. + */ + isSparse?: boolean; + /** If the field is an array, the dominant element BSON type */ + arrayItemBsonType?: string; +} + +/** + * This function traverses our JSON Schema object and collects all leaf property paths + * along with their most common data types. + * + * This information is needed for auto-completion support + * + * The approach is as follows: + * - Initialize a queue with the root properties of the schema to perform a breadth-first traversal. + * - While the queue is not empty: + * - Dequeue the next item, which includes the current schema node and its path. + * - Determine the most common type for the current node by looking at the 'x-typeOccurrence' field. + * - If the most common type is an object with properties: + * - Enqueue its child properties with their updated paths into the queue for further traversal. + * - Else if the most common type is a leaf type (e.g., string, number, boolean): + * - Add the current path and type to the result array as it represents a leaf property. + * - Continue this process until all nodes have been processed. + * - Return the result array containing objects with 'path' and 'type' for each leaf property. + */ +export function getKnownFields(schema: JSONSchema): FieldEntry[] { + const result: FieldEntry[] = []; + + type QueueItem = { + path: string; + schemaNode: JSONSchema; + parentDocumentsInspected: number; + }; + + const rootDocumentsInspected = (schema['x-documentsInspected'] as number) ?? 0; + const queue: Denque = new Denque(); + + // Initialize the queue with root properties + // + // Note: JSON Schema allows boolean values as schema references (true = accept all, + // false = reject all), but our SchemaAnalyzer never produces boolean refs โ€” it always + // emits full schema objects. The cast to JSONSchema below is therefore safe for our + // use case. If this function were ever reused with externally-sourced schemas, a + // `typeof propSchema === 'boolean'` guard should be added here and in the nested + // property loop below. + if (schema.properties) { + for (const propName of Object.keys(schema.properties)) { + const propSchema = schema.properties[propName] as JSONSchema; + queue.push({ + path: propName, + schemaNode: propSchema, + parentDocumentsInspected: rootDocumentsInspected, + }); + } + } + + while (queue.length > 0) { + const item = queue.shift(); + if (!item) continue; + + const { path, schemaNode, parentDocumentsInspected } = item; + const mostCommonTypeEntry = getMostCommonTypeEntry(schemaNode); + + if (mostCommonTypeEntry) { + if (mostCommonTypeEntry.type === 'object' && mostCommonTypeEntry.properties) { + // Not a leaf node, enqueue its properties + const objectDocumentsInspected = (mostCommonTypeEntry['x-documentsInspected'] as number) ?? 0; + for (const childName of Object.keys(mostCommonTypeEntry.properties)) { + const childSchema = mostCommonTypeEntry.properties[childName] as JSONSchema; + // TODO: Dot-delimited path concatenation is ambiguous when a field name + // itself contains a literal dot. For example, a root-level field named + // "a.b" produces path "a.b", indistinguishable from a nested field + // { a: { b: ... } }. Fields with literal dots in their names were + // prohibited before MongoDB API 3.6 and remain rare in practice. + // + // Future improvement: change `path` from `string` to `string[]` + // (segment array) to preserve the distinction between nesting and + // literal dots, pushing escaping/formatting decisions to consumers + // (TS definitions, completion items, aggregation references, etc.). + queue.push({ + path: `${path}.${childName}`, + schemaNode: childSchema, + parentDocumentsInspected: objectDocumentsInspected, + }); + } + } else { + // Leaf node, build the FieldEntry + const bsonType = (mostCommonTypeEntry['x-bsonType'] as string) ?? (mostCommonTypeEntry.type as string); + + const entry: FieldEntry = { + path, + type: mostCommonTypeEntry.type as string, + bsonType, + }; + + // bsonTypes: collect all distinct x-bsonType values from anyOf entries + const allBsonTypes = collectBsonTypes(schemaNode); + if (allBsonTypes.length >= 2) { + entry.bsonTypes = allBsonTypes; + } + + // isSparse: field was not observed in every document + const occurrence = (schemaNode['x-occurrence'] as number) ?? 0; + if (parentDocumentsInspected > 0 && occurrence < parentDocumentsInspected) { + entry.isSparse = true; + } + + // arrayItemBsonType: for array fields, find the dominant element type + if (mostCommonTypeEntry.type === 'array') { + const itemBsonType = getDominantArrayItemBsonType(mostCommonTypeEntry); + if (itemBsonType) { + entry.arrayItemBsonType = itemBsonType; + } + } + + result.push(entry); + } + } + } + + // Sort: _id first, then alphabetical by path + result.sort((a, b) => { + if (a.path === '_id') return -1; + if (b.path === '_id') return 1; + return a.path.localeCompare(b.path); + }); + + return result; +} + +/** + * Helper function to get the most common type entry from a schema node. + * It looks for the 'anyOf' array and selects the type with the highest 'x-typeOccurrence'. + */ +function getMostCommonTypeEntry(schemaNode: JSONSchema): JSONSchema | null { + if (schemaNode.anyOf && schemaNode.anyOf.length > 0) { + let maxOccurrence = -1; + let mostCommonTypeEntry: JSONSchema | null = null; + + for (const typeEntry of schemaNode.anyOf as JSONSchema[]) { + const occurrence = typeEntry['x-typeOccurrence'] || 0; + if (occurrence > maxOccurrence) { + maxOccurrence = occurrence; + mostCommonTypeEntry = typeEntry; + } + } + return mostCommonTypeEntry; + } else if (schemaNode.type) { + // If 'anyOf' is not present, use the 'type' field directly + return schemaNode; + } + return null; +} + +/** + * Collects all distinct x-bsonType values from a schema node's anyOf entries. + * Returns them sorted alphabetically for determinism. + */ +function collectBsonTypes(schemaNode: JSONSchema): string[] { + if (!schemaNode.anyOf || schemaNode.anyOf.length === 0) { + return []; + } + + const bsonTypes = new Set(); + for (const entry of schemaNode.anyOf as JSONSchema[]) { + const bsonType = entry['x-bsonType'] as string | undefined; + if (bsonType) { + bsonTypes.add(bsonType); + } + } + + return Array.from(bsonTypes).sort(); +} + +/** + * For an array type entry, finds the dominant element BSON type by looking at + * items.anyOf and selecting the entry with the highest x-typeOccurrence. + */ +function getDominantArrayItemBsonType(arrayTypeEntry: JSONSchema): string | undefined { + const itemsSchema = arrayTypeEntry.items as JSONSchema | undefined; + if (!itemsSchema?.anyOf || itemsSchema.anyOf.length === 0) { + return undefined; + } + + let maxOccurrence = -1; + let dominantBsonType: string | undefined; + + for (const entry of itemsSchema.anyOf as JSONSchema[]) { + const occurrence = (entry['x-typeOccurrence'] as number) ?? 0; + if (occurrence > maxOccurrence) { + maxOccurrence = occurrence; + dominantBsonType = entry['x-bsonType'] as string | undefined; + } + } + + return dominantBsonType; +} diff --git a/src/documentdb/scrapbook/languageServer.ts b/packages/schema-analyzer/src/index.ts similarity index 50% rename from src/documentdb/scrapbook/languageServer.ts rename to packages/schema-analyzer/src/index.ts index a94ef70a7..871fd61f8 100644 --- a/src/documentdb/scrapbook/languageServer.ts +++ b/packages/schema-analyzer/src/index.ts @@ -3,19 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LanguageService } from './services/languageService'; - -// -// -// -// HOW TO DEBUG THE LANGUAGE SERVER -// -// -// 1. Start the extension via F5 -// 2. Under vscode Debug pane, switch to "Attach to Language Server" -// 3. F5 -// -// -// - -new LanguageService(); +export { BSONTypes } from './BSONTypes'; +export { getKnownFields, type FieldEntry } from './getKnownFields'; +export { type JSONSchema, type JSONSchemaMap, type JSONSchemaRef } from './JSONSchema'; +export { SchemaAnalyzer, buildFullPaths, getPropertyNamesAtLevel } from './SchemaAnalyzer'; +export { valueToDisplayString } from './ValueFormatters'; diff --git a/packages/schema-analyzer/test/SchemaAnalyzer.arrayStats.test.ts b/packages/schema-analyzer/test/SchemaAnalyzer.arrayStats.test.ts new file mode 100644 index 000000000..2669d5214 --- /dev/null +++ b/packages/schema-analyzer/test/SchemaAnalyzer.arrayStats.test.ts @@ -0,0 +1,464 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ObjectId, type Document, type WithId } from 'mongodb'; +import { type JSONSchema } from '../src/JSONSchema'; +import { SchemaAnalyzer } from '../src/SchemaAnalyzer'; + +/** + * This test file investigates the array element occurrence/stats problem. + * + * The core issue: When an array contains mixed types (e.g., strings AND objects), + * `x-typeOccurrence` on the items' type entries counts individual elements across + * ALL documents, not occurrences-per-document. This makes "field presence probability" + * for nested object properties inside arrays hard to interpret. + * + * Example scenario: + * doc1.data = ["a", "b", "c", {"value": 23}] โ†’ 3 strings, 1 object + * doc2.data = ["x", "y", {"value": 42, "flag": true}] โ†’ 2 strings, 1 object + * doc3.data = ["z"] โ†’ 1 string, 0 objects + * + * After processing 3 docs: + * - items.anyOf[string].x-typeOccurrence = 6 (total string elements across all docs) + * - items.anyOf[object].x-typeOccurrence = 2 (total object elements across all docs) + * - items.anyOf[object].properties.value.x-occurrence = 2 (from 2 object elements) + * - items.anyOf[object].properties.flag.x-occurrence = 1 (from 1 object element) + * + * The problem: what is items.anyOf[object].properties.value's "probability"? + * - 2/2? (present in every object element โ†’ makes sense) + * - 2/3? (present in 2 of 3 documents โ†’ misleading, doc3 has no objects at all) + * - 2/6? (present in 2 of 6 total elements โ†’ nonsensical, mixes types) + * + * There's no x-documentsInspected equivalent at the array level to anchor + * the occurrence count. + */ +describe('Array element occurrence analysis', () => { + it('counts element types across multiple documents', () => { + const analyzer = new SchemaAnalyzer(); + + const doc1: WithId = { + _id: new ObjectId(), + data: ['a', 'b', 'c', { value: 23 }], + }; + const doc2: WithId = { + _id: new ObjectId(), + data: ['x', 'y', { value: 42, flag: true }], + }; + const doc3: WithId = { + _id: new ObjectId(), + data: ['z'], + }; + + analyzer.addDocument(doc1); + analyzer.addDocument(doc2); + analyzer.addDocument(doc3); + const schema = analyzer.getSchema(); + + // data field: array seen in 3 docs + const dataField = schema.properties?.['data'] as JSONSchema; + expect(dataField['x-occurrence']).toBe(3); + + // The array type entry + const arrayTypeEntry = dataField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + expect(arrayTypeEntry).toBeDefined(); + expect(arrayTypeEntry['x-typeOccurrence']).toBe(3); + + // Array items + const itemsSchema = arrayTypeEntry.items as JSONSchema; + const stringEntry = itemsSchema.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'string') as JSONSchema; + const objectEntry = itemsSchema.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'object') as JSONSchema; + + // String elements: "a","b","c","x","y","z" = 6 total + expect(stringEntry['x-typeOccurrence']).toBe(6); + + // Object elements: {value:23}, {value:42,flag:true} = 2 total + expect(objectEntry['x-typeOccurrence']).toBe(2); + + // Properties inside the object elements + const valueField = objectEntry.properties?.['value'] as JSONSchema; + const flagField = objectEntry.properties?.['flag'] as JSONSchema; + + // "value" appeared in both objects โ†’ x-occurrence = 2 + expect(valueField['x-occurrence']).toBe(2); + + // "flag" appeared in 1 object โ†’ x-occurrence = 1 + expect(flagField['x-occurrence']).toBe(1); + + // THE CORE QUESTION: What is the denominator for probability? + // + // We know objectEntry['x-typeOccurrence'] = 2 (2 objects total across all arrays). + // So valueField probability = 2/2 = 100% (correct: every object had "value") + // And flagField probability = 1/2 = 50% (correct: half of objects had "flag") + // + // BUT: there is NO x-documentsInspected on objectEntry to formally define + // the denominator. The consumer has to know to use x-typeOccurrence as the + // denominator for nested properties inside array elements. + // + // This actually WORKS โ€” the semantics are: + // "of the N objects observed inside this array, M had this property" + // + // It just isn't obvious from the schema structure. + }); + + it('tracks min/max array lengths across documents', () => { + const analyzer = new SchemaAnalyzer(); + + const doc1: WithId = { + _id: new ObjectId(), + tags: ['a', 'b', 'c'], + }; + const doc2: WithId = { + _id: new ObjectId(), + tags: ['x'], + }; + const doc3: WithId = { + _id: new ObjectId(), + tags: ['p', 'q', 'r', 's', 't'], + }; + + analyzer.addDocument(doc1); + analyzer.addDocument(doc2); + analyzer.addDocument(doc3); + const schema = analyzer.getSchema(); + + const tagsField = schema.properties?.['tags'] as JSONSchema; + const arrayEntry = tagsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + + expect(arrayEntry['x-minItems']).toBe(1); + expect(arrayEntry['x-maxItems']).toBe(5); + }); + + it('accumulates nested object properties from objects inside arrays across documents', () => { + const analyzer = new SchemaAnalyzer(); + + // doc1 has two objects with different properties in the items array + const doc1: WithId = { + _id: new ObjectId(), + items: [ + { name: 'Laptop', price: 999 }, + { name: 'Mouse', price: 29, discount: true }, + ], + }; + + // doc2 has one object with yet another property + const doc2: WithId = { + _id: new ObjectId(), + items: [{ name: 'Desk', weight: 50 }], + }; + + analyzer.addDocument(doc1); + analyzer.addDocument(doc2); + const schema = analyzer.getSchema(); + + const itemsField = schema.properties?.['items'] as JSONSchema; + const arrayEntry = itemsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + const objEntry = (arrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + const props = objEntry.properties as Record; + + // "name" appeared in all 3 object elements + expect(props['name']['x-occurrence']).toBe(3); + + // "price" appeared in 2 of 3 object elements + expect(props['price']['x-occurrence']).toBe(2); + + // "discount" appeared in 1 of 3 object elements + expect(props['discount']['x-occurrence']).toBe(1); + + // "weight" appeared in 1 of 3 object elements + expect(props['weight']['x-occurrence']).toBe(1); + + // Total object elements = 3 (2 from doc1 + 1 from doc2) + expect(objEntry['x-typeOccurrence']).toBe(3); + + // So probability interpretations: + // name: 3/3 = 100% + // price: 2/3 = 67% + // discount: 1/3 = 33% + // weight: 1/3 = 33% + // + // This is correct! x-typeOccurrence serves as the denominator. + }); + + it('handles arrays that ONLY contain primitives (no occurrence complexity)', () => { + const analyzer = new SchemaAnalyzer(); + + const doc1: WithId = { + _id: new ObjectId(), + scores: [90, 85, 78], + }; + const doc2: WithId = { + _id: new ObjectId(), + scores: [100, 55], + }; + + analyzer.addDocument(doc1); + analyzer.addDocument(doc2); + const schema = analyzer.getSchema(); + + const scoresField = schema.properties?.['scores'] as JSONSchema; + const arrayEntry = scoresField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + + const numEntry = (arrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'double', + ) as JSONSchema; + + // 5 total numeric elements + expect(numEntry['x-typeOccurrence']).toBe(5); + + // Stats across all elements + expect(numEntry['x-minValue']).toBe(55); + expect(numEntry['x-maxValue']).toBe(100); + + // Array length stats + expect(arrayEntry['x-minItems']).toBe(2); + expect(arrayEntry['x-maxItems']).toBe(3); + }); + + it('verifies that encounteredMongoTypes map is per-document', () => { + // The encounteredMongoTypes map is created inside the Array case handler. + // It controls whether initializeStatsForValue or aggregateStatsForValue is called. + // If it's per-array-occurrence (per document), stats should initialize fresh for each doc. + // + // BUT WAIT: The map is local to the switch case, which processes ONE array per queue item. + // Multiple documents contribute different queue items, and the map is re-created for each. + // However, the stats update goes to the SAME itemEntry across documents (because + // findTypeEntry finds the existing entry). So: + // + // doc1.scores = [10, 20] โ†’ first array processing, encounteredMongoTypes fresh + // - element 10: initializeStatsForValue (sets x-minValue=10, x-maxValue=10) + // - element 20: aggregateStatsForValue (updates x-maxValue=20) + // + // doc2.scores = [5, 30] โ†’ second array processing, encounteredMongoTypes fresh + // - element 5: initializeStatsForValue โ† BUT x-minValue is already 10 from doc1! + // initializeStatsForValue OVERWRITES x-minValue to 5 (correct by accident here) + // Actually let's check... initializeStatsForValue sets x-maxValue = 5 + // and x-minValue = 5. So the 20 from doc1 would be lost! + // + // This is a REAL BUG: initializeStatsForValue is called for the first occurrence + // per array, but the typeEntry already has stats from previous arrays. + + const analyzer = new SchemaAnalyzer(); + + const doc1: WithId = { + _id: new ObjectId(), + scores: [10, 20, 30], + }; + const doc2: WithId = { + _id: new ObjectId(), + scores: [5, 15], + }; + + analyzer.addDocument(doc1); + analyzer.addDocument(doc2); + const schema = analyzer.getSchema(); + + const scoresField = schema.properties?.['scores'] as JSONSchema; + const arrayEntry = scoresField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + + const numEntry = (arrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'double', + ) as JSONSchema; + + // Expected correct values: + // All 5 elements: 10, 20, 30, 5, 15 + // Global min = 5, global max = 30 + + // If there's a bug, doc2 processing re-initializes: + // after doc1: min=10, max=30 + // doc2 first element (5): initializeStatsForValue โ†’ sets min=5, max=5 + // doc2 second element (15): aggregateStatsForValue โ†’ max becomes 15 + // final: min=5, max=15 โ† WRONG (lost 30 from doc1) + + // This test documents the actual behavior (might be buggy): + expect(numEntry['x-minValue']).toBe(5); + // If the bug exists, this will be 15 instead of 30: + expect(numEntry['x-maxValue']).toBe(30); // should be 30 if correct + }); +}); + +describe('Array probability denominator problem', () => { + it('reproduces the >100% probability bug: empty array + large array', () => { + // User scenario: + // doc1: a = [] โ†’ 0 objects + // doc2: a = [{b:1}, {b:2}, ..., {b:100}] โ†’ 100 objects + // + // Naively computing probability as: + // occurrence_of_b / root.x-documentsInspected = 100 / 2 = 5000% + // + // The correct probability should be: + // occurrence_of_b / objectEntry.x-typeOccurrence = 100 / 100 = 100% + // + // FIX: Set x-documentsInspected on the object type entry so the uniform + // formula `x-occurrence / parent.x-documentsInspected` works at every + // nesting level. + + const analyzer = new SchemaAnalyzer(); + + const doc1: WithId = { + _id: new ObjectId(), + a: [], // empty array + }; + + // doc2: 100 objects, each with property "b" + const objectElements: Record[] = []; + for (let i = 1; i <= 100; i++) { + objectElements.push({ b: i }); + } + const doc2: WithId = { + _id: new ObjectId(), + a: objectElements, + }; + + analyzer.addDocument(doc1); + analyzer.addDocument(doc2); + const schema = analyzer.getSchema(); + + // Root level + expect(schema['x-documentsInspected']).toBe(2); + + // Navigate to the object type entry inside the array + const aField = schema.properties?.['a'] as JSONSchema; + expect(aField['x-occurrence']).toBe(2); // both docs have 'a' + + const arrayEntry = aField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + const objectEntry = (arrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + // 100 object elements total + expect(objectEntry['x-typeOccurrence']).toBe(100); + + // Property "b" appears in all 100 objects + const bField = objectEntry.properties?.['b'] as JSONSchema; + expect(bField['x-occurrence']).toBe(100); + + // THE FIX: objectEntry should have x-documentsInspected = 100 + // so that the uniform formula works: + // probability = b.x-occurrence / objectEntry.x-documentsInspected + // = 100 / 100 = 100% + expect(objectEntry['x-documentsInspected']).toBe(100); + }); + + it('correctly computes probability for sparse properties in array objects', () => { + // doc1: items = [{name:"A", price:10}, {name:"B"}] โ†’ 2 objects, name in both, price in 1 + // doc2: items = [{name:"C", discount:true}] โ†’ 1 object + // + // Total objects = 3 + // name: 3/3 = 100% + // price: 1/3 = 33% + // discount: 1/3 = 33% + + const analyzer = new SchemaAnalyzer(); + + const doc1: WithId = { + _id: new ObjectId(), + items: [{ name: 'A', price: 10 }, { name: 'B' }], + }; + const doc2: WithId = { + _id: new ObjectId(), + items: [{ name: 'C', discount: true }], + }; + + analyzer.addDocument(doc1); + analyzer.addDocument(doc2); + const schema = analyzer.getSchema(); + + const itemsField = schema.properties?.['items'] as JSONSchema; + const arrayEntry = itemsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + const objectEntry = (arrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + // The object type entry should have x-documentsInspected = 3 + expect(objectEntry['x-documentsInspected']).toBe(3); + + const props = objectEntry.properties as Record; + + // Probability = x-occurrence / x-documentsInspected (uniform formula) + expect(props['name']['x-occurrence']).toBe(3); // 3/3 = 100% + expect(props['price']['x-occurrence']).toBe(1); // 1/3 = 33% + expect(props['discount']['x-occurrence']).toBe(1); // 1/3 = 33% + }); + + it('sets x-documentsInspected on nested objects at all levels', () => { + // items: [{address: {city: "NY", zip: "10001"}}, {address: {city: "LA"}}] + // + // At items.anyOf[object] level: x-documentsInspected = 2 + // At address.anyOf[object] level: x-documentsInspected = 2 + // city: 2/2 = 100%, zip: 1/2 = 50% + + const analyzer = new SchemaAnalyzer(); + + const doc: WithId = { + _id: new ObjectId(), + items: [{ address: { city: 'NY', zip: '10001' } }, { address: { city: 'LA' } }], + }; + + analyzer.addDocument(doc); + const schema = analyzer.getSchema(); + + const itemsField = schema.properties?.['items'] as JSONSchema; + const arrayEntry = itemsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + const objectEntry = (arrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + // 2 objects in the array + expect(objectEntry['x-documentsInspected']).toBe(2); + + // address.anyOf[object] โ€” the nested object type + const addressProp = objectEntry.properties?.['address'] as JSONSchema; + const addressObjEntry = addressProp.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + // Both objects had address, and both addresses were objects + expect(addressObjEntry['x-documentsInspected']).toBe(2); + + const addrProps = addressObjEntry.properties as Record; + expect(addrProps['city']['x-occurrence']).toBe(2); // 2/2 = 100% + expect(addrProps['zip']['x-occurrence']).toBe(1); // 1/2 = 50% + }); + + it('does NOT change x-documentsInspected at root level (root keeps document count)', () => { + const analyzer = new SchemaAnalyzer(); + + const doc1: WithId = { + _id: new ObjectId(), + name: 'Alice', + address: { city: 'NY' }, + }; + const doc2: WithId = { + _id: new ObjectId(), + name: 'Bob', + address: { city: 'LA', zip: '90001' }, + }; + + analyzer.addDocument(doc1); + analyzer.addDocument(doc2); + const schema = analyzer.getSchema(); + + // Root x-documentsInspected is document count, not affected by the fix + expect(schema['x-documentsInspected']).toBe(2); + + // Root-level probability still works: name.occurrence(2) / documentsInspected(2) = 100% + const nameField = schema.properties?.['name'] as JSONSchema; + expect(nameField['x-occurrence']).toBe(2); + + // Nested object: address.anyOf[object] should have x-documentsInspected = 2 + const addressField = schema.properties?.['address'] as JSONSchema; + const addressObjEntry = addressField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + expect(addressObjEntry['x-documentsInspected']).toBe(2); + + const addrProps = addressObjEntry.properties as Record; + expect(addrProps['city']['x-occurrence']).toBe(2); // 2/2 = 100% + expect(addrProps['zip']['x-occurrence']).toBe(1); // 1/2 = 50% + }); +}); diff --git a/packages/schema-analyzer/test/SchemaAnalyzer.test.ts b/packages/schema-analyzer/test/SchemaAnalyzer.test.ts new file mode 100644 index 000000000..f23a97bdf --- /dev/null +++ b/packages/schema-analyzer/test/SchemaAnalyzer.test.ts @@ -0,0 +1,349 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type JSONSchema, type JSONSchemaMap, type JSONSchemaRef } from '../src/JSONSchema'; +import { getPropertyNamesAtLevel, SchemaAnalyzer } from '../src/SchemaAnalyzer'; +import { + arraysWithDifferentDataTypes, + complexDocument, + complexDocumentsArray, + complexDocumentWithOddTypes, + embeddedDocumentOnly, + flatDocument, + sparseDocumentsArray, +} from './mongoTestDocuments'; + +describe('DocumentDB Schema Analyzer', () => { + it('prints out schema for testing', () => { + const analyzer = SchemaAnalyzer.fromDocument(embeddedDocumentOnly); + const schema = analyzer.getSchema(); + expect(schema).toBeDefined(); + }); + + it('supports many documents', () => { + const analyzer = SchemaAnalyzer.fromDocuments(sparseDocumentsArray); + const schema = analyzer.getSchema(); + expect(schema).toBeDefined(); + + // Check that 'x-documentsInspected' is correct + expect(schema['x-documentsInspected']).toBe(sparseDocumentsArray.length); + + // Check that the schema has the correct root properties + const expectedRootProperties = new Set(['_id', 'name', 'age', 'email', 'isActive', 'score', 'description']); + + expect(Object.keys(schema.properties || {})).toEqual( + expect.arrayContaining(Array.from(expectedRootProperties)), + ); + + // Check that the 'name' field is detected correctly + const nameField = schema.properties?.['name'] as JSONSchema; + expect(nameField).toBeDefined(); + expect(nameField?.['x-occurrence']).toBeGreaterThan(0); + + // Access 'anyOf' to get the type entries + const nameFieldTypes = nameField.anyOf?.map((typeEntry) => (typeEntry as JSONSchema)['type']); + expect(nameFieldTypes).toContain('string'); + + // Check that the 'age' field has the correct type + const ageField = schema.properties?.['age'] as JSONSchema; + expect(ageField).toBeDefined(); + const ageFieldTypes = ageField.anyOf?.map((typeEntry) => (typeEntry as JSONSchema)['type']); + expect(ageFieldTypes).toContain('number'); + + // Check that the 'isActive' field is a boolean + const isActiveField = schema.properties?.['isActive'] as JSONSchema; + expect(isActiveField).toBeDefined(); + const isActiveTypes = isActiveField.anyOf?.map((typeEntry) => (typeEntry as JSONSchema)['type']); + expect(isActiveTypes).toContain('boolean'); + + // Check that the 'description' field is optional (occurs in some documents) + const descriptionField = schema.properties?.['description'] as JSONSchema | undefined; + expect(descriptionField).toBeDefined(); + expect(descriptionField?.['x-occurrence']).toBeLessThan(sparseDocumentsArray.length); + }); + + it('detects all BSON types from flatDocument', () => { + const analyzer = SchemaAnalyzer.fromDocument(flatDocument); + const schema = analyzer.getSchema(); + + // Check that all fields are detected + const expectedFields = Object.keys(flatDocument); + expect(Object.keys(schema.properties || {})).toEqual(expect.arrayContaining(expectedFields)); + + // Helper function to get the 'x-bsonType' from a field + function getBsonType(fieldName: string): string | undefined { + const field = schema.properties?.[fieldName] as JSONSchema | undefined; + const anyOf = field?.anyOf; + return anyOf && (anyOf[0] as JSONSchema | undefined)?.['x-bsonType']; + } + + // Check that specific BSON types are correctly identified + expect(getBsonType('int32Field')).toBe('int32'); + expect(getBsonType('doubleField')).toBe('double'); + expect(getBsonType('decimalField')).toBe('decimal128'); + expect(getBsonType('dateField')).toBe('date'); + expect(getBsonType('objectIdField')).toBe('objectid'); + expect(getBsonType('codeField')).toBe('code'); + expect(getBsonType('uuidField')).toBe('uuid'); + expect(getBsonType('uuidLegacyField')).toBe('uuid-legacy'); + }); + + it('detects embedded objects correctly', () => { + const analyzer = SchemaAnalyzer.fromDocument(embeddedDocumentOnly); + const schema = analyzer.getSchema(); + + // Check that the root properties are detected + expect(schema.properties).toHaveProperty('personalInfo'); + expect(schema.properties).toHaveProperty('jobInfo'); + + // Access 'personalInfo' properties + const personalInfoAnyOf = + schema.properties && (schema.properties['personalInfo'] as JSONSchema | undefined)?.anyOf; + const personalInfoProperties = (personalInfoAnyOf?.[0] as JSONSchema | undefined)?.properties; + expect(personalInfoProperties).toBeDefined(); + expect(personalInfoProperties).toHaveProperty('name'); + expect(personalInfoProperties).toHaveProperty('age'); + expect(personalInfoProperties).toHaveProperty('married'); + expect(personalInfoProperties).toHaveProperty('address'); + + // Access 'address' properties within 'personalInfo' + const addressAnyOf = ((personalInfoProperties as JSONSchemaMap)['address'] as JSONSchema).anyOf; + const addressProperties = (addressAnyOf?.[0] as JSONSchema | undefined)?.properties; + expect(addressProperties).toBeDefined(); + expect(addressProperties).toHaveProperty('street'); + expect(addressProperties).toHaveProperty('city'); + expect(addressProperties).toHaveProperty('zip'); + }); + + it('detects arrays and their element types correctly', () => { + const analyzer = SchemaAnalyzer.fromDocument(arraysWithDifferentDataTypes); + const schema = analyzer.getSchema(); + + // Check that arrays are detected + expect(schema.properties).toHaveProperty('integersArray'); + expect(schema.properties).toHaveProperty('stringsArray'); + expect(schema.properties).toHaveProperty('booleansArray'); + expect(schema.properties).toHaveProperty('mixedArray'); + expect(schema.properties).toHaveProperty('datesArray'); + + // Helper function to get item types from an array field + function getArrayItemTypes(fieldName: string): string[] | undefined { + const field = schema.properties?.[fieldName] as JSONSchema | undefined; + const anyOf = field?.anyOf; + const itemsAnyOf: JSONSchemaRef[] | undefined = ( + (anyOf?.[0] as JSONSchema | undefined)?.items as JSONSchema | undefined + )?.anyOf; + return itemsAnyOf?.map((typeEntry) => (typeEntry as JSONSchema)['type'] as string); + } + + // Check that 'integersArray' has elements of type 'number' + const integerItemTypes = getArrayItemTypes('integersArray'); + expect(integerItemTypes).toContain('number'); + + // Check that 'stringsArray' has elements of type 'string' + const stringItemTypes = getArrayItemTypes('stringsArray'); + expect(stringItemTypes).toContain('string'); + + // Check that 'mixedArray' contains multiple types + const mixedItemTypes = getArrayItemTypes('mixedArray'); + expect(mixedItemTypes).toEqual(expect.arrayContaining(['number', 'string', 'boolean', 'object', 'null'])); + }); + + it('handles arrays within objects and objects within arrays', () => { + const analyzer = SchemaAnalyzer.fromDocument(complexDocument); + const schema = analyzer.getSchema(); + + // Access 'user.profile.hobbies' + const user = schema.properties?.['user'] as JSONSchema | undefined; + const userProfile = (user?.anyOf?.[0] as JSONSchema | undefined)?.properties?.['profile'] as + | JSONSchema + | undefined; + const hobbies = (userProfile?.anyOf?.[0] as JSONSchema | undefined)?.properties?.['hobbies'] as + | JSONSchema + | undefined; + const hobbiesItems = (hobbies?.anyOf?.[0] as JSONSchema | undefined)?.items as JSONSchema | undefined; + const hobbiesItemTypes = hobbiesItems?.anyOf?.map((typeEntry) => (typeEntry as JSONSchema).type); + expect(hobbiesItemTypes).toContain('string'); + + // Access 'user.profile.addresses' + const addresses = (userProfile?.anyOf?.[0] as JSONSchema | undefined)?.properties?.['addresses'] as + | JSONSchema + | undefined; + const addressesItems = (addresses?.anyOf?.[0] as JSONSchema | undefined)?.items as JSONSchema | undefined; + const addressItemTypes = addressesItems?.anyOf?.map((typeEntry) => (typeEntry as JSONSchema).type); + expect(addressItemTypes).toContain('object'); + + // Check that 'orders' is an array + const orders = schema.properties?.['orders'] as JSONSchema | undefined; + expect(orders).toBeDefined(); + const ordersType = (orders?.anyOf?.[0] as JSONSchema | undefined)?.type; + expect(ordersType).toBe('array'); + + // Access 'items' within 'orders' + const orderItemsParent = (orders?.anyOf?.[0] as JSONSchema | undefined)?.items as JSONSchema | undefined; + const orderItems = (orderItemsParent?.anyOf?.[0] as JSONSchema | undefined)?.properties?.['items'] as + | JSONSchema + | undefined; + const orderItemsType = (orderItems?.anyOf?.[0] as JSONSchema | undefined)?.type; + expect(orderItemsType).toBe('array'); + }); + + it('updates schema correctly when processing multiple documents', () => { + const analyzer = SchemaAnalyzer.fromDocuments(complexDocumentsArray); + const schema = analyzer.getSchema(); + + // Check that 'x-documentsInspected' is correct + expect(schema['x-documentsInspected']).toBe(complexDocumentsArray.length); + + // Check that some fields are present from different documents + expect(schema.properties).toHaveProperty('stringField'); + expect(schema.properties).toHaveProperty('personalInfo'); + expect(schema.properties).toHaveProperty('integersArray'); + expect(schema.properties).toHaveProperty('user'); + + // Check that 'integersArray' has correct min and max values + const integersArray = schema.properties?.['integersArray'] as JSONSchema | undefined; + const integerItemType = ((integersArray?.anyOf?.[0] as JSONSchema | undefined)?.items as JSONSchema | undefined) + ?.anyOf?.[0] as JSONSchema | undefined; + expect(integerItemType?.['x-minValue']).toBe(1); + expect(integerItemType?.['x-maxValue']).toBe(5); + + // Check that 'orders.items.price' is detected as Decimal128 + const orders2 = schema.properties?.['orders'] as JSONSchema | undefined; + const orderItemsParent2 = (orders2?.anyOf?.[0] as JSONSchema | undefined)?.items as JSONSchema | undefined; + const orderItems = (orderItemsParent2?.anyOf?.[0] as JSONSchema | undefined)?.properties?.['items'] as + | JSONSchema + | undefined; + const priceFieldParent = ((orderItems?.anyOf?.[0] as JSONSchema | undefined)?.items as JSONSchema | undefined) + ?.anyOf?.[0] as JSONSchema | undefined; + const priceField = priceFieldParent?.properties?.['price'] as JSONSchema | undefined; + const priceFieldType = priceField?.anyOf?.[0] as JSONSchema | undefined; + expect(priceFieldType?.['x-bsonType']).toBe('decimal128'); + }); + + describe('traverses schema', () => { + it('with valid paths', () => { + const analyzer = SchemaAnalyzer.fromDocument(complexDocument); + const schema = analyzer.getSchema(); + + let propertiesAtRoot = getPropertyNamesAtLevel(schema, []); + expect(propertiesAtRoot).toHaveLength(4); + + propertiesAtRoot = getPropertyNamesAtLevel(schema, ['user']); + expect(propertiesAtRoot).toHaveLength(3); + + propertiesAtRoot = getPropertyNamesAtLevel(schema, ['user', 'profile']); + expect(propertiesAtRoot).toHaveLength(4); + }); + + it('with broken paths', () => { + const analyzer = SchemaAnalyzer.fromDocument(complexDocument); + const schema = analyzer.getSchema(); + + const propertiesAtRoot = getPropertyNamesAtLevel(schema, []); + expect(propertiesAtRoot).toHaveLength(4); + + expect(() => getPropertyNamesAtLevel(schema, ['no-entry'])).toThrow(); + + expect(() => getPropertyNamesAtLevel(schema, ['user', 'no-entry'])).toThrow(); + }); + + it('with sparse docs and mixed types', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(complexDocument); + analyzer.addDocument(complexDocumentWithOddTypes); + const schema = analyzer.getSchema(); + + let propertiesAtRoot = getPropertyNamesAtLevel(schema, []); + expect(propertiesAtRoot).toHaveLength(4); + + propertiesAtRoot = getPropertyNamesAtLevel(schema, ['user']); + expect(propertiesAtRoot).toHaveLength(3); + expect(propertiesAtRoot).toEqual(['email', 'profile', 'username']); + + propertiesAtRoot = getPropertyNamesAtLevel(schema, ['user', 'profile']); + expect(propertiesAtRoot).toHaveLength(4); + expect(propertiesAtRoot).toEqual(['addresses', 'firstName', 'hobbies', 'lastName']); + + propertiesAtRoot = getPropertyNamesAtLevel(schema, ['history']); + expect(propertiesAtRoot).toHaveLength(6); + }); + }); + + describe('SchemaAnalyzer class methods', () => { + it('clone() creates an independent deep copy', () => { + // Use embeddedDocumentOnly (plain JS types) to avoid structuredClone issues with BSON types + const original = SchemaAnalyzer.fromDocument(embeddedDocumentOnly); + const cloned = original.clone(); + + // Clone has the same document count + expect(cloned.getDocumentCount()).toBe(1); + + // Clone has the same properties + const originalProps = Object.keys(original.getSchema().properties || {}); + const clonedProps = Object.keys(cloned.getSchema().properties || {}); + expect(clonedProps).toEqual(originalProps); + + // Add another document to the original only + original.addDocument(arraysWithDifferentDataTypes); + expect(original.getDocumentCount()).toBe(2); + expect(cloned.getDocumentCount()).toBe(1); + + // Clone's schema was NOT affected by the mutation + const originalPropsAfter = Object.keys(original.getSchema().properties || {}); + const clonedPropsAfter = Object.keys(cloned.getSchema().properties || {}); + expect(originalPropsAfter).toContain('integersArray'); + expect(originalPropsAfter).toContain('stringsArray'); + expect(clonedPropsAfter).not.toContain('integersArray'); + expect(clonedPropsAfter).not.toContain('stringsArray'); + }); + + it('reset() clears all accumulated state', () => { + const analyzer = SchemaAnalyzer.fromDocument(flatDocument); + expect(analyzer.getDocumentCount()).toBeGreaterThan(0); + expect(Object.keys(analyzer.getSchema().properties || {})).not.toHaveLength(0); + + analyzer.reset(); + + expect(analyzer.getDocumentCount()).toBe(0); + const schema = analyzer.getSchema(); + expect(schema.properties).toBeUndefined(); + expect(schema['x-documentsInspected']).toBeUndefined(); + }); + + it('fromDocument() creates analyzer with single document', () => { + const analyzer = SchemaAnalyzer.fromDocument(flatDocument); + expect(analyzer.getDocumentCount()).toBe(1); + + const schema = analyzer.getSchema(); + const expectedFields = Object.keys(flatDocument); + expect(Object.keys(schema.properties || {})).toEqual(expect.arrayContaining(expectedFields)); + }); + + it('fromDocuments() creates analyzer with multiple documents', () => { + const analyzer = SchemaAnalyzer.fromDocuments(sparseDocumentsArray); + expect(analyzer.getDocumentCount()).toBe(sparseDocumentsArray.length); + + // Compare with manually-built analyzer + const manual = new SchemaAnalyzer(); + manual.addDocuments(sparseDocumentsArray); + + expect(JSON.stringify(analyzer.getSchema())).toBe(JSON.stringify(manual.getSchema())); + }); + + it('addDocuments() is equivalent to multiple addDocument() calls', () => { + const batch = new SchemaAnalyzer(); + batch.addDocuments(complexDocumentsArray); + + const sequential = new SchemaAnalyzer(); + for (const doc of complexDocumentsArray) { + sequential.addDocument(doc); + } + + expect(batch.getDocumentCount()).toBe(sequential.getDocumentCount()); + expect(JSON.stringify(batch.getSchema())).toBe(JSON.stringify(sequential.getSchema())); + }); + }); +}); diff --git a/packages/schema-analyzer/test/SchemaAnalyzer.versioning.test.ts b/packages/schema-analyzer/test/SchemaAnalyzer.versioning.test.ts new file mode 100644 index 000000000..38ef144a6 --- /dev/null +++ b/packages/schema-analyzer/test/SchemaAnalyzer.versioning.test.ts @@ -0,0 +1,663 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ObjectId, type Document, type WithId } from 'mongodb'; +import { type JSONSchema } from '../src/JSONSchema'; +import { SchemaAnalyzer } from '../src/SchemaAnalyzer'; + +// ------------------------------------------------------------------ +// Test fixtures +// ------------------------------------------------------------------ + +function makeDoc(fields: Record = {}): WithId { + return { _id: new ObjectId(), ...fields }; +} + +// ------------------------------------------------------------------ +// Version counter +// ------------------------------------------------------------------ +describe('SchemaAnalyzer version counter', () => { + it('starts at 0 for a new analyzer', () => { + const analyzer = new SchemaAnalyzer(); + expect(analyzer.version).toBe(0); + }); + + it('increments on addDocument()', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ a: 1 })); + expect(analyzer.version).toBe(1); + + analyzer.addDocument(makeDoc({ b: 2 })); + expect(analyzer.version).toBe(2); + }); + + it('increments only once for addDocuments() (batch)', () => { + const analyzer = new SchemaAnalyzer(); + const docs = [makeDoc({ a: 1 }), makeDoc({ b: 2 }), makeDoc({ c: 3 })]; + + analyzer.addDocuments(docs); + expect(analyzer.version).toBe(1); + }); + + it('increments on reset()', () => { + const analyzer = SchemaAnalyzer.fromDocument(makeDoc({ x: 1 })); + expect(analyzer.version).toBe(1); + + analyzer.reset(); + expect(analyzer.version).toBe(2); + }); + + it('cloned analyzer starts with version 0 (independent from original)', () => { + const original = new SchemaAnalyzer(); + original.addDocument(makeDoc({ a: 1 })); + original.addDocument(makeDoc({ b: 2 })); + expect(original.version).toBe(2); + + const cloned = original.clone(); + expect(cloned.version).toBe(0); + + // Mutating the clone does not affect the original's version + cloned.addDocument(makeDoc({ c: 3 })); + expect(cloned.version).toBe(1); + expect(original.version).toBe(2); + }); + + it('accumulates across mixed operations', () => { + const analyzer = new SchemaAnalyzer(); + // addDocument +1 + analyzer.addDocument(makeDoc()); + expect(analyzer.version).toBe(1); + + // addDocuments +1 (batch) + analyzer.addDocuments([makeDoc(), makeDoc()]); + expect(analyzer.version).toBe(2); + + // reset +1 + analyzer.reset(); + expect(analyzer.version).toBe(3); + + // addDocument after reset +1 + analyzer.addDocument(makeDoc()); + expect(analyzer.version).toBe(4); + }); + + it('fromDocument() factory yields version 1', () => { + const analyzer = SchemaAnalyzer.fromDocument(makeDoc({ a: 1 })); + expect(analyzer.version).toBe(1); + }); + + it('fromDocuments() factory yields version 1', () => { + const analyzer = SchemaAnalyzer.fromDocuments([makeDoc(), makeDoc(), makeDoc()]); + expect(analyzer.version).toBe(1); + }); +}); + +// ------------------------------------------------------------------ +// Version-based caching (getKnownFields cache) +// ------------------------------------------------------------------ +describe('SchemaAnalyzer getKnownFields cache', () => { + it('is populated on first call to getKnownFields()', () => { + const analyzer = SchemaAnalyzer.fromDocument(makeDoc({ name: 'Alice', age: 30 })); + const fields = analyzer.getKnownFields(); + + expect(fields.length).toBeGreaterThan(0); + // Should contain _id, age, name + const paths = fields.map((f) => f.path); + expect(paths).toContain('_id'); + expect(paths).toContain('name'); + expect(paths).toContain('age'); + }); + + it('is reused when version has not changed (same reference)', () => { + const analyzer = SchemaAnalyzer.fromDocument(makeDoc({ name: 'Alice' })); + const first = analyzer.getKnownFields(); + const second = analyzer.getKnownFields(); + + // Same array reference โ€” cache was reused, not recomputed + expect(second).toBe(first); + }); + + it('is invalidated when addDocument() is called', () => { + const analyzer = SchemaAnalyzer.fromDocument(makeDoc({ name: 'Alice' })); + const before = analyzer.getKnownFields(); + + analyzer.addDocument(makeDoc({ name: 'Bob', email: 'bob@test.com' })); + const after = analyzer.getKnownFields(); + + // Different reference โ€” cache was recomputed + expect(after).not.toBe(before); + // New field should be present + expect(after.map((f) => f.path)).toContain('email'); + }); + + it('is invalidated when addDocuments() is called', () => { + const analyzer = SchemaAnalyzer.fromDocument(makeDoc({ name: 'Alice' })); + const before = analyzer.getKnownFields(); + + analyzer.addDocuments([makeDoc({ score: 42 }), makeDoc({ level: 7 })]); + const after = analyzer.getKnownFields(); + + expect(after).not.toBe(before); + const paths = after.map((f) => f.path); + expect(paths).toContain('score'); + expect(paths).toContain('level'); + }); + + it('is invalidated when reset() is called', () => { + const analyzer = SchemaAnalyzer.fromDocument(makeDoc({ name: 'Alice' })); + const before = analyzer.getKnownFields(); + expect(before.length).toBeGreaterThan(0); + + analyzer.reset(); + const after = analyzer.getKnownFields(); + + expect(after).not.toBe(before); + // After reset the schema is empty so no fields + expect(after).toHaveLength(0); + }); + + it('returns updated results after cache invalidation', () => { + const analyzer = new SchemaAnalyzer(); + // Empty analyzer โ†’ no known fields + expect(analyzer.getKnownFields()).toHaveLength(0); + + // Add first doc + analyzer.addDocument(makeDoc({ x: 1 })); + const fields1 = analyzer.getKnownFields(); + expect(fields1.map((f) => f.path)).toEqual(expect.arrayContaining(['_id', 'x'])); + + // Add second doc with new field + analyzer.addDocument(makeDoc({ x: 2, y: 'hello' })); + const fields2 = analyzer.getKnownFields(); + expect(fields2).not.toBe(fields1); + expect(fields2.map((f) => f.path)).toContain('y'); + }); + + it('clone gets its own independent cache', () => { + const original = SchemaAnalyzer.fromDocument(makeDoc({ name: 'Alice' })); + const originalFields = original.getKnownFields(); + + const cloned = original.clone(); + const clonedFields = cloned.getKnownFields(); + + // Both should have the same content but be independent objects + expect(clonedFields).not.toBe(originalFields); + expect(clonedFields.map((f) => f.path)).toEqual(originalFields.map((f) => f.path)); + + // Mutating the clone should not affect the original cache + cloned.addDocument(makeDoc({ extra: true })); + const clonedFieldsAfter = cloned.getKnownFields(); + expect(clonedFieldsAfter.map((f) => f.path)).toContain('extra'); + expect(original.getKnownFields().map((f) => f.path)).not.toContain('extra'); + }); +}); + +// ------------------------------------------------------------------ +// Instances and types counting +// ------------------------------------------------------------------ +describe('SchemaAnalyzer instances and types counting', () => { + describe('x-occurrence (field instance counting)', () => { + it('counts 1 for a field present in a single document', () => { + const analyzer = SchemaAnalyzer.fromDocument(makeDoc({ name: 'Alice' })); + const schema = analyzer.getSchema(); + const nameField = schema.properties?.['name'] as JSONSchema; + expect(nameField['x-occurrence']).toBe(1); + }); + + it('counts correctly across multiple documents', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ name: 'Alice', age: 30 })); + analyzer.addDocument(makeDoc({ name: 'Bob', age: 25 })); + analyzer.addDocument(makeDoc({ name: 'Carol' })); // no age + + const schema = analyzer.getSchema(); + expect((schema.properties?.['name'] as JSONSchema)['x-occurrence']).toBe(3); + expect((schema.properties?.['age'] as JSONSchema)['x-occurrence']).toBe(2); + }); + + it('counts sparse fields correctly (field missing in some documents)', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ a: 1, b: 2, c: 3 })); + analyzer.addDocument(makeDoc({ a: 10 })); // only 'a' + analyzer.addDocument(makeDoc({ a: 100, c: 300 })); // 'a' and 'c' + + const schema = analyzer.getSchema(); + expect((schema.properties?.['a'] as JSONSchema)['x-occurrence']).toBe(3); + expect((schema.properties?.['b'] as JSONSchema)['x-occurrence']).toBe(1); + expect((schema.properties?.['c'] as JSONSchema)['x-occurrence']).toBe(2); + }); + + it('counts occurrences for nested object properties', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ user: { name: 'Alice', age: 30 } })); + analyzer.addDocument(makeDoc({ user: { name: 'Bob' } })); // no age + + const schema = analyzer.getSchema(); + const userField = schema.properties?.['user'] as JSONSchema; + const objectEntry = userField.anyOf?.find((e) => (e as JSONSchema).type === 'object') as JSONSchema; + + expect((objectEntry.properties?.['name'] as JSONSchema)['x-occurrence']).toBe(2); + expect((objectEntry.properties?.['age'] as JSONSchema)['x-occurrence']).toBe(1); + }); + }); + + describe('x-typeOccurrence (type counting)', () => { + it('counts type occurrences for a single-type field', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ name: 'Alice' })); + analyzer.addDocument(makeDoc({ name: 'Bob' })); + analyzer.addDocument(makeDoc({ name: 'Carol' })); + + const schema = analyzer.getSchema(); + const nameField = schema.properties?.['name'] as JSONSchema; + const stringEntry = nameField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'string', + ) as JSONSchema; + + expect(stringEntry['x-typeOccurrence']).toBe(3); + }); + + it('counts type occurrences for polymorphic fields', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ value: 'hello' })); + analyzer.addDocument(makeDoc({ value: 42 })); + analyzer.addDocument(makeDoc({ value: 'world' })); + analyzer.addDocument(makeDoc({ value: true })); + + const schema = analyzer.getSchema(); + const valueField = schema.properties?.['value'] as JSONSchema; + + const stringEntry = valueField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'string', + ) as JSONSchema; + const booleanEntry = valueField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'boolean', + ) as JSONSchema; + + // 2 strings, 1 number, 1 boolean + expect(stringEntry['x-typeOccurrence']).toBe(2); + expect(booleanEntry['x-typeOccurrence']).toBe(1); + + // total x-occurrence should equal sum of x-typeOccurrence values + const totalTypeOccurrence = (valueField.anyOf as JSONSchema[]).reduce( + (sum, entry) => sum + ((entry['x-typeOccurrence'] as number) ?? 0), + 0, + ); + expect(valueField['x-occurrence']).toBe(totalTypeOccurrence); + }); + + it('counts array element types across documents', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ tags: ['a', 'b'] })); // 2 strings + analyzer.addDocument(makeDoc({ tags: ['c', 42] })); // 1 string + 1 number + analyzer.addDocument(makeDoc({ tags: [true] })); // 1 boolean + + const schema = analyzer.getSchema(); + const tagsField = schema.properties?.['tags'] as JSONSchema; + const arrayEntry = tagsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + const itemsSchema = arrayEntry.items as JSONSchema; + + const stringEntry = itemsSchema.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'string', + ) as JSONSchema; + const booleanEntry = itemsSchema.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'boolean', + ) as JSONSchema; + + // 3 string elements total: "a", "b", "c" + expect(stringEntry['x-typeOccurrence']).toBe(3); + + // 1 boolean element + expect(booleanEntry['x-typeOccurrence']).toBe(1); + }); + + it('type occurrence count equals field occurrence for a single-type field', () => { + const analyzer = new SchemaAnalyzer(); + for (let i = 0; i < 5; i++) { + analyzer.addDocument(makeDoc({ score: i * 10 })); + } + + const schema = analyzer.getSchema(); + const scoreField = schema.properties?.['score'] as JSONSchema; + const typeEntries = scoreField.anyOf as JSONSchema[]; + + // Only one type, so its typeOccurrence should equal the field occurrence + expect(typeEntries).toHaveLength(1); + expect(typeEntries[0]['x-typeOccurrence']).toBe(scoreField['x-occurrence']); + }); + }); + + describe('x-documentsInspected counting', () => { + it('tracks document count at root level', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ a: 1 })); + analyzer.addDocument(makeDoc({ b: 2 })); + analyzer.addDocument(makeDoc({ c: 3 })); + + expect(analyzer.getSchema()['x-documentsInspected']).toBe(3); + expect(analyzer.getDocumentCount()).toBe(3); + }); + + it('tracks object instances for nested objects', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ info: { x: 1 } })); + analyzer.addDocument(makeDoc({ info: { x: 2, y: 3 } })); + + const schema = analyzer.getSchema(); + const infoField = schema.properties?.['info'] as JSONSchema; + const objectEntry = infoField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + expect(objectEntry['x-documentsInspected']).toBe(2); + }); + + it('tracks object instances inside arrays accurately', () => { + const analyzer = new SchemaAnalyzer(); + // doc1: array with 2 objects + analyzer.addDocument(makeDoc({ items: [{ a: 1 }, { a: 2 }] })); + // doc2: array with 1 object + analyzer.addDocument(makeDoc({ items: [{ a: 3, b: 4 }] })); + + const schema = analyzer.getSchema(); + const itemsField = schema.properties?.['items'] as JSONSchema; + const arrayEntry = itemsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + const objectEntry = (arrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + // 3 objects total (2 from doc1, 1 from doc2) + expect(objectEntry['x-documentsInspected']).toBe(3); + // "a" appears in all 3 objects + expect((objectEntry.properties?.['a'] as JSONSchema)['x-occurrence']).toBe(3); + // "b" appears in 1 of 3 objects + expect((objectEntry.properties?.['b'] as JSONSchema)['x-occurrence']).toBe(1); + }); + + it('resets to 0 after reset()', () => { + const analyzer = SchemaAnalyzer.fromDocuments([makeDoc({ a: 1 }), makeDoc({ b: 2 })]); + expect(analyzer.getDocumentCount()).toBe(2); + + analyzer.reset(); + expect(analyzer.getDocumentCount()).toBe(0); + }); + }); + + describe('probability correctness (occurrence / documentsInspected)', () => { + it('yields 100% for fields present in every document', () => { + const analyzer = new SchemaAnalyzer(); + for (let i = 0; i < 10; i++) { + analyzer.addDocument(makeDoc({ name: `user-${i}` })); + } + + const schema = analyzer.getSchema(); + const occurrence = (schema.properties?.['name'] as JSONSchema)['x-occurrence'] as number; + const total = schema['x-documentsInspected'] as number; + expect(occurrence / total).toBe(1); + }); + + it('yields correct fraction for sparse fields', () => { + const analyzer = new SchemaAnalyzer(); + // 3 docs with 'a', 1 doc with 'b' + analyzer.addDocument(makeDoc({ a: 1, b: 10 })); + analyzer.addDocument(makeDoc({ a: 2 })); + analyzer.addDocument(makeDoc({ a: 3 })); + + const schema = analyzer.getSchema(); + const total = schema['x-documentsInspected'] as number; + const aOccurrence = (schema.properties?.['a'] as JSONSchema)['x-occurrence'] as number; + const bOccurrence = (schema.properties?.['b'] as JSONSchema)['x-occurrence'] as number; + + expect(aOccurrence / total).toBe(1); // 3/3 + expect(bOccurrence / total).toBeCloseTo(1 / 3); // 1/3 + }); + + it('yields correct fraction for nested objects inside arrays', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument( + makeDoc({ + items: [ + { name: 'A', price: 10 }, + { name: 'B' }, // no price + ], + }), + ); + analyzer.addDocument(makeDoc({ items: [{ name: 'C', price: 20 }] })); + + const schema = analyzer.getSchema(); + const itemsField = schema.properties?.['items'] as JSONSchema; + const arrayEntry = itemsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + const objectEntry = (arrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + const denominator = objectEntry['x-documentsInspected'] as number; + const nameOccurrence = (objectEntry.properties?.['name'] as JSONSchema)['x-occurrence'] as number; + const priceOccurrence = (objectEntry.properties?.['price'] as JSONSchema)['x-occurrence'] as number; + + expect(denominator).toBe(3); // 3 objects total + expect(nameOccurrence / denominator).toBe(1); // 3/3 + expect(priceOccurrence / denominator).toBeCloseTo(2 / 3); // 2/3 + }); + }); + + describe('array and nested array counting', () => { + it('counts x-typeOccurrence for the array type entry across documents', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ tags: ['a'] })); + analyzer.addDocument(makeDoc({ tags: ['b', 'c'] })); + analyzer.addDocument(makeDoc({ tags: 42 })); // not an array + + const schema = analyzer.getSchema(); + const tagsField = schema.properties?.['tags'] as JSONSchema; + + // Field seen 3 times total + expect(tagsField['x-occurrence']).toBe(3); + + const arrayEntry = tagsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + + // Array type seen 2 out of 3 times + expect(arrayEntry['x-typeOccurrence']).toBe(2); + + // x-minItems / x-maxItems tracked across array instances + expect(arrayEntry['x-minItems']).toBe(1); + expect(arrayEntry['x-maxItems']).toBe(2); + }); + + it('counts x-minItems / x-maxItems for arrays across documents', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ nums: [1, 2, 3] })); // length 3 + analyzer.addDocument(makeDoc({ nums: [10] })); // length 1 + analyzer.addDocument(makeDoc({ nums: [4, 5, 6, 7, 8] })); // length 5 + + const schema = analyzer.getSchema(); + const numsField = schema.properties?.['nums'] as JSONSchema; + const arrayEntry = numsField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + + expect(arrayEntry['x-minItems']).toBe(1); + expect(arrayEntry['x-maxItems']).toBe(5); + expect(arrayEntry['x-typeOccurrence']).toBe(3); + }); + + it('counts nested arrays (arrays within arrays)', () => { + const analyzer = new SchemaAnalyzer(); + // matrix is an array of arrays of numbers + analyzer.addDocument( + makeDoc({ + matrix: [ + [1, 2], + [3, 4, 5], + ], + }), + ); + analyzer.addDocument(makeDoc({ matrix: [[10]] })); + + const schema = analyzer.getSchema(); + const matrixField = schema.properties?.['matrix'] as JSONSchema; + const outerArrayEntry = matrixField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'array', + ) as JSONSchema; + + // Outer array seen in 2 documents + expect(outerArrayEntry['x-typeOccurrence']).toBe(2); + // doc1 has 2 inner arrays, doc2 has 1 + expect(outerArrayEntry['x-minItems']).toBe(1); + expect(outerArrayEntry['x-maxItems']).toBe(2); + + // Inner arrays: items type should be 'array' + const innerArrayEntry = (outerArrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'array', + ) as JSONSchema; + expect(innerArrayEntry).toBeDefined(); + // 3 inner arrays total: [1,2], [3,4,5], [10] + expect(innerArrayEntry['x-typeOccurrence']).toBe(3); + // inner array lengths: 2, 3, 1 + expect(innerArrayEntry['x-minItems']).toBe(1); + expect(innerArrayEntry['x-maxItems']).toBe(3); + + // Elements inside inner arrays are numbers + const numberEntry = (innerArrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema).type === 'number', + ) as JSONSchema; + expect(numberEntry).toBeDefined(); + // 6 numbers total: 1,2,3,4,5,10 + expect(numberEntry['x-typeOccurrence']).toBe(6); + }); + + it('counts objects within arrays within objects (deep nesting)', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument( + makeDoc({ + company: { + departments: [ + { name: 'Eng', employees: [{ role: 'Dev' }, { role: 'QA', level: 3 }] }, + { name: 'Sales' }, + ], + }, + }), + ); + analyzer.addDocument( + makeDoc({ + company: { + departments: [{ name: 'HR', employees: [{ role: 'Recruiter' }] }], + }, + }), + ); + + const schema = analyzer.getSchema(); + + // company is an object + const companyField = schema.properties?.['company'] as JSONSchema; + const companyObj = companyField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + expect(companyObj['x-documentsInspected']).toBe(2); + + // departments is an array inside company + const deptField = companyObj.properties?.['departments'] as JSONSchema; + const deptArrayEntry = deptField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'array', + ) as JSONSchema; + expect(deptArrayEntry['x-typeOccurrence']).toBe(2); + + // department objects: 2 from doc1 + 1 from doc2 = 3 + const deptObjEntry = (deptArrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + expect(deptObjEntry['x-documentsInspected']).toBe(3); + expect(deptObjEntry['x-typeOccurrence']).toBe(3); + + // "name" in all 3 department objects, "employees" in 2 of 3 + expect((deptObjEntry.properties?.['name'] as JSONSchema)['x-occurrence']).toBe(3); + expect((deptObjEntry.properties?.['employees'] as JSONSchema)['x-occurrence']).toBe(2); + + // employees is an array inside department objects + const empField = deptObjEntry.properties?.['employees'] as JSONSchema; + const empArrayEntry = empField.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'array', + ) as JSONSchema; + expect(empArrayEntry['x-typeOccurrence']).toBe(2); + + // employee objects: 2 from first dept + 1 from HR = 3 + const empObjEntry = (empArrayEntry.items as JSONSchema).anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + expect(empObjEntry['x-documentsInspected']).toBe(3); + + // "role" in all 3 employee objects, "level" in 1 + expect((empObjEntry.properties?.['role'] as JSONSchema)['x-occurrence']).toBe(3); + expect((empObjEntry.properties?.['level'] as JSONSchema)['x-occurrence']).toBe(1); + }); + + it('tracks mixed types inside arrays (objects + primitives)', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument(makeDoc({ data: ['hello', { key: 'val' }, 42] })); + analyzer.addDocument(makeDoc({ data: [{ key: 'v2', extra: true }] })); + + const schema = analyzer.getSchema(); + const dataField = schema.properties?.['data'] as JSONSchema; + const arrayEntry = dataField.anyOf?.find((e) => (e as JSONSchema)['x-bsonType'] === 'array') as JSONSchema; + const itemsSchema = arrayEntry.items as JSONSchema; + + // string: 1, object: 2, number: 1 + const stringEntry = itemsSchema.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'string', + ) as JSONSchema; + const objectEntry = itemsSchema.anyOf?.find( + (e) => (e as JSONSchema)['x-bsonType'] === 'object', + ) as JSONSchema; + + expect(stringEntry['x-typeOccurrence']).toBe(1); + expect(objectEntry['x-typeOccurrence']).toBe(2); + expect(objectEntry['x-documentsInspected']).toBe(2); + + // "key" in both objects, "extra" in 1 + expect((objectEntry.properties?.['key'] as JSONSchema)['x-occurrence']).toBe(2); + expect((objectEntry.properties?.['extra'] as JSONSchema)['x-occurrence']).toBe(1); + }); + }); + + describe('addDocuments vs sequential addDocument equivalence', () => { + it('produces identical occurrence counts', () => { + const docs = [makeDoc({ a: 1, b: 'x' }), makeDoc({ a: 2 }), makeDoc({ a: 3, c: true })]; + + const batch = new SchemaAnalyzer(); + batch.addDocuments(docs); + + const sequential = new SchemaAnalyzer(); + for (const doc of docs) { + sequential.addDocument(doc); + } + + const batchSchema = batch.getSchema(); + const seqSchema = sequential.getSchema(); + + // Root counts match + expect(batchSchema['x-documentsInspected']).toBe(seqSchema['x-documentsInspected']); + + // Field-level occurrence counts match + for (const key of Object.keys(batchSchema.properties ?? {})) { + const batchField = batchSchema.properties?.[key] as JSONSchema; + const seqField = seqSchema.properties?.[key] as JSONSchema; + expect(batchField['x-occurrence']).toBe(seqField['x-occurrence']); + } + }); + + it('produces identical type occurrence counts', () => { + const docs = [makeDoc({ value: 'hello' }), makeDoc({ value: 42 }), makeDoc({ value: 'world' })]; + + const batch = new SchemaAnalyzer(); + batch.addDocuments(docs); + + const sequential = new SchemaAnalyzer(); + for (const doc of docs) { + sequential.addDocument(doc); + } + + // Stringify the schemas to compare their full type entry structures + expect(JSON.stringify(batch.getSchema())).toBe(JSON.stringify(sequential.getSchema())); + }); + }); +}); diff --git a/src/utils/json/mongo/mongoTestDocuments.ts b/packages/schema-analyzer/test/mongoTestDocuments.ts similarity index 100% rename from src/utils/json/mongo/mongoTestDocuments.ts rename to packages/schema-analyzer/test/mongoTestDocuments.ts diff --git a/packages/schema-analyzer/tsconfig.json b/packages/schema-analyzer/tsconfig.json new file mode 100644 index 000000000..8688f97ff --- /dev/null +++ b/packages/schema-analyzer/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "module": "commonjs", + "target": "ES2023", + "lib": ["ES2023"], + "rootDir": "./src", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/resources/icons/scratchpad-block-active-light.svg b/resources/icons/scratchpad-block-active-light.svg new file mode 100644 index 000000000..45fd17795 --- /dev/null +++ b/resources/icons/scratchpad-block-active-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/scratchpad-block-active.svg b/resources/icons/scratchpad-block-active.svg new file mode 100644 index 000000000..e4b1c1c7f --- /dev/null +++ b/resources/icons/scratchpad-block-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/scratchpad-block-inactive-light.svg b/resources/icons/scratchpad-block-inactive-light.svg new file mode 100644 index 000000000..466dba255 --- /dev/null +++ b/resources/icons/scratchpad-block-inactive-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/scratchpad-block-inactive.svg b/resources/icons/scratchpad-block-inactive.svg new file mode 100644 index 000000000..b8b9474b0 --- /dev/null +++ b/resources/icons/scratchpad-block-inactive.svg @@ -0,0 +1,3 @@ + + + diff --git a/scratchpad-language-configuration.json b/scratchpad-language-configuration.json new file mode 100644 index 000000000..b399985e3 --- /dev/null +++ b/scratchpad-language-configuration.json @@ -0,0 +1,37 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": ["/*", "*/"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "`", "close": "`", "notIn": ["string", "comment"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["'", "'"], + ["\"", "\""], + ["`", "`"] + ], + "folding": { + "markers": { + "start": "^\\s*//\\s*#?region\\b", + "end": "^\\s*//\\s*#?endregion\\b" + } + }, + "indentationRules": { + "increaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$", + "decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]].*$" + } +} diff --git a/src/commands/deleteCollection/deleteCollection.ts b/src/commands/deleteCollection/deleteCollection.ts index 3acb15d5e..0445acb21 100644 --- a/src/commands/deleteCollection/deleteCollection.ts +++ b/src/commands/deleteCollection/deleteCollection.ts @@ -6,6 +6,7 @@ import { type IActionContext } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import { ClustersClient } from '../../documentdb/ClustersClient'; +import { SchemaStore } from '../../documentdb/SchemaStore'; import { ext } from '../../extensionVariables'; import { checkCanProceedAndInformUser } from '../../services/taskService/resourceUsageHelper'; import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; @@ -59,6 +60,11 @@ export async function deleteCollection(context: IActionContext, node: Collection }); if (success) { + SchemaStore.getInstance().clearSchema( + node.cluster.clusterId, + node.databaseInfo.name, + node.collectionInfo.name, + ); showConfirmationAsInSettings(successMessage); } } finally { diff --git a/src/commands/deleteDatabase/deleteDatabase.ts b/src/commands/deleteDatabase/deleteDatabase.ts index 7882940b1..4ad5deb45 100644 --- a/src/commands/deleteDatabase/deleteDatabase.ts +++ b/src/commands/deleteDatabase/deleteDatabase.ts @@ -6,6 +6,7 @@ import { type IActionContext } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import { ClustersClient } from '../../documentdb/ClustersClient'; +import { SchemaStore } from '../../documentdb/SchemaStore'; import { ext } from '../../extensionVariables'; import { checkCanProceedAndInformUser } from '../../services/taskService/resourceUsageHelper'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; @@ -58,6 +59,7 @@ export async function deleteDatabase(context: IActionContext, node: DatabaseItem }); if (success) { + SchemaStore.getInstance().clearDatabase(node.cluster.clusterId, node.databaseInfo.name); showConfirmationAsInSettings(l10n.t('The "{databaseId}" database has been deleted.', { databaseId })); } } finally { diff --git a/src/commands/openCollectionView/openCollectionView.ts b/src/commands/openCollectionView/openCollectionView.ts index 596bc36a1..2dde0700c 100644 --- a/src/commands/openCollectionView/openCollectionView.ts +++ b/src/commands/openCollectionView/openCollectionView.ts @@ -70,5 +70,10 @@ export async function openCollectionViewInternal( feedbackSignalsEnabled: feedbackSignalsEnabled, }); + // Clean up the ClusterSession when the tab is closed + view.onDisposed(() => { + ClusterSession.closeSession(sessionId); + }); + view.revealToForeground(); } diff --git a/src/commands/removeConnection/removeConnection.ts b/src/commands/removeConnection/removeConnection.ts index c56a43b5d..d4fb6cb70 100644 --- a/src/commands/removeConnection/removeConnection.ts +++ b/src/commands/removeConnection/removeConnection.ts @@ -6,6 +6,7 @@ import { UserCancelledError, type IActionContext } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import { CredentialCache } from '../../documentdb/CredentialCache'; +import { SchemaStore } from '../../documentdb/SchemaStore'; import { ext } from '../../extensionVariables'; import { ConnectionStorageService, ConnectionType } from '../../services/connectionStorageService'; import { checkCanProceedAndInformUser } from '../../services/taskService/resourceUsageHelper'; @@ -66,6 +67,9 @@ export async function removeConnection(context: IActionContext, node: DocumentDB // delete cached credentials from memory using stable clusterId (not treeId) CredentialCache.deleteCredentials(node.cluster.clusterId); + // clear cached schema data for this cluster + SchemaStore.getInstance().clearCluster(node.cluster.clusterId); + refreshParentInConnectionsView(node.id); }); diff --git a/src/commands/schemaStore/clearSchemaCache.ts b/src/commands/schemaStore/clearSchemaCache.ts new file mode 100644 index 000000000..088b653b4 --- /dev/null +++ b/src/commands/schemaStore/clearSchemaCache.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import { SchemaStore } from '../../documentdb/SchemaStore'; +import { ext } from '../../extensionVariables'; + +/** + * Command handler: Clear the shared schema cache. + * Logs current size, clears all entries, and logs the result. + */ +export function clearSchemaCache(_context: IActionContext): void { + const store = SchemaStore.getInstance(); + const before = store.getStats(); + + ext.outputChannel.appendLog( + l10n.t( + '[SchemaStore] Clearing schema cache: {0} collections, {1} documents, {2} fields', + String(before.collectionCount), + String(before.totalDocuments), + String(before.totalFields), + ), + ); + + store.reset(); + + const after = store.getStats(); + ext.outputChannel.appendLog( + l10n.t( + '[SchemaStore] Schema cache cleared: {0} collections, {1} documents, {2} fields', + String(after.collectionCount), + String(after.totalDocuments), + String(after.totalFields), + ), + ); + + ext.outputChannel.show(); +} diff --git a/src/commands/schemaStore/showSchemaStoreStats.ts b/src/commands/schemaStore/showSchemaStoreStats.ts new file mode 100644 index 000000000..f038f34d9 --- /dev/null +++ b/src/commands/schemaStore/showSchemaStoreStats.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { SchemaStore } from '../../documentdb/SchemaStore'; +import { ext } from '../../extensionVariables'; + +/** + * Command handler: Show SchemaStore statistics in the output channel. + * Displays collection count, document count, field count, and per-collection breakdown. + */ +export function showSchemaStoreStats(_context: IActionContext): void { + const store = SchemaStore.getInstance(); + const stats = store.getStats(); + + ext.outputChannel.appendLog( + `[SchemaStore] Stats: ${String(stats.collectionCount)} collections, ` + + `${String(stats.totalDocuments)} documents analyzed, ` + + `${String(stats.totalFields)} fields discovered`, + ); + + if (stats.collections.length > 0) { + for (const c of stats.collections) { + // Key format is "clusterId::db::collection" โ€” show only db/collection + const parts = c.key.split('::'); + const displayKey = parts.length >= 3 ? `${parts[1]}/${parts[2]}` : c.key; + ext.outputChannel.appendLog( + `[SchemaStore] ${displayKey}: ${String(c.documentCount)} docs, ${String(c.fieldCount)} fields`, + ); + } + } else { + ext.outputChannel.appendLog('[SchemaStore] (empty โ€” no schemas cached)'); + } + + ext.outputChannel.show(); +} diff --git a/src/commands/scrapbook-commands/connectCluster.ts b/src/commands/scrapbook-commands/connectCluster.ts deleted file mode 100644 index fa7c226b9..000000000 --- a/src/commands/scrapbook-commands/connectCluster.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import * as vscode from 'vscode'; -import { ScrapbookService } from '../../documentdb/scrapbook/ScrapbookService'; -import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; -import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; - -export async function connectCluster(_context: IActionContext, node?: DatabaseItem | CollectionItem): Promise { - if (!node) { - await vscode.window.showInformationMessage( - l10n.t('You can connect to a different DocumentDB by:') + - '\n\n' + - l10n.t("1. Locating the one you'd like from the DocumentDB side panel,") + - '\n' + - l10n.t('2. Selecting a database or a collection,') + - '\n' + - l10n.t('3. Right-clicking and then choosing the "Mongo Scrapbook" submenu,') + - '\n' + - l10n.t('4. Selecting the "Connect to this database" command.'), - { modal: true }, - ); - return; - } - - await ScrapbookService.setConnectedCluster(node.cluster, node.databaseInfo); -} diff --git a/src/commands/scrapbook-commands/createScrapbook.ts b/src/commands/scrapbook-commands/createScrapbook.ts deleted file mode 100644 index 96be6fe07..000000000 --- a/src/commands/scrapbook-commands/createScrapbook.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import { ScrapbookService } from '../../documentdb/scrapbook/ScrapbookService'; -import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; -import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; -import * as vscodeUtil from '../../utils/vscodeUtils'; - -export async function createScrapbook(_context: IActionContext, node: DatabaseItem | CollectionItem): Promise { - const initialFileContents: string = '// MongoDB API Scrapbook: Use this file to run MongoDB API commands\n\n'; - - // if (node instanceof CollectionItem) { - // initialFileContents += `\n\n// You are connected to the "${node.collectionInfo.name}" collection in the "${node.databaseInfo.name}" database.`; - // } else if (node instanceof DatabaseItem) { - // initialFileContents += `\n\n// You are connected to the "${node.databaseInfo.name}" database.`; - // } - - await ScrapbookService.setConnectedCluster(node.cluster, node.databaseInfo); - - await vscodeUtil.showNewFile(initialFileContents, 'Scrapbook', '.vscode-documentdb-scrapbook'); -} diff --git a/src/commands/scrapbook-commands/executeAllCommand.ts b/src/commands/scrapbook-commands/executeAllCommand.ts deleted file mode 100644 index 8b2de473e..000000000 --- a/src/commands/scrapbook-commands/executeAllCommand.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import * as vscode from 'vscode'; -import { ScrapbookService } from '../../documentdb/scrapbook/ScrapbookService'; -import { withProgress } from '../../utils/withProgress'; - -export async function executeAllCommand(context: IActionContext): Promise { - const editor = vscode.window.activeTextEditor; - if (!editor) { - throw new Error(l10n.t('You must open a *.vscode-documentdb-scrapbook file to run commands.')); - } - await withProgress( - ScrapbookService.executeAllCommands(context, editor.document), - l10n.t('Executing all commands in shellโ€ฆ'), - ); -} diff --git a/src/commands/scrapbook-commands/executeCommand.ts b/src/commands/scrapbook-commands/executeCommand.ts deleted file mode 100644 index b3c2bc7bf..000000000 --- a/src/commands/scrapbook-commands/executeCommand.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import * as vscode from 'vscode'; -import { ScrapbookService } from '../../documentdb/scrapbook/ScrapbookService'; -import { withProgress } from '../../utils/withProgress'; - -export async function executeCommand(context: IActionContext, position?: vscode.Position): Promise { - const editor = vscode.window.activeTextEditor; - if (!editor) { - throw new Error(l10n.t('You must open a *.vscode-documentdb-scrapbook file to run commands.')); - } - - const pos = position ?? editor.selection.start; - - await withProgress( - ScrapbookService.executeCommandAtPosition(context, editor.document, pos), - l10n.t('Executing the command in shellโ€ฆ'), - ); -} diff --git a/src/commands/scratchpad/connectDatabase.ts b/src/commands/scratchpad/connectDatabase.ts new file mode 100644 index 000000000..c2fb03fa7 --- /dev/null +++ b/src/commands/scratchpad/connectDatabase.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ScratchpadService } from '../../documentdb/scratchpad/ScratchpadService'; +import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; +import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; + +/** + * Sets the active scratchpad connection from a tree node context, + * or shows instructions when invoked without a tree context (e.g., CodeLens click). + */ +export async function connectDatabase(_context: IActionContext, node?: DatabaseItem | CollectionItem): Promise { + if (node) { + const service = ScratchpadService.getInstance(); + service.setConnection({ + clusterId: node.cluster.clusterId, + clusterDisplayName: node.cluster.name, + databaseName: node.databaseInfo.name, + }); + + void vscode.window.showInformationMessage( + l10n.t('DocumentDB Scratchpad connected to {0}/{1}', node.cluster.name, node.databaseInfo.name), + ); + } else { + // No tree context โ€” show instructions as modal dialog + void vscode.window.showInformationMessage(l10n.t('No database connected'), { + modal: true, + detail: l10n.t( + 'Right-click a database or collection in the DocumentDB panel and select "Connect Scratchpad to this database".', + ), + }); + } +} diff --git a/src/commands/scratchpad/executeScratchpadCode.ts b/src/commands/scratchpad/executeScratchpadCode.ts new file mode 100644 index 000000000..98bb02a27 --- /dev/null +++ b/src/commands/scratchpad/executeScratchpadCode.ts @@ -0,0 +1,276 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + UserCancelledError, + callWithTelemetryAndErrorHandling, + openReadOnlyContent, +} from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import { type Document, type WithId } from 'mongodb'; +import * as vscode from 'vscode'; +import { CredentialCache } from '../../documentdb/CredentialCache'; +import { SchemaStore } from '../../documentdb/SchemaStore'; +import { ScratchpadEvaluator } from '../../documentdb/scratchpad/ScratchpadEvaluator'; +import { ScratchpadService } from '../../documentdb/scratchpad/ScratchpadService'; +import { formatError, formatResult } from '../../documentdb/scratchpad/resultFormatter'; +import { type ExecutionResult, type ScratchpadConnection } from '../../documentdb/scratchpad/types'; +import { getHostsFromConnectionString } from '../../documentdb/utils/connectionStringHelpers'; +import { addDomainInfoToProperties } from '../../documentdb/utils/getClusterMetadata'; + +/** Shared evaluator instance โ€” lazily created, reused across runs. */ +let evaluator: ScratchpadEvaluator | undefined; + +/** + * Dispose the shared evaluator instance (kills the worker thread). + * Called during extension deactivation. + */ +export function disposeEvaluator(): void { + evaluator?.dispose(); + evaluator = undefined; +} + +/** + * Gracefully shut down the evaluator's worker thread (closes MongoClient). + * Called when the scratchpad connection is cleared or when all scratchpad editors close. + * The worker will be re-spawned lazily on the next Run. + */ +export function shutdownEvaluator(): void { + void evaluator?.shutdown(); +} + +/** + * Executes scratchpad code and displays the result in a read-only side panel. + * Used by both `runAll` and `runSelected` commands. + */ +export type ScratchpadRunMode = 'runAll' | 'runSelected'; + +export async function executeScratchpadCode(code: string, runMode: ScratchpadRunMode): Promise { + const service = ScratchpadService.getInstance(); + const connection = service.getConnection(); + if (!connection) { + return; + } + + // Prevent concurrent runs โ€” no queuing + if (service.isExecuting) { + return; + } + + if (!evaluator) { + evaluator = new ScratchpadEvaluator(); + } + + service.setExecuting(true); + + // callWithTelemetryAndErrorHandling automatically tracks: + // - duration (measured from callback start to end) + // - result: 'Succeeded' | 'Failed' | 'Canceled' + // - error / errorMessage (from thrown errors) + // We add custom properties for scratchpad-specific analytics. + await callWithTelemetryAndErrorHandling('scratchpad.execute', async (context) => { + context.errorHandling.suppressDisplay = true; // we show our own error UI + context.errorHandling.rethrow = false; + + // โ”€โ”€ Pre-execution telemetry (known before eval) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + context.telemetry.properties.sessionId = evaluator!.sessionId ?? 'none'; + context.telemetry.properties.sessionEvalCount = String(evaluator!.sessionEvalCount); + context.telemetry.properties.authMethod = evaluator!.sessionAuthMethod ?? 'unknown'; + context.telemetry.properties.runMode = runMode; + context.telemetry.measurements.codeLineCount = code.split('\n').length; + + // Domain info โ€” privacy-safe hashed host data for platform analytics + const domainProps: Record = {}; + collectDomainTelemetry(connection, domainProps); + Object.assign(context.telemetry.properties, domainProps); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: l10n.t('DocumentDB Scratchpad'), + cancellable: true, + }, + async (progress, token) => { + let cancelled = false; + + token.onCancellationRequested(() => { + cancelled = true; + evaluator?.killWorker(); + }); + + const startTime = Date.now(); + try { + const result = await evaluator!.evaluate(connection, code, (message) => { + progress.report({ message }); + }); + + // โ”€โ”€ Post-execution telemetry (known after success) โ”€โ”€โ”€โ”€ + context.telemetry.properties.resultType = result.type ?? 'null'; + context.telemetry.measurements.initDurationMs = evaluator!.lastInitDurationMs; + // sessionId/sessionEvalCount may have changed after evaluate (if worker was spawned) + context.telemetry.properties.sessionId = evaluator!.sessionId ?? 'none'; + context.telemetry.properties.sessionEvalCount = String(evaluator!.sessionEvalCount); + context.telemetry.properties.authMethod = evaluator!.sessionAuthMethod ?? 'unknown'; + + const formattedOutput = formatResult(result, code, connection); + feedResultToSchemaStore(result, connection); + + const resultLabel = l10n.t( + '{0}/{1} โ€” Results', + connection.clusterDisplayName, + connection.databaseName, + ); + + await openReadOnlyContent( + { label: resultLabel, fullId: `scratchpad-results-${Date.now()}` }, + formattedOutput, + '.jsonc', + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }, + ); + + // result: 'Succeeded' is set automatically by the framework + } catch (error: unknown) { + // Update session telemetry even on failure (worker may have spawned before failing) + context.telemetry.properties.sessionId = evaluator!.sessionId ?? 'none'; + context.telemetry.properties.sessionEvalCount = String(evaluator!.sessionEvalCount); + context.telemetry.properties.authMethod = evaluator!.sessionAuthMethod ?? 'unknown'; + context.telemetry.measurements.initDurationMs = evaluator!.lastInitDurationMs; + + if (cancelled) { + // Throw UserCancelledError so framework marks result as 'Canceled' + throw new UserCancelledError('scratchpad.execute'); + } + + // Show our own error UI before re-throwing + const errorMessage = error instanceof Error ? error.message : String(error); + const durationMs = Date.now() - startTime; + const formattedOutput = formatError(error, code, durationMs, connection); + + const errorLabel = l10n.t( + '{0}/{1} โ€” Error', + connection.clusterDisplayName, + connection.databaseName, + ); + + await openReadOnlyContent( + { label: errorLabel, fullId: `scratchpad-error-${Date.now()}` }, + formattedOutput, + '.jsonc', + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }, + ); + + void vscode.window.showErrorMessage(l10n.t('Scratchpad execution failed: {0}', errorMessage)); + + // Re-throw so framework automatically captures result: 'Failed', + // error, and errorMessage in telemetry + throw error; + } finally { + service.setExecuting(false); + } + }, + ); + }); +} + +// โ”€โ”€โ”€ Domain telemetry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** + * Collects domain info from the scratchpad connection's cached credentials. + * Reuses the same hashing logic as the connection metadata telemetry. + */ +function collectDomainTelemetry( + connection: ScratchpadConnection, + properties: Record, +): void { + try { + const credentials = CredentialCache.getCredentials(connection.clusterId); + if (!credentials?.connectionString) { + return; + } + const hosts = getHostsFromConnectionString(credentials.connectionString); + addDomainInfoToProperties(hosts, properties); + } catch { + // Domain info is best-effort โ€” don't fail telemetry if parsing fails + } +} + +/** + * Maximum number of documents to feed to SchemaStore per execution. + * If the result set is larger, a random sample of this size is used. + */ +const SCHEMA_DOC_CAP = 100; + +/** + * Very conservative schema feeding: only feed 'Cursor' and 'Document' result types. + * Extracts documents from the printable result and adds them to SchemaStore. + * + * Caps at {@link SCHEMA_DOC_CAP} documents (randomly sampled if more). + */ +function feedResultToSchemaStore(result: ExecutionResult, connection: ScratchpadConnection): void { + // Only feed known document-producing result types + if (result.type !== 'Cursor' && result.type !== 'Document') { + return; + } + + const ns = result.source?.namespace; + if (!ns?.collection) { + return; + } + + const printable = result.printable; + if (printable === null || printable === undefined) { + return; + } + + // CursorIterationResult from @mongosh wraps documents in { cursorHasMore, documents }. + // Only unwrap when the full wrapper shape is present to avoid false positives + // on user documents that happen to have a `documents` field. + let items: unknown[]; + if ( + typeof printable === 'object' && + !Array.isArray(printable) && + 'cursorHasMore' in printable && + typeof (printable as Record).cursorHasMore === 'boolean' && + 'documents' in printable && + Array.isArray((printable as { documents: unknown }).documents) + ) { + items = (printable as { documents: unknown[] }).documents; + } else if (Array.isArray(printable)) { + items = printable; + } else { + items = [printable]; + } + + // Filter to actual document objects with _id (not primitives, not nested arrays, + // not projection results with _id: 0 which have artificial shapes) + let docs = items.filter( + (d): d is WithId => + d !== null && d !== undefined && typeof d === 'object' && !Array.isArray(d) && '_id' in d, + ); + + if (docs.length === 0) { + return; + } + + // Cap at SCHEMA_DOC_CAP documents โ€” randomly sample if more + if (docs.length > SCHEMA_DOC_CAP) { + docs = randomSample(docs, SCHEMA_DOC_CAP); + } + + SchemaStore.getInstance().addDocuments(connection.clusterId, ns.db, ns.collection, docs); +} + +/** + * Partial Fisherโ€“Yates random sample of `count` items from `array`. + * Only performs `count` swaps instead of shuffling the entire array. + */ +function randomSample(array: T[], count: number): T[] { + const copy = [...array]; + for (let i = 0; i < count; i++) { + const j = i + Math.floor(Math.random() * (copy.length - i)); + [copy[i], copy[j]] = [copy[j], copy[i]]; + } + return copy.slice(0, count); +} diff --git a/src/commands/scratchpad/newScratchpad.ts b/src/commands/scratchpad/newScratchpad.ts new file mode 100644 index 000000000..d3fcdd8a4 --- /dev/null +++ b/src/commands/scratchpad/newScratchpad.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ScratchpadService } from '../../documentdb/scratchpad/ScratchpadService'; +import { SCRATCHPAD_FILE_EXTENSION } from '../../documentdb/scratchpad/constants'; +import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; +import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; + +/** + * Creates a new DocumentDB Scratchpad file. + * + * When invoked from a tree node (database or collection), the scratchpad + * connection is set to that node's cluster/database and the template + * is pre-filled accordingly. + */ +export async function newScratchpad(_context: IActionContext, node?: DatabaseItem | CollectionItem): Promise { + const service = ScratchpadService.getInstance(); + + // If invoked from a tree node, set the connection + if (node) { + service.setConnection({ + clusterId: node.cluster.clusterId, + clusterDisplayName: node.cluster.name, + databaseName: node.databaseInfo.name, + }); + } + + // Build template โ€” customize when launched from a collection node + const collectionName = isCollectionItem(node) ? node.collectionInfo.name : 'collectionName'; + const headerComment = node + ? `// DocumentDB Scratchpad โ€” ${collectionName} @ ${node.cluster.name}/${node.databaseInfo.name}` + : '// DocumentDB Scratchpad โ€” Write and run DocumentDB API queries'; + + const template = [ + headerComment, + '// Use Ctrl+Enter (Cmd+Enter) to run the current block', + '// Use Ctrl+Shift+Enter (Cmd+Shift+Enter) to run the entire file', + '// Note: when running multiple statements, only the last result is displayed', + '', + `db.getCollection('${collectionName}').find({ })`, + '', + ].join('\n'); + + // Create untitled file with a unique human-readable name + const now = new Date(); + const timestamp = now + .toLocaleString(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }) + .replace(/[/\\:]/g, '-') + .replace(/,\s*/g, '_'); + const uri = vscode.Uri.from({ scheme: 'untitled', path: `scratchpad-${timestamp}${SCRATCHPAD_FILE_EXTENSION}` }); + const edit = new vscode.WorkspaceEdit(); + edit.insert(uri, new vscode.Position(0, 0), template); + await vscode.workspace.applyEdit(edit); + const doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc); +} + +function isCollectionItem(node: DatabaseItem | CollectionItem | undefined): node is CollectionItem { + return node !== undefined && 'collectionInfo' in node; +} diff --git a/src/commands/scratchpad/runAll.ts b/src/commands/scratchpad/runAll.ts new file mode 100644 index 000000000..d97817303 --- /dev/null +++ b/src/commands/scratchpad/runAll.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ScratchpadService } from '../../documentdb/scratchpad/ScratchpadService'; +import { SCRATCHPAD_LANGUAGE_ID } from '../../documentdb/scratchpad/constants'; +import { executeScratchpadCode } from './executeScratchpadCode'; + +/** + * Runs the entire content of the active scratchpad file. + */ +export async function runAll(_context: IActionContext): Promise { + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.languageId !== SCRATCHPAD_LANGUAGE_ID) { + return; + } + + const service = ScratchpadService.getInstance(); + if (!service.isConnected()) { + void vscode.window.showWarningMessage( + l10n.t('Connect to a database before running. Right-click a database in the DocumentDB panel.'), + ); + return; + } + + const code = editor.document.getText(); + if (!code.trim()) { + return; + } + + await executeScratchpadCode(code, 'runAll'); +} diff --git a/src/commands/scratchpad/runSelected.ts b/src/commands/scratchpad/runSelected.ts new file mode 100644 index 000000000..0cf30c06b --- /dev/null +++ b/src/commands/scratchpad/runSelected.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ScratchpadService } from '../../documentdb/scratchpad/ScratchpadService'; +import { SCRATCHPAD_LANGUAGE_ID } from '../../documentdb/scratchpad/constants'; +import { detectBlocks, detectCurrentBlock } from '../../documentdb/scratchpad/statementDetector'; +import { executeScratchpadCode } from './executeScratchpadCode'; + +/** + * Runs the selected text, the block specified by CodeLens arguments, + * or the current block at the cursor position. + * + * CodeLens passes `[startLine, endLine]` as arguments so clicking + * a per-block "โ–ถ Run" lens executes exactly that block. + */ +export async function runSelected(_context: IActionContext, startLine?: number, endLine?: number): Promise { + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.languageId !== SCRATCHPAD_LANGUAGE_ID) { + return; + } + + const service = ScratchpadService.getInstance(); + if (!service.isConnected()) { + void vscode.window.showWarningMessage( + l10n.t('Connect to a database before running. Right-click a database in the DocumentDB panel.'), + ); + return; + } + + let codeToRun: string; + + if (startLine !== undefined && endLine !== undefined) { + // Invoked from CodeLens with explicit block range + const range = new vscode.Range(startLine, 0, endLine, editor.document.lineAt(endLine).text.length); + codeToRun = editor.document.getText(range); + } else if (!editor.selection.isEmpty) { + // Behavior 1: Run selection + codeToRun = editor.document.getText(editor.selection); + } else { + // Behavior 2: Run current block at cursor, fall back to preceding block + codeToRun = detectCurrentBlock(editor.document, editor.selection.active); + if (!codeToRun.trim()) { + // Cursor is on a blank line โ€” fall back to the nearest preceding block + // (same behavior as CodeLens resolveActiveBlock) + const blocks = detectBlocks(editor.document); + const cursorLine = editor.selection.active.line; + for (let i = blocks.length - 1; i >= 0; i--) { + if (blocks[i].endLine < cursorLine) { + const range = new vscode.Range( + blocks[i].startLine, + 0, + blocks[i].endLine, + editor.document.lineAt(blocks[i].endLine).text.length, + ); + codeToRun = editor.document.getText(range); + break; + } + } + } + } + + if (!codeToRun.trim()) { + return; + } + + await executeScratchpadCode(codeToRun, 'runSelected'); +} diff --git a/src/documentdb/ClusterSession.ts b/src/documentdb/ClusterSession.ts index da81218fe..9436d33b1 100644 --- a/src/documentdb/ClusterSession.ts +++ b/src/documentdb/ClusterSession.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ParseMode, parse as parseShellBSON } from '@mongodb-js/shell-bson-parser'; +import { type FieldEntry } from '@vscode-documentdb/schema-analyzer'; import * as l10n from '@vscode/l10n'; import { EJSON } from 'bson'; import { ObjectId, type Document, type Filter, type WithId } from 'mongodb'; -import { type JSONSchema } from '../utils/json/JSONSchema'; -import { getPropertyNamesAtLevel, updateSchemaWithDocument } from '../utils/json/mongo/SchemaAnalyzer'; +import { ext } from '../extensionVariables'; import { getDataAtPath } from '../utils/slickgrid/mongo/toSlickGridTable'; import { toSlickGridTree, type TreeData } from '../utils/slickgrid/mongo/toSlickGridTree'; import { ClustersClient, type FindQueryParams } from './ClustersClient'; +import { SchemaStore } from './SchemaStore'; import { toFilterQueryObj } from './utils/toFilterQuery'; export type TableDataEntry = { @@ -65,7 +67,10 @@ export class ClusterSession { * Private constructor to enforce the use of `initNewSession` for creating new sessions. * This ensures that sessions are properly initialized and managed. */ - private constructor(private _client: ClustersClient) { + private constructor( + private _client: ClustersClient, + private readonly _clusterId: string, + ) { return; } @@ -74,19 +79,16 @@ export class ClusterSession { } /** - * Accumulated JSON schema across all pages seen for the current query. - * Updates progressively as users navigate through different pages. - * Reset when the query or page size changes. + * Database name for the current collection view. + * Set on the first runFindQueryWithCache call. */ - private _accumulatedJsonSchema: JSONSchema = {}; + private _databaseName: string | undefined; /** - * Tracks the highest page number that has been accumulated into the schema. - * Users navigate sequentially starting from page 1, so any page โ‰ค this value - * has already been accumulated and should be skipped. - * Reset when the query or page size changes. + * Collection name for the current collection view. + * Set on the first runFindQueryWithCache call. */ - private _highestPageAccumulated: number = 0; + private _collectionName: string | undefined; /** * Stores the user's original query parameters (filter, project, sort, skip, limit). @@ -161,9 +163,14 @@ export class ClusterSession { } } - // The user's query has changed, invalidate all caches - this._accumulatedJsonSchema = {}; - this._highestPageAccumulated = 0; + // The user's query has changed, invalidate all caches. + // + // NOTE: We intentionally do NOT reset schema here. + // Schema data is accumulated monotonically in the shared SchemaStore. + // When a new query returns 0 results, preserving field knowledge from + // previous queries is more valuable for autocompletion than having an + // empty field list. Consumers should treat type frequency data as + // approximate/relative (e.g., "mostly String"). this._currentPageSize = null; this._currentRawDocuments = []; this._lastExecutionTimeMs = undefined; @@ -184,9 +191,9 @@ export class ClusterSession { */ private resetAccumulationIfPageSizeChanged(newPageSize: number): void { if (this._currentPageSize !== null && this._currentPageSize !== newPageSize) { - // Page size changed, reset accumulation tracking - this._accumulatedJsonSchema = {}; - this._highestPageAccumulated = 0; + // Page size changed โ€” schema data in SchemaStore is still valid + // (accumulated monotonically), just note the size change + ext.outputChannel.trace('[SchemaStore] Page size changed'); } this._currentPageSize = newPageSize; } @@ -294,12 +301,18 @@ export class ClusterSession { // Update current page documents (always replace, not accumulate) this._currentRawDocuments = documents; - // Accumulate schema only if this page hasn't been seen before - // Since navigation is sequential and starts at page 1, we only need to track - // the highest page number accumulated - if (pageNumber > this._highestPageAccumulated) { - this._currentRawDocuments.map((doc) => updateSchemaWithDocument(this._accumulatedJsonSchema, doc)); - this._highestPageAccumulated = pageNumber; + // Store database/collection context for schema operations + this._databaseName = databaseName; + this._collectionName = collectionName; + + // Feed documents to the shared SchemaStore + if (documents.length > 0) { + const store = SchemaStore.getInstance(); + store.addDocuments(this._clusterId, databaseName, collectionName, documents); + + ext.outputChannel.trace( + `[SchemaStore] Fed ${String(documents.length)} documents, ${String(store.getDocumentCount(this._clusterId, databaseName, collectionName))} total, ${String(store.getKnownFields(this._clusterId, databaseName, collectionName).length)} known fields`, + ); } return documents.length; @@ -353,17 +366,28 @@ export class ClusterSession { } public getCurrentPageAsTable(path: string[]): TableData { + const store = SchemaStore.getInstance(); const responsePack: TableData = { path: path, - headers: getPropertyNamesAtLevel(this._accumulatedJsonSchema, path), + headers: + this._databaseName && this._collectionName + ? store.getPropertyNamesAtLevel(this._clusterId, this._databaseName, this._collectionName, path) + : [], data: getDataAtPath(this._currentRawDocuments, path), }; return responsePack; } - public getCurrentSchema(): JSONSchema { - return this._accumulatedJsonSchema; + /** + * Returns the cached list of known fields from the shared SchemaStore. + * Delegates to SchemaStore which provides cross-tab schema sharing. + */ + public getKnownFields(): FieldEntry[] { + if (!this._databaseName || !this._collectionName) { + return []; + } + return SchemaStore.getInstance().getKnownFields(this._clusterId, this._databaseName, this._collectionName); } // ============================================================================ @@ -521,7 +545,7 @@ export class ClusterSession { * @remarks * This method uses the same BSON parsing logic as ClustersClient.runFindQuery(): * - filter is parsed with toFilterQueryObj() which handles UUID(), Date(), MinKey(), MaxKey() constructors - * - projection and sort are parsed with EJSON.parse() + * - projection and sort are parsed with parseShellBSON() in Loose mode * * Use this method when you need the actual MongoDB Document objects for query execution. * Use getCurrentFindQueryParams() when you only need the string representations. @@ -536,7 +560,9 @@ export class ClusterSession { let projectionObj: Document | undefined; if (stringParams.project && stringParams.project.trim() !== '{}') { try { - projectionObj = EJSON.parse(stringParams.project) as Document; + projectionObj = parseShellBSON(stringParams.project, { + mode: ParseMode.Loose, + }) as Document; } catch (error) { throw new Error( l10n.t('Invalid projection syntax: {0}', error instanceof Error ? error.message : String(error)), @@ -548,7 +574,9 @@ export class ClusterSession { let sortObj: Document | undefined; if (stringParams.sort && stringParams.sort.trim() !== '{}') { try { - sortObj = EJSON.parse(stringParams.sort) as Document; + sortObj = parseShellBSON(stringParams.sort, { + mode: ParseMode.Loose, + }) as Document; } catch (error) { throw new Error( l10n.t('Invalid sort syntax: {0}', error instanceof Error ? error.message : String(error)), @@ -658,7 +686,7 @@ export class ClusterSession { const sessionId = Math.random().toString(16).substring(2, 15) + Math.random().toString(36).substring(2, 15); - const session = new ClusterSession(client); + const session = new ClusterSession(client, credentialId); ClusterSession._sessions.set(sessionId, session); diff --git a/src/documentdb/ClustersClient.ts b/src/documentdb/ClustersClient.ts index 4d7c34ede..dd5cf091d 100644 --- a/src/documentdb/ClustersClient.ts +++ b/src/documentdb/ClustersClient.ts @@ -15,6 +15,7 @@ import { callWithTelemetryAndErrorHandling, parseError, } from '@microsoft/vscode-azext-utils'; +import { ParseMode, parse as parseShellBSON } from '@mongodb-js/shell-bson-parser'; import * as l10n from '@vscode/l10n'; import { EJSON } from 'bson'; import { @@ -53,6 +54,7 @@ import { type IndexSpecification, type IndexStats, } from './LlmEnhancedFeatureApis'; +import { SchemaStore } from './SchemaStore'; import { getHostsFromConnectionString, hasAzureDomain } from './utils/connectionStringHelpers'; import { getClusterMetadata, type ClusterMetadata } from './utils/getClusterMetadata'; import { toFilterQueryObj } from './utils/toFilterQuery'; @@ -289,6 +291,15 @@ export class ClustersClient { } } + /** + * Returns the underlying MongoClient instance. + * Used by the scratchpad evaluator to create a `@mongosh` ServiceProvider + * that reuses the existing, authenticated connection. + */ + public getMongoClient(): MongoClient { + return this._mongoClient; + } + /** * Retrieves an instance of `ClustersClient` based on the provided `clusterId`. * @@ -350,6 +361,9 @@ export class ClustersClient { const client = ClustersClient._clients.get(credentialId) as ClustersClient; await client._mongoClient.close(true); ClustersClient._clients.delete(credentialId); + + // Clear cached schema data for this cluster + SchemaStore.getInstance().clearCluster(credentialId); } } @@ -555,13 +569,15 @@ export class ClustersClient { // Parse and add projection if provided if (queryParams.project && queryParams.project.trim() !== '{}') { try { - options.projection = EJSON.parse(queryParams.project) as Document; + options.projection = parseShellBSON(queryParams.project, { + mode: ParseMode.Loose, + }) as Document; } catch (error) { const cause = error instanceof Error ? error : new Error(String(error)); throw new QueryError( 'INVALID_PROJECTION', l10n.t( - 'Invalid projection syntax: {0}. Please use valid JSON, for example: { "fieldName": 1 }', + 'Invalid projection syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { fieldName: 1 }', cause.message, ), cause, @@ -572,13 +588,15 @@ export class ClustersClient { // Parse and add sort if provided if (queryParams.sort && queryParams.sort.trim() !== '{}') { try { - options.sort = EJSON.parse(queryParams.sort) as Document; + options.sort = parseShellBSON(queryParams.sort, { + mode: ParseMode.Loose, + }) as Document; } catch (error) { const cause = error instanceof Error ? error : new Error(String(error)); throw new QueryError( 'INVALID_SORT', l10n.t( - 'Invalid sort syntax: {0}. Please use valid JSON, for example: { "fieldName": 1 }', + 'Invalid sort syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { fieldName: 1 }', cause.message, ), cause, @@ -704,13 +722,15 @@ export class ClustersClient { // Parse and add projection if provided if (queryParams.project && queryParams.project.trim() !== '{}') { try { - options.projection = EJSON.parse(queryParams.project) as Document; + options.projection = parseShellBSON(queryParams.project, { + mode: ParseMode.Loose, + }) as Document; } catch (error) { const cause = error instanceof Error ? error : new Error(String(error)); throw new QueryError( 'INVALID_PROJECTION', l10n.t( - 'Invalid projection syntax: {0}. Please use valid JSON, for example: { "fieldName": 1 }', + 'Invalid projection syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { fieldName: 1 }', cause.message, ), cause, @@ -721,13 +741,15 @@ export class ClustersClient { // Parse and add sort if provided if (queryParams.sort && queryParams.sort.trim() !== '{}') { try { - options.sort = EJSON.parse(queryParams.sort) as Document; + options.sort = parseShellBSON(queryParams.sort, { + mode: ParseMode.Loose, + }) as Document; } catch (error) { const cause = error instanceof Error ? error : new Error(String(error)); throw new QueryError( 'INVALID_SORT', l10n.t( - 'Invalid sort syntax: {0}. Please use valid JSON, for example: { "fieldName": 1 }', + 'Invalid sort syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { fieldName: 1 }', cause.message, ), cause, diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 59f9422e8..f6554267d 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -55,6 +55,13 @@ import { removeConnection } from '../commands/removeConnection/removeConnection' import { removeDiscoveryRegistry } from '../commands/removeDiscoveryRegistry/removeDiscoveryRegistry'; import { retryAuthentication } from '../commands/retryAuthentication/retryAuthentication'; import { revealView } from '../commands/revealView/revealView'; +import { clearSchemaCache } from '../commands/schemaStore/clearSchemaCache'; +import { showSchemaStoreStats } from '../commands/schemaStore/showSchemaStoreStats'; +import { connectDatabase } from '../commands/scratchpad/connectDatabase'; +import { disposeEvaluator, shutdownEvaluator } from '../commands/scratchpad/executeScratchpadCode'; +import { newScratchpad } from '../commands/scratchpad/newScratchpad'; +import { runAll } from '../commands/scratchpad/runAll'; +import { runSelected } from '../commands/scratchpad/runSelected'; import { updateConnectionString } from '../commands/updateConnectionString/updateConnectionString'; import { updateCredentials } from '../commands/updateCredentials/updateCredentials'; import { isVCoreAndRURolloutEnabled } from '../extension'; @@ -79,7 +86,10 @@ import { registerCommandWithTreeNodeUnwrappingAndModalErrors, } from '../utils/commandErrorHandling'; import { withCommandCorrelation, withTreeNodeCommandCorrelation } from '../utils/commandTelemetry'; -import { registerScrapbookCommands } from './scrapbook/registerScrapbookCommands'; +import { SCRATCHPAD_LANGUAGE_ID, ScratchpadCommandIds } from './scratchpad/constants'; +import { ScratchpadBlockHighlighter } from './scratchpad/ScratchpadBlockHighlighter'; +import { ScratchpadCodeLensProvider } from './scratchpad/ScratchpadCodeLensProvider'; +import { ScratchpadService } from './scratchpad/ScratchpadService'; import { Views } from './Views'; export class ClustersExtension implements vscode.Disposable { @@ -192,6 +202,97 @@ export class ClustersExtension implements vscode.Disposable { // Initialize TaskService and TaskProgressReportingService TaskProgressReportingService.attach(TaskService); + // Initialize ScratchpadService (connection state + StatusBarItem) + const scratchpadService = ScratchpadService.getInstance(); + ext.context.subscriptions.push(scratchpadService); + + // Register evaluator disposal for clean worker shutdown on deactivation + ext.context.subscriptions.push({ dispose: disposeEvaluator }); + + // Shut down the scratchpad worker when connection is cleared + ext.context.subscriptions.push( + scratchpadService.onDidChangeState(() => { + if (!scratchpadService.isConnected()) { + ext.outputChannel.debug('[Scratchpad] Connection cleared โ€” shutting down worker'); + shutdownEvaluator(); + } + }), + ); + + // Shut down the scratchpad worker when the last .documentdb editor closes + ext.context.subscriptions.push( + vscode.window.tabGroups.onDidChangeTabs((event) => { + // Only react when tabs are closed + if (event.closed.length === 0) { + return; + } + + // Check if any closed tab was a scratchpad + const closedScratchpad = event.closed.some((tab) => { + const input = tab.input; + return ( + input instanceof vscode.TabInputText && + (input.uri.path.endsWith('.documentdb') || input.uri.path.endsWith('.documentdb.js')) + ); + }); + + if (!closedScratchpad) { + return; + } + + // Check if any scratchpad tabs remain open + const hasOpenScratchpad = vscode.window.tabGroups.all.some((group) => + group.tabs.some((tab) => { + const input = tab.input; + return ( + input instanceof vscode.TabInputText && + (input.uri.path.endsWith('.documentdb') || + input.uri.path.endsWith('.documentdb.js')) + ); + }), + ); + + if (!hasOpenScratchpad) { + ext.outputChannel.debug('[Scratchpad] All editors closed โ€” shutting down worker'); + shutdownEvaluator(); + } + }), + ); + + // Register CodeLens provider for scratchpad files + const codeLensProvider = new ScratchpadCodeLensProvider(); + ext.context.subscriptions.push(codeLensProvider); + ext.context.subscriptions.push( + vscode.languages.registerCodeLensProvider({ language: SCRATCHPAD_LANGUAGE_ID }, codeLensProvider), + ); + + // Register block highlighter for scratchpad files + const blockHighlighter = new ScratchpadBlockHighlighter(ext.context.extensionPath); + ext.context.subscriptions.push(blockHighlighter); + + //// Scratchpad Commands: + + registerCommandWithTreeNodeUnwrapping( + ScratchpadCommandIds.new, + withTreeNodeCommandCorrelation(newScratchpad), + ); + + registerCommandWithTreeNodeUnwrapping( + ScratchpadCommandIds.connect, + withTreeNodeCommandCorrelation(connectDatabase), + ); + + registerCommand(ScratchpadCommandIds.runAll, withCommandCorrelation(runAll)); + + registerCommand(ScratchpadCommandIds.runSelected, withCommandCorrelation(runSelected)); + + registerCommand('vscode-documentdb.command.clearSchemaCache', withCommandCorrelation(clearSchemaCache)); + + registerCommand( + 'vscode-documentdb.command.showSchemaStoreStats', + withCommandCorrelation(showSchemaStoreStats), + ); + //// General Commands: registerCommandWithTreeNodeUnwrapping( @@ -415,8 +516,6 @@ export class ClustersExtension implements vscode.Disposable { withTreeNodeCommandCorrelation(importDocuments), ); - registerScrapbookCommands(); - /** * Here, exporting documents is done in two ways: one is accessible from the tree view * via a context menu, and the other is accessible programmatically. Both of them diff --git a/src/documentdb/SchemaStore.test.ts b/src/documentdb/SchemaStore.test.ts new file mode 100644 index 000000000..e26422b38 --- /dev/null +++ b/src/documentdb/SchemaStore.test.ts @@ -0,0 +1,292 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ObjectId } from 'bson'; +import { SchemaStore, type SchemaChangeEvent } from './SchemaStore'; + +describe('SchemaStore', () => { + let store: SchemaStore; + + beforeEach(() => { + // Reset singleton between tests + SchemaStore.getInstance().dispose(); + store = SchemaStore.getInstance(); + }); + + afterEach(() => { + store.dispose(); + jest.useRealTimers(); + }); + + // โ”€โ”€ Helpers โ”€โ”€ + + const clusterId = 'cluster-1'; + const db = 'testDb'; + const coll = 'testColl'; + + function makeDocs(fields: Record[]) { + return fields.map((f) => ({ _id: new ObjectId(), ...f })); + } + + // โ”€โ”€ Read/Write operations โ”€โ”€ + + it('returns empty results for unknown keys', () => { + expect(store.hasSchema(clusterId, db, coll)).toBe(false); + expect(store.getKnownFields(clusterId, db, coll)).toEqual([]); + expect(store.getDocumentCount(clusterId, db, coll)).toBe(0); + expect(store.getSchema(clusterId, db, coll)).toEqual({ type: 'object' }); + expect(store.getPropertyNamesAtLevel(clusterId, db, coll, [])).toEqual([]); + }); + + it('creates analyzer and exposes fields after addDocuments', () => { + const docs = makeDocs([{ name: 'Alice', age: 30 }]); + store.addDocuments(clusterId, db, coll, docs); + + expect(store.hasSchema(clusterId, db, coll)).toBe(true); + expect(store.getDocumentCount(clusterId, db, coll)).toBe(1); + + const fields = store.getKnownFields(clusterId, db, coll); + const fieldNames = fields.map((f) => f.path); + expect(fieldNames).toContain('_id'); + expect(fieldNames).toContain('name'); + expect(fieldNames).toContain('age'); + }); + + it('accumulates schema across multiple addDocuments calls', () => { + store.addDocuments(clusterId, db, coll, makeDocs([{ name: 'Alice' }])); + store.addDocuments(clusterId, db, coll, makeDocs([{ email: 'a@b.com' }])); + + const fields = store.getKnownFields(clusterId, db, coll); + const fieldNames = fields.map((f) => f.path); + expect(fieldNames).toContain('name'); + expect(fieldNames).toContain('email'); + expect(store.getDocumentCount(clusterId, db, coll)).toBe(2); + }); + + it('does not create analyzer for empty document array', () => { + store.addDocuments(clusterId, db, coll, []); + expect(store.hasSchema(clusterId, db, coll)).toBe(false); + }); + + it('returns property names at root level', () => { + store.addDocuments(clusterId, db, coll, makeDocs([{ name: 'Alice', age: 30 }])); + const props = store.getPropertyNamesAtLevel(clusterId, db, coll, []); + expect(props).toContain('_id'); + expect(props).toContain('name'); + expect(props).toContain('age'); + }); + + it('returns property names at nested level', () => { + store.addDocuments(clusterId, db, coll, makeDocs([{ address: { city: 'NYC', zip: '10001' } }])); + const props = store.getPropertyNamesAtLevel(clusterId, db, coll, ['address']); + expect(props).toContain('city'); + expect(props).toContain('zip'); + }); + + // โ”€โ”€ Multiple keys are independent โ”€โ”€ + + it('keeps schemas independent per collection', () => { + store.addDocuments(clusterId, db, 'users', makeDocs([{ name: 'Alice' }])); + store.addDocuments(clusterId, db, 'orders', makeDocs([{ total: 99 }])); + + const userFields = store.getKnownFields(clusterId, db, 'users').map((f) => f.path); + const orderFields = store.getKnownFields(clusterId, db, 'orders').map((f) => f.path); + + expect(userFields).toContain('name'); + expect(userFields).not.toContain('total'); + expect(orderFields).toContain('total'); + expect(orderFields).not.toContain('name'); + }); + + it('keeps schemas independent per cluster', () => { + store.addDocuments('cluster-a', db, coll, makeDocs([{ fieldA: 1 }])); + store.addDocuments('cluster-b', db, coll, makeDocs([{ fieldB: 2 }])); + + const fieldsA = store.getKnownFields('cluster-a', db, coll).map((f) => f.path); + const fieldsB = store.getKnownFields('cluster-b', db, coll).map((f) => f.path); + + expect(fieldsA).toContain('fieldA'); + expect(fieldsA).not.toContain('fieldB'); + expect(fieldsB).toContain('fieldB'); + expect(fieldsB).not.toContain('fieldA'); + }); + + // โ”€โ”€ Clear operations โ”€โ”€ + + it('clearSchema removes a single collection', () => { + store.addDocuments(clusterId, db, 'users', makeDocs([{ name: 'Alice' }])); + store.addDocuments(clusterId, db, 'orders', makeDocs([{ total: 99 }])); + + store.clearSchema(clusterId, db, 'users'); + + expect(store.hasSchema(clusterId, db, 'users')).toBe(false); + expect(store.hasSchema(clusterId, db, 'orders')).toBe(true); + }); + + it('clearSchema is a no-op for unknown keys', () => { + // Should not throw + store.clearSchema(clusterId, db, 'nonexistent'); + expect(store.hasSchema(clusterId, db, 'nonexistent')).toBe(false); + }); + + it('clearCluster removes all schemas for a cluster', () => { + store.addDocuments(clusterId, db, 'users', makeDocs([{ name: 'Alice' }])); + store.addDocuments(clusterId, db, 'orders', makeDocs([{ total: 99 }])); + store.addDocuments('other-cluster', db, 'users', makeDocs([{ name: 'Bob' }])); + + store.clearCluster(clusterId); + + expect(store.hasSchema(clusterId, db, 'users')).toBe(false); + expect(store.hasSchema(clusterId, db, 'orders')).toBe(false); + expect(store.hasSchema('other-cluster', db, 'users')).toBe(true); + }); + + it('clearDatabase removes schemas for a specific database', () => { + store.addDocuments(clusterId, 'db1', 'users', makeDocs([{ name: 'Alice' }])); + store.addDocuments(clusterId, 'db1', 'orders', makeDocs([{ total: 99 }])); + store.addDocuments(clusterId, 'db2', 'products', makeDocs([{ sku: 'X' }])); + + store.clearDatabase(clusterId, 'db1'); + + expect(store.hasSchema(clusterId, 'db1', 'users')).toBe(false); + expect(store.hasSchema(clusterId, 'db1', 'orders')).toBe(false); + expect(store.hasSchema(clusterId, 'db2', 'products')).toBe(true); + }); + + it('reset clears everything', () => { + store.addDocuments('c1', db, 'a', makeDocs([{ x: 1 }])); + store.addDocuments('c2', db, 'b', makeDocs([{ y: 2 }])); + + store.reset(); + + expect(store.hasSchema('c1', db, 'a')).toBe(false); + expect(store.hasSchema('c2', db, 'b')).toBe(false); + }); + + // โ”€โ”€ Singleton โ”€โ”€ + + it('getInstance returns the same instance', () => { + const a = SchemaStore.getInstance(); + const b = SchemaStore.getInstance(); + expect(a).toBe(b); + }); + + it('dispose resets the singleton', () => { + const before = SchemaStore.getInstance(); + before.dispose(); + const after = SchemaStore.getInstance(); + expect(after).not.toBe(before); + }); + + // โ”€โ”€ Events (debounced) โ”€โ”€ + + describe('onDidChangeSchema', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + it('fires after debounce delay on addDocuments', () => { + const events: SchemaChangeEvent[] = []; + store.onDidChangeSchema((e) => events.push(e)); + + store.addDocuments(clusterId, db, coll, makeDocs([{ name: 'Alice' }])); + + // Should not have fired yet + expect(events).toHaveLength(0); + + // Advance past the 1-second debounce + jest.advanceTimersByTime(1000); + + expect(events).toHaveLength(1); + expect(events[0]).toEqual({ + clusterId, + databaseName: db, + collectionName: coll, + }); + }); + + it('coalesces rapid addDocuments calls into a single event', () => { + const events: SchemaChangeEvent[] = []; + store.onDidChangeSchema((e) => events.push(e)); + + // Rapid successive calls + store.addDocuments(clusterId, db, coll, makeDocs([{ a: 1 }])); + store.addDocuments(clusterId, db, coll, makeDocs([{ b: 2 }])); + store.addDocuments(clusterId, db, coll, makeDocs([{ c: 3 }])); + + jest.advanceTimersByTime(1000); + + // Only one event, not three + expect(events).toHaveLength(1); + }); + + it('fires separate events for different collections', () => { + const events: SchemaChangeEvent[] = []; + store.onDidChangeSchema((e) => events.push(e)); + + store.addDocuments(clusterId, db, 'users', makeDocs([{ name: 'Alice' }])); + store.addDocuments(clusterId, db, 'orders', makeDocs([{ total: 99 }])); + + jest.advanceTimersByTime(1000); + + expect(events).toHaveLength(2); + expect(events.map((e) => e.collectionName)).toEqual(expect.arrayContaining(['users', 'orders'])); + }); + + it('fires immediately on clearSchema (not debounced)', () => { + store.addDocuments(clusterId, db, coll, makeDocs([{ name: 'Alice' }])); + jest.advanceTimersByTime(1000); // flush addDocuments event + + const events: SchemaChangeEvent[] = []; + store.onDidChangeSchema((e) => events.push(e)); + + store.clearSchema(clusterId, db, coll); + + // Should fire immediately, not after debounce + expect(events).toHaveLength(1); + expect(events[0]).toEqual({ + clusterId, + databaseName: db, + collectionName: coll, + }); + }); + + it('does not fire clearSchema for unknown keys', () => { + const events: SchemaChangeEvent[] = []; + store.onDidChangeSchema((e) => events.push(e)); + + store.clearSchema(clusterId, db, 'nonexistent'); + + expect(events).toHaveLength(0); + }); + + it('does not fire on addDocuments with empty array', () => { + const events: SchemaChangeEvent[] = []; + store.onDidChangeSchema((e) => events.push(e)); + + store.addDocuments(clusterId, db, coll, []); + + jest.advanceTimersByTime(1000); + + expect(events).toHaveLength(0); + }); + + it('cancels pending debounced event when clearSchema is called', () => { + const events: SchemaChangeEvent[] = []; + store.onDidChangeSchema((e) => events.push(e)); + + store.addDocuments(clusterId, db, coll, makeDocs([{ name: 'Alice' }])); + // Pending debounced event exists + store.clearSchema(clusterId, db, coll); + + // clearSchema fires immediately + expect(events).toHaveLength(1); + + // Advance timers โ€” no additional event from the cancelled addDocuments debounce + jest.advanceTimersByTime(1000); + expect(events).toHaveLength(1); + }); + }); +}); diff --git a/src/documentdb/SchemaStore.ts b/src/documentdb/SchemaStore.ts new file mode 100644 index 000000000..bc20296c3 --- /dev/null +++ b/src/documentdb/SchemaStore.ts @@ -0,0 +1,288 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; +import { + SchemaAnalyzer, + getPropertyNamesAtLevel, + type FieldEntry, + type JSONSchema, +} from '@vscode-documentdb/schema-analyzer'; +import * as vscode from 'vscode'; + +import { type Document, type WithId } from 'mongodb'; +import { ext } from '../extensionVariables'; + +export interface SchemaChangeEvent { + readonly clusterId: string; + readonly databaseName: string; + readonly collectionName: string; +} + +export interface SchemaStoreStats { + /** Number of collections with cached schema. */ + readonly collectionCount: number; + /** Total documents analyzed across all collections. */ + readonly totalDocuments: number; + /** Total known fields across all collections. */ + readonly totalFields: number; + /** Per-collection breakdown. */ + readonly collections: ReadonlyArray<{ + readonly key: string; + readonly documentCount: number; + readonly fieldCount: number; + }>; +} + +/** + * Shared, cluster-scoped schema cache. + * + * Accumulates schema data per `{clusterId, databaseName, collectionName}` triple, + * enabling cross-tab and scratchpad schema sharing. All schema consumers + * (Collection View tabs, scratchpad, future shell) read from and contribute + * to the same store. + * + * Schema change notifications are debounced per key (1 second) to avoid + * excessive churn when pages are navigated rapidly. + */ +export class SchemaStore implements vscode.Disposable { + private static _instance: SchemaStore | undefined; + private readonly _analyzers = new Map(); + private readonly _onDidChangeSchema = new vscode.EventEmitter(); + private readonly _pendingNotifications = new Map>(); + + /** High-water marks for telemetry โ€” tracks peak usage across the session. */ + private _maxCollectionCount = 0; + private _maxTotalDocuments = 0; + private _maxTotalFields = 0; + private _statsChanged = false; + + /** Fires when schema data changes for any collection (debounced, 1 second). */ + public readonly onDidChangeSchema: vscode.Event = this._onDidChangeSchema.event; + + /** Get the singleton instance. */ + public static getInstance(): SchemaStore { + if (!SchemaStore._instance) { + SchemaStore._instance = new SchemaStore(); + } + return SchemaStore._instance; + } + + // โ”€โ”€ Key construction โ”€โ”€ + + private static key(clusterId: string, db: string, coll: string): string { + return `${clusterId}::${db}::${coll}`; + } + + // โ”€โ”€ Read operations โ”€โ”€ + + /** Check if schema data exists for a collection. */ + public hasSchema(clusterId: string, db: string, coll: string): boolean { + const analyzer = this._analyzers.get(SchemaStore.key(clusterId, db, coll)); + return analyzer !== undefined && analyzer.getDocumentCount() > 0; + } + + /** Get known fields for a collection (empty array if no schema). */ + public getKnownFields(clusterId: string, db: string, coll: string): FieldEntry[] { + const analyzer = this._analyzers.get(SchemaStore.key(clusterId, db, coll)); + return analyzer?.getKnownFields() ?? []; + } + + /** Get the raw JSON Schema for a collection (empty schema if none). */ + public getSchema(clusterId: string, db: string, coll: string): JSONSchema { + const analyzer = this._analyzers.get(SchemaStore.key(clusterId, db, coll)); + return analyzer?.getSchema() ?? { type: 'object' }; + } + + /** Get schema document count for a collection. */ + public getDocumentCount(clusterId: string, db: string, coll: string): number { + return this._analyzers.get(SchemaStore.key(clusterId, db, coll))?.getDocumentCount() ?? 0; + } + + /** Get property names at a given schema path (for table headers). */ + public getPropertyNamesAtLevel(clusterId: string, db: string, coll: string, path: string[]): string[] { + const schema = this.getSchema(clusterId, db, coll); + return getPropertyNamesAtLevel(schema, path); + } + + // โ”€โ”€ Stats & telemetry โ”€โ”€ + + /** Get a snapshot of current schema store statistics. */ + public getStats(): SchemaStoreStats { + let totalDocuments = 0; + let totalFields = 0; + const collections: Array<{ key: string; documentCount: number; fieldCount: number }> = []; + + for (const [key, analyzer] of this._analyzers) { + const docCount = analyzer.getDocumentCount(); + const fieldCount = analyzer.getKnownFields().length; + totalDocuments += docCount; + totalFields += fieldCount; + collections.push({ key, documentCount: docCount, fieldCount }); + } + + return { + collectionCount: this._analyzers.size, + totalDocuments, + totalFields, + collections, + }; + } + + /** Log current stats to the output channel. */ + public logStats(): void { + const stats = this.getStats(); + ext.outputChannel?.appendLog( + `[SchemaStore] ${String(stats.collectionCount)} collections cached, ` + + `${String(stats.totalDocuments)} documents analyzed, ` + + `${String(stats.totalFields)} fields discovered`, + ); + for (const c of stats.collections) { + ext.outputChannel?.trace( + `[SchemaStore] ${c.key}: ${String(c.documentCount)} docs, ${String(c.fieldCount)} fields`, + ); + } + } + + /** Report peak usage to telemetry (called on dispose or periodically). */ + private _reportTelemetry(): void { + if (!this._statsChanged) { + return; + } + this._statsChanged = false; + + void callWithTelemetryAndErrorHandling('schemaStore.stats', (ctx) => { + ctx.errorHandling.suppressDisplay = true; + ctx.errorHandling.rethrow = false; + ctx.telemetry.measurements.maxCollectionCount = this._maxCollectionCount; + ctx.telemetry.measurements.maxTotalDocuments = this._maxTotalDocuments; + ctx.telemetry.measurements.maxTotalFields = this._maxTotalFields; + + // Current snapshot + const stats = this.getStats(); + ctx.telemetry.measurements.currentCollectionCount = stats.collectionCount; + ctx.telemetry.measurements.currentTotalDocuments = stats.totalDocuments; + ctx.telemetry.measurements.currentTotalFields = stats.totalFields; + }); + } + + /** Update high-water marks after schema changes. */ + private _updateMaxStats(): void { + const stats = this.getStats(); + if ( + stats.collectionCount > this._maxCollectionCount || + stats.totalDocuments > this._maxTotalDocuments || + stats.totalFields > this._maxTotalFields + ) { + this._maxCollectionCount = Math.max(this._maxCollectionCount, stats.collectionCount); + this._maxTotalDocuments = Math.max(this._maxTotalDocuments, stats.totalDocuments); + this._maxTotalFields = Math.max(this._maxTotalFields, stats.totalFields); + this._statsChanged = true; + } + } + + // โ”€โ”€ Write operations โ”€โ”€ + + /** Feed documents to the schema store (from any source). */ + public addDocuments(clusterId: string, db: string, coll: string, documents: ReadonlyArray>): void { + if (documents.length === 0) return; + + const key = SchemaStore.key(clusterId, db, coll); + let analyzer = this._analyzers.get(key); + if (!analyzer) { + analyzer = new SchemaAnalyzer(); + this._analyzers.set(key, analyzer); + } + + analyzer.addDocuments(documents); + this._updateMaxStats(); + this._fireSchemaChanged(key, { clusterId, databaseName: db, collectionName: coll }); + } + + // โ”€โ”€ Lifecycle โ”€โ”€ + + /** Clear schema for a specific collection (e.g., after collection drop). Fires immediately (not debounced). */ + public clearSchema(clusterId: string, db: string, coll: string): void { + const key = SchemaStore.key(clusterId, db, coll); + if (this._analyzers.delete(key)) { + // Cancel any pending debounced notification for this key + const pending = this._pendingNotifications.get(key); + if (pending !== undefined) { + clearTimeout(pending); + this._pendingNotifications.delete(key); + } + this._onDidChangeSchema.fire({ clusterId, databaseName: db, collectionName: coll }); + } + } + + /** Clear all schemas for a cluster (e.g., on disconnect). */ + public clearCluster(clusterId: string): void { + const prefix = `${clusterId}::`; + for (const key of this._analyzers.keys()) { + if (key.startsWith(prefix)) { + this._analyzers.delete(key); + const pending = this._pendingNotifications.get(key); + if (pending !== undefined) { + clearTimeout(pending); + this._pendingNotifications.delete(key); + } + } + } + } + + /** Clear all schemas for a database within a cluster (e.g., on database drop). */ + public clearDatabase(clusterId: string, db: string): void { + const prefix = `${clusterId}::${db}::`; + for (const key of this._analyzers.keys()) { + if (key.startsWith(prefix)) { + this._analyzers.delete(key); + const pending = this._pendingNotifications.get(key); + if (pending !== undefined) { + clearTimeout(pending); + this._pendingNotifications.delete(key); + } + } + } + } + + /** Clear all schemas (e.g., for testing). */ + public reset(): void { + this._analyzers.clear(); + for (const timer of this._pendingNotifications.values()) { + clearTimeout(timer); + } + this._pendingNotifications.clear(); + } + + public dispose(): void { + // Report peak stats to telemetry before teardown + this._reportTelemetry(); + this.logStats(); + + for (const timer of this._pendingNotifications.values()) { + clearTimeout(timer); + } + this._pendingNotifications.clear(); + this._onDidChangeSchema.dispose(); + this._analyzers.clear(); + SchemaStore._instance = undefined; + } + + // โ”€โ”€ Debounced notification (1 second per key) โ”€โ”€ + + private _fireSchemaChanged(key: string, event: SchemaChangeEvent): void { + const existing = this._pendingNotifications.get(key); + if (existing !== undefined) { + clearTimeout(existing); + } + this._pendingNotifications.set( + key, + setTimeout(() => { + this._pendingNotifications.delete(key); + this._onDidChangeSchema.fire(event); + }, 1000), + ); + } +} diff --git a/src/documentdb/scrapbook/connectToClient.ts b/src/documentdb/connectToClient.ts similarity index 94% rename from src/documentdb/scrapbook/connectToClient.ts rename to src/documentdb/connectToClient.ts index 25fe24c4b..2f20a9d6f 100644 --- a/src/documentdb/scrapbook/connectToClient.ts +++ b/src/documentdb/connectToClient.ts @@ -5,8 +5,8 @@ import * as l10n from '@vscode/l10n'; import { MongoClient, type MongoClientOptions } from 'mongodb'; -import { Links, wellKnownEmulatorPassword } from '../../constants'; -import { type EmulatorConfiguration } from '../../utils/emulatorConfiguration'; +import { Links, wellKnownEmulatorPassword } from '../constants'; +import { type EmulatorConfiguration } from '../utils/emulatorConfiguration'; export async function connectToClient( connectionString: string, diff --git a/src/documentdb/grammar/mongo.tokens b/src/documentdb/grammar/mongo.tokens deleted file mode 100644 index c54199908..000000000 --- a/src/documentdb/grammar/mongo.tokens +++ /dev/null @@ -1,36 +0,0 @@ -T__0=1 -T__1=2 -T__2=3 -T__3=4 -T__4=5 -T__5=6 -T__6=7 -T__7=8 -RegexLiteral=9 -SingleLineComment=10 -MultiLineComment=11 -StringLiteral=12 -NullLiteral=13 -BooleanLiteral=14 -NumericLiteral=15 -DecimalLiteral=16 -LineTerminator=17 -SEMICOLON=18 -DOT=19 -DB=20 -IDENTIFIER=21 -DOUBLE_QUOTED_STRING_LITERAL=22 -SINGLE_QUOTED_STRING_LITERAL=23 -WHITESPACE=24 -'('=1 -','=2 -')'=3 -'{'=4 -'}'=5 -'['=6 -']'=7 -':'=8 -'null'=13 -';'=18 -'.'=19 -'db'=20 diff --git a/src/documentdb/grammar/mongoLexer.tokens b/src/documentdb/grammar/mongoLexer.tokens deleted file mode 100644 index c54199908..000000000 --- a/src/documentdb/grammar/mongoLexer.tokens +++ /dev/null @@ -1,36 +0,0 @@ -T__0=1 -T__1=2 -T__2=3 -T__3=4 -T__4=5 -T__5=6 -T__6=7 -T__7=8 -RegexLiteral=9 -SingleLineComment=10 -MultiLineComment=11 -StringLiteral=12 -NullLiteral=13 -BooleanLiteral=14 -NumericLiteral=15 -DecimalLiteral=16 -LineTerminator=17 -SEMICOLON=18 -DOT=19 -DB=20 -IDENTIFIER=21 -DOUBLE_QUOTED_STRING_LITERAL=22 -SINGLE_QUOTED_STRING_LITERAL=23 -WHITESPACE=24 -'('=1 -','=2 -')'=3 -'{'=4 -'}'=5 -'['=6 -']'=7 -':'=8 -'null'=13 -';'=18 -'.'=19 -'db'=20 diff --git a/src/documentdb/grammar/mongoLexer.ts b/src/documentdb/grammar/mongoLexer.ts deleted file mode 100644 index 6b32a6a99..000000000 --- a/src/documentdb/grammar/mongoLexer.ts +++ /dev/null @@ -1,316 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Generated from ./grammar/mongo.g4 by ANTLR 4.6-SNAPSHOT - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// This is legacy code that we are not maintaining for Typescript 4 -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck - -import { type ATN } from 'antlr4ts/atn/ATN'; -import { ATNDeserializer } from 'antlr4ts/atn/ATNDeserializer'; -import { LexerATNSimulator } from 'antlr4ts/atn/LexerATNSimulator'; -import { type CharStream } from 'antlr4ts/CharStream'; -import { NotNull, Override } from 'antlr4ts/Decorators'; -import { Lexer } from 'antlr4ts/Lexer'; -import * as Utils from 'antlr4ts/misc/Utils'; -import { type RuleContext } from 'antlr4ts/RuleContext'; -import { type Vocabulary } from 'antlr4ts/Vocabulary'; -import { VocabularyImpl } from 'antlr4ts/VocabularyImpl'; - -export class mongoLexer extends Lexer { - public static readonly T__0 = 1; - public static readonly T__1 = 2; - public static readonly T__2 = 3; - public static readonly T__3 = 4; - public static readonly T__4 = 5; - public static readonly T__5 = 6; - public static readonly T__6 = 7; - public static readonly T__7 = 8; - public static readonly RegexLiteral = 9; - public static readonly SingleLineComment = 10; - public static readonly MultiLineComment = 11; - public static readonly StringLiteral = 12; - public static readonly NullLiteral = 13; - public static readonly BooleanLiteral = 14; - public static readonly NumericLiteral = 15; - public static readonly DecimalLiteral = 16; - public static readonly LineTerminator = 17; - public static readonly SEMICOLON = 18; - public static readonly DOT = 19; - public static readonly DB = 20; - public static readonly IDENTIFIER = 21; - public static readonly DOUBLE_QUOTED_STRING_LITERAL = 22; - public static readonly SINGLE_QUOTED_STRING_LITERAL = 23; - public static readonly WHITESPACE = 24; - public static readonly modeNames: string[] = ['DEFAULT_MODE']; - - public static readonly ruleNames: string[] = [ - 'T__0', - 'T__1', - 'T__2', - 'T__3', - 'T__4', - 'T__5', - 'T__6', - 'T__7', - 'RegexLiteral', - 'RegexFlag', - 'SingleLineComment', - 'MultiLineComment', - 'StringLiteral', - 'NullLiteral', - 'BooleanLiteral', - 'NumericLiteral', - 'DecimalLiteral', - 'LineTerminator', - 'SEMICOLON', - 'DOT', - 'DB', - 'IDENTIFIER', - 'DOUBLE_QUOTED_STRING_LITERAL', - 'SINGLE_QUOTED_STRING_LITERAL', - 'STRING_ESCAPE', - 'DecimalIntegerLiteral', - 'ExponentPart', - 'DecimalDigit', - 'WHITESPACE', - ]; - - private static readonly _LITERAL_NAMES: (string | undefined)[] = [ - undefined, - "'('", - "','", - "')'", - "'{'", - "'}'", - "'['", - "']'", - "':'", - undefined, - undefined, - undefined, - undefined, - "'null'", - undefined, - undefined, - undefined, - undefined, - "';'", - "'.'", - "'db'", - ]; - private static readonly _SYMBOLIC_NAMES: (string | undefined)[] = [ - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - 'RegexLiteral', - 'SingleLineComment', - 'MultiLineComment', - 'StringLiteral', - 'NullLiteral', - 'BooleanLiteral', - 'NumericLiteral', - 'DecimalLiteral', - 'LineTerminator', - 'SEMICOLON', - 'DOT', - 'DB', - 'IDENTIFIER', - 'DOUBLE_QUOTED_STRING_LITERAL', - 'SINGLE_QUOTED_STRING_LITERAL', - 'WHITESPACE', - ]; - public static readonly VOCABULARY: Vocabulary = new VocabularyImpl( - mongoLexer._LITERAL_NAMES, - mongoLexer._SYMBOLIC_NAMES, - [], - ); - - @Override - @NotNull - public get vocabulary(): Vocabulary { - return mongoLexer.VOCABULARY; - } - - private isExternalIdentifierText(text) { - return text === 'db'; - } - - constructor(input: CharStream) { - super(input); - this._interp = new LexerATNSimulator(mongoLexer._ATN, this); - } - - @Override - public get grammarFileName(): string { - return 'mongo.g4'; - } - - @Override - public get ruleNames(): string[] { - return mongoLexer.ruleNames; - } - - @Override - public get serializedATN(): string { - return mongoLexer._serializedATN; - } - - @Override - public get modeNames(): string[] { - return mongoLexer.modeNames; - } - - @Override - public sempred(_localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { - switch (ruleIndex) { - case 21: - return this.IDENTIFIER_sempred(_localctx, predIndex); - } - return true; - } - private IDENTIFIER_sempred(_localctx: RuleContext, predIndex: number): boolean { - switch (predIndex) { - case 0: - return !this.isExternalIdentifierText(this.text); - } - return true; - } - - public static readonly _serializedATN: string = - '\x03\uAF6F\u8320\u479D\uB75C\u4880\u1605\u191C\uAB37\x02\x1A\xF2\b\x01' + - '\x04\x02\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06' + - '\x04\x07\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r' + - '\t\r\x04\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04\x12\t' + - '\x12\x04\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04\x16\t\x16\x04\x17\t' + - '\x17\x04\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B\x04\x1C\t' + - '\x1C\x04\x1D\t\x1D\x04\x1E\t\x1E\x03\x02\x03\x02\x03\x03\x03\x03\x03\x04' + - '\x03\x04\x03\x05\x03\x05\x03\x06\x03\x06\x03\x07\x03\x07\x03\b\x03\b\x03' + - '\t\x03\t\x03\n\x03\n\x03\n\x03\n\x05\nR\n\n\x03\n\x03\n\x03\n\x07\nW\n' + - '\n\f\n\x0E\nZ\v\n\x03\n\x03\n\x07\n^\n\n\f\n\x0E\na\v\n\x03\v\x03\v\x03' + - '\f\x03\f\x03\f\x03\f\x07\fi\n\f\f\f\x0E\fl\v\f\x03\f\x03\f\x03\r\x03\r' + - '\x03\r\x03\r\x07\rt\n\r\f\r\x0E\rw\v\r\x03\r\x03\r\x03\r\x03\r\x03\r\x03' + - '\x0E\x03\x0E\x05\x0E\x80\n\x0E\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F' + - '\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10' + - '\x05\x10\x90\n\x10\x03\x11\x05\x11\x93\n\x11\x03\x11\x03\x11\x03\x12\x03' + - '\x12\x03\x12\x06\x12\x9A\n\x12\r\x12\x0E\x12\x9B\x03\x12\x05\x12\x9F\n' + - '\x12\x03\x12\x03\x12\x06\x12\xA3\n\x12\r\x12\x0E\x12\xA4\x03\x12\x05\x12' + - '\xA8\n\x12\x03\x12\x03\x12\x05\x12\xAC\n\x12\x05\x12\xAE\n\x12\x03\x13' + - '\x03\x13\x03\x13\x03\x13\x03\x14\x03\x14\x03\x15\x03\x15\x03\x16\x03\x16' + - '\x03\x16\x03\x17\x03\x17\x06\x17\xBD\n\x17\r\x17\x0E\x17\xBE\x03\x17\x03' + - '\x17\x03\x18\x03\x18\x03\x18\x07\x18\xC6\n\x18\f\x18\x0E\x18\xC9\v\x18' + - '\x03\x18\x03\x18\x03\x19\x03\x19\x03\x19\x07\x19\xD0\n\x19\f\x19\x0E\x19' + - '\xD3\v\x19\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1A\x03\x1B\x03\x1B\x03' + - '\x1B\x07\x1B\xDD\n\x1B\f\x1B\x0E\x1B\xE0\v\x1B\x05\x1B\xE2\n\x1B\x03\x1C' + - '\x03\x1C\x05\x1C\xE6\n\x1C\x03\x1C\x06\x1C\xE9\n\x1C\r\x1C\x0E\x1C\xEA' + - '\x03\x1D\x03\x1D\x03\x1E\x03\x1E\x03\x1E\x03\x1E\x03u\x02\x02\x1F\x03' + - '\x02\x03\x05\x02\x04\x07\x02\x05\t\x02\x06\v\x02\x07\r\x02\b\x0F\x02\t' + - '\x11\x02\n\x13\x02\v\x15\x02\x02\x17\x02\f\x19\x02\r\x1B\x02\x0E\x1D\x02' + - "\x0F\x1F\x02\x10!\x02\x11#\x02\x12%\x02\x13'\x02\x14)\x02\x15+\x02\x16" + - '-\x02\x17/\x02\x181\x02\x193\x02\x025\x02\x027\x02\x029\x02\x02;\x02\x1A' + - '\x03\x02\x0F\x06\x02\f\f\x0F\x0F,,11\x05\x02\f\f\x0F\x0F11\x07\x02iik' + - 'kooww{{\x05\x02\f\f\x0F\x0F\u202A\u202B\f\x02\v\f\x0F\x0F""$$)+.0<=' + - ']_}}\x7F\x7F\x04\x02$$^^\x04\x02))^^\x05\x02$$))^^\x03\x023;\x04\x02G' + - 'Ggg\x04\x02--//\x03\x022;\x04\x02\v\v""\u0106\x02\x03\x03\x02\x02\x02' + - '\x02\x05\x03\x02\x02\x02\x02\x07\x03\x02\x02\x02\x02\t\x03\x02\x02\x02' + - '\x02\v\x03\x02\x02\x02\x02\r\x03\x02\x02\x02\x02\x0F\x03\x02\x02\x02\x02' + - '\x11\x03\x02\x02\x02\x02\x13\x03\x02\x02\x02\x02\x17\x03\x02\x02\x02\x02' + - '\x19\x03\x02\x02\x02\x02\x1B\x03\x02\x02\x02\x02\x1D\x03\x02\x02\x02\x02' + - '\x1F\x03\x02\x02\x02\x02!\x03\x02\x02\x02\x02#\x03\x02\x02\x02\x02%\x03' + - "\x02\x02\x02\x02'\x03\x02\x02\x02\x02)\x03\x02\x02\x02\x02+\x03\x02\x02" + - '\x02\x02-\x03\x02\x02\x02\x02/\x03\x02\x02\x02\x021\x03\x02\x02\x02\x02' + - ';\x03\x02\x02\x02\x03=\x03\x02\x02\x02\x05?\x03\x02\x02\x02\x07A\x03\x02' + - '\x02\x02\tC\x03\x02\x02\x02\vE\x03\x02\x02\x02\rG\x03\x02\x02\x02\x0F' + - 'I\x03\x02\x02\x02\x11K\x03\x02\x02\x02\x13M\x03\x02\x02\x02\x15b\x03\x02' + - '\x02\x02\x17d\x03\x02\x02\x02\x19o\x03\x02\x02\x02\x1B\x7F\x03\x02\x02' + - '\x02\x1D\x81\x03\x02\x02\x02\x1F\x8F\x03\x02\x02\x02!\x92\x03\x02\x02' + - "\x02#\xAD\x03\x02\x02\x02%\xAF\x03\x02\x02\x02'\xB3\x03\x02\x02\x02)" + - '\xB5\x03\x02\x02\x02+\xB7\x03\x02\x02\x02-\xBC\x03\x02\x02\x02/\xC2\x03' + - '\x02\x02\x021\xCC\x03\x02\x02\x023\xD6\x03\x02\x02\x025\xE1\x03\x02\x02' + - '\x027\xE3\x03\x02\x02\x029\xEC\x03\x02\x02\x02;\xEE\x03\x02\x02\x02=>' + - '\x07*\x02\x02>\x04\x03\x02\x02\x02?@\x07.\x02\x02@\x06\x03\x02\x02\x02' + - 'AB\x07+\x02\x02B\b\x03\x02\x02\x02CD\x07}\x02\x02D\n\x03\x02\x02\x02E' + - 'F\x07\x7F\x02\x02F\f\x03\x02\x02\x02GH\x07]\x02\x02H\x0E\x03\x02\x02\x02' + - 'IJ\x07_\x02\x02J\x10\x03\x02\x02\x02KL\x07<\x02\x02L\x12\x03\x02\x02\x02' + - 'MQ\x071\x02\x02NR\n\x02\x02\x02OP\x07^\x02\x02PR\x071\x02\x02QN\x03\x02' + - '\x02\x02QO\x03\x02\x02\x02RX\x03\x02\x02\x02SW\n\x03\x02\x02TU\x07^\x02' + - '\x02UW\x071\x02\x02VS\x03\x02\x02\x02VT\x03\x02\x02\x02WZ\x03\x02\x02' + - '\x02XV\x03\x02\x02\x02XY\x03\x02\x02\x02Y[\x03\x02\x02\x02ZX\x03\x02\x02' + - '\x02[_\x071\x02\x02\\^\x05\x15\v\x02]\\\x03\x02\x02\x02^a\x03\x02\x02' + - '\x02_]\x03\x02\x02\x02_`\x03\x02\x02\x02`\x14\x03\x02\x02\x02a_\x03\x02' + - '\x02\x02bc\t\x04\x02\x02c\x16\x03\x02\x02\x02de\x071\x02\x02ef\x071\x02' + - '\x02fj\x03\x02\x02\x02gi\n\x05\x02\x02hg\x03\x02\x02\x02il\x03\x02\x02' + - '\x02jh\x03\x02\x02\x02jk\x03\x02\x02\x02km\x03\x02\x02\x02lj\x03\x02\x02' + - '\x02mn\b\f\x02\x02n\x18\x03\x02\x02\x02op\x071\x02\x02pq\x07,\x02\x02' + - 'qu\x03\x02\x02\x02rt\v\x02\x02\x02sr\x03\x02\x02\x02tw\x03\x02\x02\x02' + - 'uv\x03\x02\x02\x02us\x03\x02\x02\x02vx\x03\x02\x02\x02wu\x03\x02\x02\x02' + - 'xy\x07,\x02\x02yz\x071\x02\x02z{\x03\x02\x02\x02{|\b\r\x02\x02|\x1A\x03' + - '\x02\x02\x02}\x80\x051\x19\x02~\x80\x05/\x18\x02\x7F}\x03\x02\x02\x02' + - '\x7F~\x03\x02\x02\x02\x80\x1C\x03\x02\x02\x02\x81\x82\x07p\x02\x02\x82' + - '\x83\x07w\x02\x02\x83\x84\x07n\x02\x02\x84\x85\x07n\x02\x02\x85\x1E\x03' + - '\x02\x02\x02\x86\x87\x07v\x02\x02\x87\x88\x07t\x02\x02\x88\x89\x07w\x02' + - '\x02\x89\x90\x07g\x02\x02\x8A\x8B\x07h\x02\x02\x8B\x8C\x07c\x02\x02\x8C' + - '\x8D\x07n\x02\x02\x8D\x8E\x07u\x02\x02\x8E\x90\x07g\x02\x02\x8F\x86\x03' + - '\x02\x02\x02\x8F\x8A\x03\x02\x02\x02\x90 \x03\x02\x02\x02\x91\x93\x07' + - '/\x02\x02\x92\x91\x03\x02\x02\x02\x92\x93\x03\x02\x02\x02\x93\x94\x03' + - '\x02\x02\x02\x94\x95\x05#\x12\x02\x95"\x03\x02\x02\x02\x96\x97\x055\x1B' + - '\x02\x97\x99\x070\x02\x02\x98\x9A\x059\x1D\x02\x99\x98\x03\x02\x02\x02' + - '\x9A\x9B\x03\x02\x02\x02\x9B\x99\x03\x02\x02\x02\x9B\x9C\x03\x02\x02\x02' + - '\x9C\x9E\x03\x02\x02\x02\x9D\x9F\x057\x1C\x02\x9E\x9D\x03\x02\x02\x02' + - '\x9E\x9F\x03\x02\x02\x02\x9F\xAE\x03\x02\x02\x02\xA0\xA2\x070\x02\x02' + - '\xA1\xA3\x059\x1D\x02\xA2\xA1\x03\x02\x02\x02\xA3\xA4\x03\x02\x02\x02' + - '\xA4\xA2\x03\x02\x02\x02\xA4\xA5\x03\x02\x02\x02\xA5\xA7\x03\x02\x02\x02' + - '\xA6\xA8\x057\x1C\x02\xA7\xA6\x03\x02\x02\x02\xA7\xA8\x03\x02\x02\x02' + - '\xA8\xAE\x03\x02\x02\x02\xA9\xAB\x055\x1B\x02\xAA\xAC\x057\x1C\x02\xAB' + - '\xAA\x03\x02\x02\x02\xAB\xAC\x03\x02\x02\x02\xAC\xAE\x03\x02\x02\x02\xAD' + - '\x96\x03\x02\x02\x02\xAD\xA0\x03\x02\x02\x02\xAD\xA9\x03\x02\x02\x02\xAE' + - '$\x03\x02\x02\x02\xAF\xB0\t\x05\x02\x02\xB0\xB1\x03\x02\x02\x02\xB1\xB2' + - '\b\x13\x02\x02\xB2&\x03\x02\x02\x02\xB3\xB4\x07=\x02\x02\xB4(\x03\x02' + - '\x02\x02\xB5\xB6\x070\x02\x02\xB6*\x03\x02\x02\x02\xB7\xB8\x07f\x02\x02' + - '\xB8\xB9\x07d\x02\x02\xB9,\x03\x02\x02\x02\xBA\xBD\n\x06\x02\x02\xBB\xBD' + - '\x053\x1A\x02\xBC\xBA\x03\x02\x02\x02\xBC\xBB\x03\x02\x02\x02\xBD\xBE' + - '\x03\x02\x02\x02\xBE\xBC\x03\x02\x02\x02\xBE\xBF\x03\x02\x02\x02\xBF\xC0' + - '\x03\x02\x02\x02\xC0\xC1\x06\x17\x02\x02\xC1.\x03\x02\x02\x02\xC2\xC7' + - '\x07$\x02\x02\xC3\xC6\n\x07\x02\x02\xC4\xC6\x053\x1A\x02\xC5\xC3\x03\x02' + - '\x02\x02\xC5\xC4\x03\x02\x02\x02\xC6\xC9\x03\x02\x02\x02\xC7\xC5\x03\x02' + - '\x02\x02\xC7\xC8\x03\x02\x02\x02\xC8\xCA\x03\x02\x02\x02\xC9\xC7\x03\x02' + - '\x02\x02\xCA\xCB\x07$\x02\x02\xCB0\x03\x02\x02\x02\xCC\xD1\x07)\x02\x02' + - '\xCD\xD0\n\b\x02\x02\xCE\xD0\x053\x1A\x02\xCF\xCD\x03\x02\x02\x02\xCF' + - '\xCE\x03\x02\x02\x02\xD0\xD3\x03\x02\x02\x02\xD1\xCF\x03\x02\x02\x02\xD1' + - '\xD2\x03\x02\x02\x02\xD2\xD4\x03\x02\x02\x02\xD3\xD1\x03\x02\x02\x02\xD4' + - '\xD5\x07)\x02\x02\xD52\x03\x02\x02\x02\xD6\xD7\x07^\x02\x02\xD7\xD8\t' + - '\t\x02\x02\xD84\x03\x02\x02\x02\xD9\xE2\x072\x02\x02\xDA\xDE\t\n\x02\x02' + - '\xDB\xDD\x059\x1D\x02\xDC\xDB\x03\x02\x02\x02\xDD\xE0\x03\x02\x02\x02' + - '\xDE\xDC\x03\x02\x02\x02\xDE\xDF\x03\x02\x02\x02\xDF\xE2\x03\x02\x02\x02' + - '\xE0\xDE\x03\x02\x02\x02\xE1\xD9\x03\x02\x02\x02\xE1\xDA\x03\x02\x02\x02' + - '\xE26\x03\x02\x02\x02\xE3\xE5\t\v\x02\x02\xE4\xE6\t\f\x02\x02\xE5\xE4' + - '\x03\x02\x02\x02\xE5\xE6\x03\x02\x02\x02\xE6\xE8\x03\x02\x02\x02\xE7\xE9' + - '\x059\x1D\x02\xE8\xE7\x03\x02\x02\x02\xE9\xEA\x03\x02\x02\x02\xEA\xE8' + - '\x03\x02\x02\x02\xEA\xEB\x03\x02\x02\x02\xEB8\x03\x02\x02\x02\xEC\xED' + - '\t\r\x02\x02\xED:\x03\x02\x02\x02\xEE\xEF\t\x0E\x02\x02\xEF\xF0\x03\x02' + - '\x02\x02\xF0\xF1\b\x1E\x03\x02\xF1<\x03\x02\x02\x02\x1C\x02QVX_ju\x7F' + - '\x8F\x92\x9B\x9E\xA4\xA7\xAB\xAD\xBC\xBE\xC5\xC7\xCF\xD1\xDE\xE1\xE5\xEA' + - '\x04\x02\x03\x02\b\x02\x02'; - public static __ATN: ATN; - public static get _ATN(): ATN { - if (!mongoLexer.__ATN) { - mongoLexer.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(mongoLexer._serializedATN)); - } - - return mongoLexer.__ATN; - } -} diff --git a/src/documentdb/grammar/mongoListener.ts b/src/documentdb/grammar/mongoListener.ts deleted file mode 100644 index bfbdd1170..000000000 --- a/src/documentdb/grammar/mongoListener.ts +++ /dev/null @@ -1,225 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Generated from ./grammar/mongo.g4 by ANTLR 4.6-SNAPSHOT - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type ParseTreeListener } from 'antlr4ts/tree/ParseTreeListener'; -import { - type ArgumentContext, - type ArgumentsContext, - type ArrayLiteralContext, - type CollectionContext, - type CommandContext, - type CommandsContext, - type CommentContext, - type ElementListContext, - type EmptyCommandContext, - type FunctionCallContext, - type LiteralContext, - type MongoCommandsContext, - type ObjectLiteralContext, - type PropertyAssignmentContext, - type PropertyNameAndValueListContext, - type PropertyNameContext, - type PropertyValueContext, -} from './mongoParser'; - -/** - * This interface defines a complete listener for a parse tree produced by - * `mongoParser`. - */ -export interface mongoListener extends ParseTreeListener { - /** - * Enter a parse tree produced by `mongoParser.mongoCommands`. - * @param ctx the parse tree - */ - enterMongoCommands?: (ctx: MongoCommandsContext) => void; - /** - * Exit a parse tree produced by `mongoParser.mongoCommands`. - * @param ctx the parse tree - */ - exitMongoCommands?: (ctx: MongoCommandsContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.commands`. - * @param ctx the parse tree - */ - enterCommands?: (ctx: CommandsContext) => void; - /** - * Exit a parse tree produced by `mongoParser.commands`. - * @param ctx the parse tree - */ - exitCommands?: (ctx: CommandsContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.command`. - * @param ctx the parse tree - */ - enterCommand?: (ctx: CommandContext) => void; - /** - * Exit a parse tree produced by `mongoParser.command`. - * @param ctx the parse tree - */ - exitCommand?: (ctx: CommandContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.emptyCommand`. - * @param ctx the parse tree - */ - enterEmptyCommand?: (ctx: EmptyCommandContext) => void; - /** - * Exit a parse tree produced by `mongoParser.emptyCommand`. - * @param ctx the parse tree - */ - exitEmptyCommand?: (ctx: EmptyCommandContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.collection`. - * @param ctx the parse tree - */ - enterCollection?: (ctx: CollectionContext) => void; - /** - * Exit a parse tree produced by `mongoParser.collection`. - * @param ctx the parse tree - */ - exitCollection?: (ctx: CollectionContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.functionCall`. - * @param ctx the parse tree - */ - enterFunctionCall?: (ctx: FunctionCallContext) => void; - /** - * Exit a parse tree produced by `mongoParser.functionCall`. - * @param ctx the parse tree - */ - exitFunctionCall?: (ctx: FunctionCallContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.arguments`. - * @param ctx the parse tree - */ - enterArguments?: (ctx: ArgumentsContext) => void; - /** - * Exit a parse tree produced by `mongoParser.arguments`. - * @param ctx the parse tree - */ - exitArguments?: (ctx: ArgumentsContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.argument`. - * @param ctx the parse tree - */ - enterArgument?: (ctx: ArgumentContext) => void; - /** - * Exit a parse tree produced by `mongoParser.argument`. - * @param ctx the parse tree - */ - exitArgument?: (ctx: ArgumentContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.objectLiteral`. - * @param ctx the parse tree - */ - enterObjectLiteral?: (ctx: ObjectLiteralContext) => void; - /** - * Exit a parse tree produced by `mongoParser.objectLiteral`. - * @param ctx the parse tree - */ - exitObjectLiteral?: (ctx: ObjectLiteralContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.arrayLiteral`. - * @param ctx the parse tree - */ - enterArrayLiteral?: (ctx: ArrayLiteralContext) => void; - /** - * Exit a parse tree produced by `mongoParser.arrayLiteral`. - * @param ctx the parse tree - */ - exitArrayLiteral?: (ctx: ArrayLiteralContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.elementList`. - * @param ctx the parse tree - */ - enterElementList?: (ctx: ElementListContext) => void; - /** - * Exit a parse tree produced by `mongoParser.elementList`. - * @param ctx the parse tree - */ - exitElementList?: (ctx: ElementListContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.propertyNameAndValueList`. - * @param ctx the parse tree - */ - enterPropertyNameAndValueList?: (ctx: PropertyNameAndValueListContext) => void; - /** - * Exit a parse tree produced by `mongoParser.propertyNameAndValueList`. - * @param ctx the parse tree - */ - exitPropertyNameAndValueList?: (ctx: PropertyNameAndValueListContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.propertyAssignment`. - * @param ctx the parse tree - */ - enterPropertyAssignment?: (ctx: PropertyAssignmentContext) => void; - /** - * Exit a parse tree produced by `mongoParser.propertyAssignment`. - * @param ctx the parse tree - */ - exitPropertyAssignment?: (ctx: PropertyAssignmentContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.propertyValue`. - * @param ctx the parse tree - */ - enterPropertyValue?: (ctx: PropertyValueContext) => void; - /** - * Exit a parse tree produced by `mongoParser.propertyValue`. - * @param ctx the parse tree - */ - exitPropertyValue?: (ctx: PropertyValueContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.literal`. - * @param ctx the parse tree - */ - enterLiteral?: (ctx: LiteralContext) => void; - /** - * Exit a parse tree produced by `mongoParser.literal`. - * @param ctx the parse tree - */ - exitLiteral?: (ctx: LiteralContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.propertyName`. - * @param ctx the parse tree - */ - enterPropertyName?: (ctx: PropertyNameContext) => void; - /** - * Exit a parse tree produced by `mongoParser.propertyName`. - * @param ctx the parse tree - */ - exitPropertyName?: (ctx: PropertyNameContext) => void; - - /** - * Enter a parse tree produced by `mongoParser.comment`. - * @param ctx the parse tree - */ - enterComment?: (ctx: CommentContext) => void; - /** - * Exit a parse tree produced by `mongoParser.comment`. - * @param ctx the parse tree - */ - exitComment?: (ctx: CommentContext) => void; -} diff --git a/src/documentdb/grammar/mongoParser.ts b/src/documentdb/grammar/mongoParser.ts deleted file mode 100644 index 7e4c52933..000000000 --- a/src/documentdb/grammar/mongoParser.ts +++ /dev/null @@ -1,1561 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Generated from ./grammar/mongo.g4 by ANTLR 4.6-SNAPSHOT - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// This is legacy code that we are not maintaining for Typescript 4 -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck - -import { ATN } from 'antlr4ts/atn/ATN'; -import { ATNDeserializer } from 'antlr4ts/atn/ATNDeserializer'; -import { ParserATNSimulator } from 'antlr4ts/atn/ParserATNSimulator'; -import { NotNull, Override } from 'antlr4ts/Decorators'; -import * as Utils from 'antlr4ts/misc/Utils'; -import { NoViableAltException } from 'antlr4ts/NoViableAltException'; -import { Parser } from 'antlr4ts/Parser'; -import { ParserRuleContext } from 'antlr4ts/ParserRuleContext'; -import { RecognitionException } from 'antlr4ts/RecognitionException'; -import { RuleVersion } from 'antlr4ts/RuleVersion'; -import { Token } from 'antlr4ts/Token'; -import { type TokenStream } from 'antlr4ts/TokenStream'; -import { type TerminalNode } from 'antlr4ts/tree/TerminalNode'; -import { type Vocabulary } from 'antlr4ts/Vocabulary'; -import { VocabularyImpl } from 'antlr4ts/VocabularyImpl'; -import { type mongoListener } from './mongoListener'; -import { type mongoVisitor } from './mongoVisitor'; - -export class mongoParser extends Parser { - public static readonly T__0 = 1; - public static readonly T__1 = 2; - public static readonly T__2 = 3; - public static readonly T__3 = 4; - public static readonly T__4 = 5; - public static readonly T__5 = 6; - public static readonly T__6 = 7; - public static readonly T__7 = 8; - public static readonly RegexLiteral = 9; - public static readonly SingleLineComment = 10; - public static readonly MultiLineComment = 11; - public static readonly StringLiteral = 12; - public static readonly NullLiteral = 13; - public static readonly BooleanLiteral = 14; - public static readonly NumericLiteral = 15; - public static readonly DecimalLiteral = 16; - public static readonly LineTerminator = 17; - public static readonly SEMICOLON = 18; - public static readonly DOT = 19; - public static readonly DB = 20; - public static readonly IDENTIFIER = 21; - public static readonly DOUBLE_QUOTED_STRING_LITERAL = 22; - public static readonly SINGLE_QUOTED_STRING_LITERAL = 23; - public static readonly WHITESPACE = 24; - public static readonly RULE_mongoCommands = 0; - public static readonly RULE_commands = 1; - public static readonly RULE_command = 2; - public static readonly RULE_emptyCommand = 3; - public static readonly RULE_collection = 4; - public static readonly RULE_functionCall = 5; - public static readonly RULE_arguments = 6; - public static readonly RULE_argument = 7; - public static readonly RULE_objectLiteral = 8; - public static readonly RULE_arrayLiteral = 9; - public static readonly RULE_elementList = 10; - public static readonly RULE_propertyNameAndValueList = 11; - public static readonly RULE_propertyAssignment = 12; - public static readonly RULE_propertyValue = 13; - public static readonly RULE_literal = 14; - public static readonly RULE_propertyName = 15; - public static readonly RULE_comment = 16; - public static readonly ruleNames: string[] = [ - 'mongoCommands', - 'commands', - 'command', - 'emptyCommand', - 'collection', - 'functionCall', - 'arguments', - 'argument', - 'objectLiteral', - 'arrayLiteral', - 'elementList', - 'propertyNameAndValueList', - 'propertyAssignment', - 'propertyValue', - 'literal', - 'propertyName', - 'comment', - ]; - - private static readonly _LITERAL_NAMES: (string | undefined)[] = [ - undefined, - "'('", - "','", - "')'", - "'{'", - "'}'", - "'['", - "']'", - "':'", - undefined, - undefined, - undefined, - undefined, - "'null'", - undefined, - undefined, - undefined, - undefined, - "';'", - "'.'", - "'db'", - ]; - private static readonly _SYMBOLIC_NAMES: (string | undefined)[] = [ - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - 'RegexLiteral', - 'SingleLineComment', - 'MultiLineComment', - 'StringLiteral', - 'NullLiteral', - 'BooleanLiteral', - 'NumericLiteral', - 'DecimalLiteral', - 'LineTerminator', - 'SEMICOLON', - 'DOT', - 'DB', - 'IDENTIFIER', - 'DOUBLE_QUOTED_STRING_LITERAL', - 'SINGLE_QUOTED_STRING_LITERAL', - 'WHITESPACE', - ]; - public static readonly VOCABULARY: Vocabulary = new VocabularyImpl( - mongoParser._LITERAL_NAMES, - mongoParser._SYMBOLIC_NAMES, - [], - ); - - @Override - @NotNull - public get vocabulary(): Vocabulary { - return mongoParser.VOCABULARY; - } - - @Override - public get grammarFileName(): string { - return 'mongo.g4'; - } - - @Override - public get ruleNames(): string[] { - return mongoParser.ruleNames; - } - - @Override - public get serializedATN(): string { - return mongoParser._serializedATN; - } - - constructor(input: TokenStream) { - super(input); - this._interp = new ParserATNSimulator(mongoParser._ATN, this); - } - @RuleVersion(0) - public mongoCommands(): MongoCommandsContext { - const _localctx: MongoCommandsContext = new MongoCommandsContext(this._ctx, this.state); - this.enterRule(_localctx, 0, mongoParser.RULE_mongoCommands); - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 34; - this.commands(); - this.state = 35; - this.match(mongoParser.EOF); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public commands(): CommandsContext { - const _localctx: CommandsContext = new CommandsContext(this._ctx, this.state); - this.enterRule(_localctx, 2, mongoParser.RULE_commands); - let _la: number; - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 42; - this._errHandler.sync(this); - _la = this._input.LA(1); - while ( - (_la & ~0x1f) === 0 && - ((1 << _la) & - ((1 << mongoParser.SingleLineComment) | - (1 << mongoParser.MultiLineComment) | - (1 << mongoParser.SEMICOLON) | - (1 << mongoParser.DB))) !== - 0 - ) { - { - this.state = 40; - this._errHandler.sync(this); - switch (this._input.LA(1)) { - case mongoParser.DB: - { - this.state = 37; - this.command(); - } - break; - case mongoParser.SEMICOLON: - { - this.state = 38; - this.emptyCommand(); - } - break; - case mongoParser.SingleLineComment: - case mongoParser.MultiLineComment: - { - this.state = 39; - this.comment(); - } - break; - default: - throw new NoViableAltException(this); - } - } - this.state = 44; - this._errHandler.sync(this); - _la = this._input.LA(1); - } - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public command(): CommandContext { - const _localctx: CommandContext = new CommandContext(this._ctx, this.state); - this.enterRule(_localctx, 4, mongoParser.RULE_command); - let _la: number; - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 45; - this.match(mongoParser.DB); - this.state = 48; - this._errHandler.sync(this); - switch (this.interpreter.adaptivePredict(this._input, 2, this._ctx)) { - case 1: - { - this.state = 46; - this.match(mongoParser.DOT); - this.state = 47; - this.collection(); - } - break; - } - this.state = 52; - this._errHandler.sync(this); - _la = this._input.LA(1); - do { - { - { - this.state = 50; - this.match(mongoParser.DOT); - this.state = 51; - this.functionCall(); - } - } - this.state = 54; - this._errHandler.sync(this); - _la = this._input.LA(1); - } while (_la === mongoParser.DOT); - this.state = 57; - this._errHandler.sync(this); - switch (this.interpreter.adaptivePredict(this._input, 4, this._ctx)) { - case 1: - { - this.state = 56; - this.match(mongoParser.SEMICOLON); - } - break; - } - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public emptyCommand(): EmptyCommandContext { - const _localctx: EmptyCommandContext = new EmptyCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 6, mongoParser.RULE_emptyCommand); - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 59; - this.match(mongoParser.SEMICOLON); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public collection(): CollectionContext { - const _localctx: CollectionContext = new CollectionContext(this._ctx, this.state); - this.enterRule(_localctx, 8, mongoParser.RULE_collection); - try { - let _alt: number; - this.enterOuterAlt(_localctx, 1); - { - this.state = 61; - this.match(mongoParser.IDENTIFIER); - this.state = 66; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 5, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - { - { - this.state = 62; - this.match(mongoParser.DOT); - this.state = 63; - this.match(mongoParser.IDENTIFIER); - } - } - } - this.state = 68; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 5, this._ctx); - } - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public functionCall(): FunctionCallContext { - const _localctx: FunctionCallContext = new FunctionCallContext(this._ctx, this.state); - this.enterRule(_localctx, 10, mongoParser.RULE_functionCall); - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 69; - _localctx._FUNCTION_NAME = this.match(mongoParser.IDENTIFIER); - this.state = 70; - this.arguments(); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public arguments(): ArgumentsContext { - const _localctx: ArgumentsContext = new ArgumentsContext(this._ctx, this.state); - this.enterRule(_localctx, 12, mongoParser.RULE_arguments); - let _la: number; - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 72; - _localctx._OPEN_PARENTHESIS = this.match(mongoParser.T__0); - this.state = 81; - this._errHandler.sync(this); - _la = this._input.LA(1); - if ( - (_la & ~0x1f) === 0 && - ((1 << _la) & - ((1 << mongoParser.T__3) | - (1 << mongoParser.T__5) | - (1 << mongoParser.RegexLiteral) | - (1 << mongoParser.StringLiteral) | - (1 << mongoParser.NullLiteral) | - (1 << mongoParser.BooleanLiteral) | - (1 << mongoParser.NumericLiteral))) !== - 0 - ) { - { - this.state = 73; - this.argument(); - this.state = 78; - this._errHandler.sync(this); - _la = this._input.LA(1); - while (_la === mongoParser.T__1) { - { - { - this.state = 74; - this.match(mongoParser.T__1); - this.state = 75; - this.argument(); - } - } - this.state = 80; - this._errHandler.sync(this); - _la = this._input.LA(1); - } - } - } - - this.state = 83; - _localctx._CLOSED_PARENTHESIS = this.match(mongoParser.T__2); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public argument(): ArgumentContext { - const _localctx: ArgumentContext = new ArgumentContext(this._ctx, this.state); - this.enterRule(_localctx, 14, mongoParser.RULE_argument); - try { - this.state = 88; - this._errHandler.sync(this); - switch (this._input.LA(1)) { - case mongoParser.RegexLiteral: - case mongoParser.StringLiteral: - case mongoParser.NullLiteral: - case mongoParser.BooleanLiteral: - case mongoParser.NumericLiteral: - this.enterOuterAlt(_localctx, 1); - { - this.state = 85; - this.literal(); - } - break; - case mongoParser.T__3: - this.enterOuterAlt(_localctx, 2); - { - this.state = 86; - this.objectLiteral(); - } - break; - case mongoParser.T__5: - this.enterOuterAlt(_localctx, 3); - { - this.state = 87; - this.arrayLiteral(); - } - break; - default: - throw new NoViableAltException(this); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public objectLiteral(): ObjectLiteralContext { - const _localctx: ObjectLiteralContext = new ObjectLiteralContext(this._ctx, this.state); - this.enterRule(_localctx, 16, mongoParser.RULE_objectLiteral); - let _la: number; - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 90; - this.match(mongoParser.T__3); - this.state = 92; - this._errHandler.sync(this); - _la = this._input.LA(1); - if (_la === mongoParser.StringLiteral || _la === mongoParser.IDENTIFIER) { - { - this.state = 91; - this.propertyNameAndValueList(); - } - } - - this.state = 95; - this._errHandler.sync(this); - _la = this._input.LA(1); - if (_la === mongoParser.T__1) { - { - this.state = 94; - this.match(mongoParser.T__1); - } - } - - this.state = 97; - this.match(mongoParser.T__4); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public arrayLiteral(): ArrayLiteralContext { - const _localctx: ArrayLiteralContext = new ArrayLiteralContext(this._ctx, this.state); - this.enterRule(_localctx, 18, mongoParser.RULE_arrayLiteral); - let _la: number; - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 99; - this.match(mongoParser.T__5); - this.state = 101; - this._errHandler.sync(this); - _la = this._input.LA(1); - if ( - (_la & ~0x1f) === 0 && - ((1 << _la) & - ((1 << mongoParser.T__3) | - (1 << mongoParser.T__5) | - (1 << mongoParser.RegexLiteral) | - (1 << mongoParser.StringLiteral) | - (1 << mongoParser.NullLiteral) | - (1 << mongoParser.BooleanLiteral) | - (1 << mongoParser.NumericLiteral) | - (1 << mongoParser.IDENTIFIER))) !== - 0 - ) { - { - this.state = 100; - this.elementList(); - } - } - - this.state = 103; - this.match(mongoParser.T__6); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public elementList(): ElementListContext { - const _localctx: ElementListContext = new ElementListContext(this._ctx, this.state); - this.enterRule(_localctx, 20, mongoParser.RULE_elementList); - let _la: number; - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 105; - this.propertyValue(); - this.state = 110; - this._errHandler.sync(this); - _la = this._input.LA(1); - while (_la === mongoParser.T__1) { - { - { - this.state = 106; - this.match(mongoParser.T__1); - this.state = 107; - this.propertyValue(); - } - } - this.state = 112; - this._errHandler.sync(this); - _la = this._input.LA(1); - } - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public propertyNameAndValueList(): PropertyNameAndValueListContext { - const _localctx: PropertyNameAndValueListContext = new PropertyNameAndValueListContext(this._ctx, this.state); - this.enterRule(_localctx, 22, mongoParser.RULE_propertyNameAndValueList); - try { - let _alt: number; - this.enterOuterAlt(_localctx, 1); - { - this.state = 113; - this.propertyAssignment(); - this.state = 118; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 13, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - { - { - this.state = 114; - this.match(mongoParser.T__1); - this.state = 115; - this.propertyAssignment(); - } - } - } - this.state = 120; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 13, this._ctx); - } - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public propertyAssignment(): PropertyAssignmentContext { - const _localctx: PropertyAssignmentContext = new PropertyAssignmentContext(this._ctx, this.state); - this.enterRule(_localctx, 24, mongoParser.RULE_propertyAssignment); - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 121; - this.propertyName(); - this.state = 122; - this.match(mongoParser.T__7); - this.state = 123; - this.propertyValue(); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public propertyValue(): PropertyValueContext { - const _localctx: PropertyValueContext = new PropertyValueContext(this._ctx, this.state); - this.enterRule(_localctx, 26, mongoParser.RULE_propertyValue); - try { - this.state = 129; - this._errHandler.sync(this); - switch (this._input.LA(1)) { - case mongoParser.RegexLiteral: - case mongoParser.StringLiteral: - case mongoParser.NullLiteral: - case mongoParser.BooleanLiteral: - case mongoParser.NumericLiteral: - this.enterOuterAlt(_localctx, 1); - { - this.state = 125; - this.literal(); - } - break; - case mongoParser.T__3: - this.enterOuterAlt(_localctx, 2); - { - this.state = 126; - this.objectLiteral(); - } - break; - case mongoParser.T__5: - this.enterOuterAlt(_localctx, 3); - { - this.state = 127; - this.arrayLiteral(); - } - break; - case mongoParser.IDENTIFIER: - this.enterOuterAlt(_localctx, 4); - { - this.state = 128; - this.functionCall(); - } - break; - default: - throw new NoViableAltException(this); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public literal(): LiteralContext { - const _localctx: LiteralContext = new LiteralContext(this._ctx, this.state); - this.enterRule(_localctx, 28, mongoParser.RULE_literal); - let _la: number; - try { - this.state = 134; - this._errHandler.sync(this); - switch (this._input.LA(1)) { - case mongoParser.StringLiteral: - case mongoParser.NullLiteral: - case mongoParser.BooleanLiteral: - this.enterOuterAlt(_localctx, 1); - { - this.state = 131; - _la = this._input.LA(1); - if ( - !( - (_la & ~0x1f) === 0 && - ((1 << _la) & - ((1 << mongoParser.StringLiteral) | - (1 << mongoParser.NullLiteral) | - (1 << mongoParser.BooleanLiteral))) !== - 0 - ) - ) { - this._errHandler.recoverInline(this); - } else { - if (this._input.LA(1) === Token.EOF) { - this.matchedEOF = true; - } - - this._errHandler.reportMatch(this); - this.consume(); - } - } - break; - case mongoParser.RegexLiteral: - this.enterOuterAlt(_localctx, 2); - { - this.state = 132; - this.match(mongoParser.RegexLiteral); - } - break; - case mongoParser.NumericLiteral: - this.enterOuterAlt(_localctx, 3); - { - this.state = 133; - this.match(mongoParser.NumericLiteral); - } - break; - default: - throw new NoViableAltException(this); - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public propertyName(): PropertyNameContext { - const _localctx: PropertyNameContext = new PropertyNameContext(this._ctx, this.state); - this.enterRule(_localctx, 30, mongoParser.RULE_propertyName); - let _la: number; - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 136; - _la = this._input.LA(1); - if (!(_la === mongoParser.StringLiteral || _la === mongoParser.IDENTIFIER)) { - this._errHandler.recoverInline(this); - } else { - if (this._input.LA(1) === Token.EOF) { - this.matchedEOF = true; - } - - this._errHandler.reportMatch(this); - this.consume(); - } - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - @RuleVersion(0) - public comment(): CommentContext { - const _localctx: CommentContext = new CommentContext(this._ctx, this.state); - this.enterRule(_localctx, 32, mongoParser.RULE_comment); - let _la: number; - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 138; - _la = this._input.LA(1); - if (!(_la === mongoParser.SingleLineComment || _la === mongoParser.MultiLineComment)) { - this._errHandler.recoverInline(this); - } else { - if (this._input.LA(1) === Token.EOF) { - this.matchedEOF = true; - } - - this._errHandler.reportMatch(this); - this.consume(); - } - } - } catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } finally { - this.exitRule(); - } - return _localctx; - } - - public static readonly _serializedATN: string = - '\x03\uAF6F\u8320\u479D\uB75C\u4880\u1605\u191C\uAB37\x03\x1A\x8F\x04\x02' + - '\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x04\x07' + - '\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r\t\r\x04' + - '\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04\x12\t\x12\x03' + - '\x02\x03\x02\x03\x02\x03\x03\x03\x03\x03\x03\x07\x03+\n\x03\f\x03\x0E' + - '\x03.\v\x03\x03\x04\x03\x04\x03\x04\x05\x043\n\x04\x03\x04\x03\x04\x06' + - '\x047\n\x04\r\x04\x0E\x048\x03\x04\x05\x04<\n\x04\x03\x05\x03\x05\x03' + - '\x06\x03\x06\x03\x06\x07\x06C\n\x06\f\x06\x0E\x06F\v\x06\x03\x07\x03\x07' + - '\x03\x07\x03\b\x03\b\x03\b\x03\b\x07\bO\n\b\f\b\x0E\bR\v\b\x05\bT\n\b' + - '\x03\b\x03\b\x03\t\x03\t\x03\t\x05\t[\n\t\x03\n\x03\n\x05\n_\n\n\x03\n' + - '\x05\nb\n\n\x03\n\x03\n\x03\v\x03\v\x05\vh\n\v\x03\v\x03\v\x03\f\x03\f' + - '\x03\f\x07\fo\n\f\f\f\x0E\fr\v\f\x03\r\x03\r\x03\r\x07\rw\n\r\f\r\x0E' + - '\rz\v\r\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0F\x03\x0F\x03\x0F\x03\x0F' + - '\x05\x0F\x84\n\x0F\x03\x10\x03\x10\x03\x10\x05\x10\x89\n\x10\x03\x11\x03' + - '\x11\x03\x12\x03\x12\x03\x12\x02\x02\x02\x13\x02\x02\x04\x02\x06\x02\b' + - '\x02\n\x02\f\x02\x0E\x02\x10\x02\x12\x02\x14\x02\x16\x02\x18\x02\x1A\x02' + - '\x1C\x02\x1E\x02 \x02"\x02\x02\x05\x03\x02\x0E\x10\x04\x02\x0E\x0E\x17' + - '\x17\x03\x02\f\r\x92\x02$\x03\x02\x02\x02\x04,\x03\x02\x02\x02\x06/\x03' + - '\x02\x02\x02\b=\x03\x02\x02\x02\n?\x03\x02\x02\x02\fG\x03\x02\x02\x02' + - '\x0EJ\x03\x02\x02\x02\x10Z\x03\x02\x02\x02\x12\\\x03\x02\x02\x02\x14e' + - '\x03\x02\x02\x02\x16k\x03\x02\x02\x02\x18s\x03\x02\x02\x02\x1A{\x03\x02' + - '\x02\x02\x1C\x83\x03\x02\x02\x02\x1E\x88\x03\x02\x02\x02 \x8A\x03\x02' + - '\x02\x02"\x8C\x03\x02\x02\x02$%\x05\x04\x03\x02%&\x07\x02\x02\x03&\x03' + - "\x03\x02\x02\x02'+\x05\x06\x04\x02(+\x05\b\x05\x02)+\x05\"\x12\x02*'" + - '\x03\x02\x02\x02*(\x03\x02\x02\x02*)\x03\x02\x02\x02+.\x03\x02\x02\x02' + - ',*\x03\x02\x02\x02,-\x03\x02\x02\x02-\x05\x03\x02\x02\x02.,\x03\x02\x02' + - '\x02/2\x07\x16\x02\x0201\x07\x15\x02\x0213\x05\n\x06\x0220\x03\x02\x02' + - '\x0223\x03\x02\x02\x0236\x03\x02\x02\x0245\x07\x15\x02\x0257\x05\f\x07' + - '\x0264\x03\x02\x02\x0278\x03\x02\x02\x0286\x03\x02\x02\x0289\x03\x02\x02' + - '\x029;\x03\x02\x02\x02:<\x07\x14\x02\x02;:\x03\x02\x02\x02;<\x03\x02\x02' + - '\x02<\x07\x03\x02\x02\x02=>\x07\x14\x02\x02>\t\x03\x02\x02\x02?D\x07\x17' + - '\x02\x02@A\x07\x15\x02\x02AC\x07\x17\x02\x02B@\x03\x02\x02\x02CF\x03\x02' + - '\x02\x02DB\x03\x02\x02\x02DE\x03\x02\x02\x02E\v\x03\x02\x02\x02FD\x03' + - '\x02\x02\x02GH\x07\x17\x02\x02HI\x05\x0E\b\x02I\r\x03\x02\x02\x02JS\x07' + - '\x03\x02\x02KP\x05\x10\t\x02LM\x07\x04\x02\x02MO\x05\x10\t\x02NL\x03\x02' + - '\x02\x02OR\x03\x02\x02\x02PN\x03\x02\x02\x02PQ\x03\x02\x02\x02QT\x03\x02' + - '\x02\x02RP\x03\x02\x02\x02SK\x03\x02\x02\x02ST\x03\x02\x02\x02TU\x03\x02' + - '\x02\x02UV\x07\x05\x02\x02V\x0F\x03\x02\x02\x02W[\x05\x1E\x10\x02X[\x05' + - '\x12\n\x02Y[\x05\x14\v\x02ZW\x03\x02\x02\x02ZX\x03\x02\x02\x02ZY\x03\x02' + - '\x02\x02[\x11\x03\x02\x02\x02\\^\x07\x06\x02\x02]_\x05\x18\r\x02^]\x03' + - '\x02\x02\x02^_\x03\x02\x02\x02_a\x03\x02\x02\x02`b\x07\x04\x02\x02a`\x03' + - '\x02\x02\x02ab\x03\x02\x02\x02bc\x03\x02\x02\x02cd\x07\x07\x02\x02d\x13' + - '\x03\x02\x02\x02eg\x07\b\x02\x02fh\x05\x16\f\x02gf\x03\x02\x02\x02gh\x03' + - '\x02\x02\x02hi\x03\x02\x02\x02ij\x07\t\x02\x02j\x15\x03\x02\x02\x02kp' + - '\x05\x1C\x0F\x02lm\x07\x04\x02\x02mo\x05\x1C\x0F\x02nl\x03\x02\x02\x02' + - 'or\x03\x02\x02\x02pn\x03\x02\x02\x02pq\x03\x02\x02\x02q\x17\x03\x02\x02' + - '\x02rp\x03\x02\x02\x02sx\x05\x1A\x0E\x02tu\x07\x04\x02\x02uw\x05\x1A\x0E' + - '\x02vt\x03\x02\x02\x02wz\x03\x02\x02\x02xv\x03\x02\x02\x02xy\x03\x02\x02' + - '\x02y\x19\x03\x02\x02\x02zx\x03\x02\x02\x02{|\x05 \x11\x02|}\x07\n\x02' + - '\x02}~\x05\x1C\x0F\x02~\x1B\x03\x02\x02\x02\x7F\x84\x05\x1E\x10\x02\x80' + - '\x84\x05\x12\n\x02\x81\x84\x05\x14\v\x02\x82\x84\x05\f\x07\x02\x83\x7F' + - '\x03\x02\x02\x02\x83\x80\x03\x02\x02\x02\x83\x81\x03\x02\x02\x02\x83\x82' + - '\x03\x02\x02\x02\x84\x1D\x03\x02\x02\x02\x85\x89\t\x02\x02\x02\x86\x89' + - '\x07\v\x02\x02\x87\x89\x07\x11\x02\x02\x88\x85\x03\x02\x02\x02\x88\x86' + - '\x03\x02\x02\x02\x88\x87\x03\x02\x02\x02\x89\x1F\x03\x02\x02\x02\x8A\x8B' + - '\t\x03\x02\x02\x8B!\x03\x02\x02\x02\x8C\x8D\t\x04\x02\x02\x8D#\x03\x02' + - '\x02\x02\x12*,28;DPSZ^agpx\x83\x88'; - public static __ATN: ATN; - public static get _ATN(): ATN { - if (!mongoParser.__ATN) { - mongoParser.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(mongoParser._serializedATN)); - } - - return mongoParser.__ATN; - } -} - -export class MongoCommandsContext extends ParserRuleContext { - public commands(): CommandsContext { - return this.getRuleContext(0, CommandsContext); - } - public EOF(): TerminalNode { - return this.getToken(mongoParser.EOF, 0); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_mongoCommands; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterMongoCommands) listener.enterMongoCommands(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitMongoCommands) listener.exitMongoCommands(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitMongoCommands) return visitor.visitMongoCommands(this); - else return visitor.visitChildren(this); - } -} - -export class CommandsContext extends ParserRuleContext { - public command(): CommandContext[]; - public command(i: number): CommandContext; - public command(i?: number): CommandContext | CommandContext[] { - if (i === undefined) { - return this.getRuleContexts(CommandContext); - } else { - return this.getRuleContext(i, CommandContext); - } - } - public emptyCommand(): EmptyCommandContext[]; - public emptyCommand(i: number): EmptyCommandContext; - public emptyCommand(i?: number): EmptyCommandContext | EmptyCommandContext[] { - if (i === undefined) { - return this.getRuleContexts(EmptyCommandContext); - } else { - return this.getRuleContext(i, EmptyCommandContext); - } - } - public comment(): CommentContext[]; - public comment(i: number): CommentContext; - public comment(i?: number): CommentContext | CommentContext[] { - if (i === undefined) { - return this.getRuleContexts(CommentContext); - } else { - return this.getRuleContext(i, CommentContext); - } - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_commands; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterCommands) listener.enterCommands(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitCommands) listener.exitCommands(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitCommands) return visitor.visitCommands(this); - else return visitor.visitChildren(this); - } -} - -export class CommandContext extends ParserRuleContext { - public DB(): TerminalNode { - return this.getToken(mongoParser.DB, 0); - } - public DOT(): TerminalNode[]; - public DOT(i: number): TerminalNode; - public DOT(i?: number): TerminalNode | TerminalNode[] { - if (i === undefined) { - return this.getTokens(mongoParser.DOT); - } else { - return this.getToken(mongoParser.DOT, i); - } - } - public collection(): CollectionContext | undefined { - return this.tryGetRuleContext(0, CollectionContext); - } - public functionCall(): FunctionCallContext[]; - public functionCall(i: number): FunctionCallContext; - public functionCall(i?: number): FunctionCallContext | FunctionCallContext[] { - if (i === undefined) { - return this.getRuleContexts(FunctionCallContext); - } else { - return this.getRuleContext(i, FunctionCallContext); - } - } - public SEMICOLON(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.SEMICOLON, 0); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_command; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterCommand) listener.enterCommand(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitCommand) listener.exitCommand(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitCommand) return visitor.visitCommand(this); - else return visitor.visitChildren(this); - } -} - -export class EmptyCommandContext extends ParserRuleContext { - public SEMICOLON(): TerminalNode { - return this.getToken(mongoParser.SEMICOLON, 0); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_emptyCommand; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterEmptyCommand) listener.enterEmptyCommand(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitEmptyCommand) listener.exitEmptyCommand(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitEmptyCommand) return visitor.visitEmptyCommand(this); - else return visitor.visitChildren(this); - } -} - -export class CollectionContext extends ParserRuleContext { - public IDENTIFIER(): TerminalNode[]; - public IDENTIFIER(i: number): TerminalNode; - public IDENTIFIER(i?: number): TerminalNode | TerminalNode[] { - if (i === undefined) { - return this.getTokens(mongoParser.IDENTIFIER); - } else { - return this.getToken(mongoParser.IDENTIFIER, i); - } - } - public DOT(): TerminalNode[]; - public DOT(i: number): TerminalNode; - public DOT(i?: number): TerminalNode | TerminalNode[] { - if (i === undefined) { - return this.getTokens(mongoParser.DOT); - } else { - return this.getToken(mongoParser.DOT, i); - } - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_collection; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterCollection) listener.enterCollection(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitCollection) listener.exitCollection(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitCollection) return visitor.visitCollection(this); - else return visitor.visitChildren(this); - } -} - -export class FunctionCallContext extends ParserRuleContext { - public _FUNCTION_NAME: Token; - public arguments(): ArgumentsContext { - return this.getRuleContext(0, ArgumentsContext); - } - public IDENTIFIER(): TerminalNode { - return this.getToken(mongoParser.IDENTIFIER, 0); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_functionCall; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterFunctionCall) listener.enterFunctionCall(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitFunctionCall) listener.exitFunctionCall(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitFunctionCall) return visitor.visitFunctionCall(this); - else return visitor.visitChildren(this); - } -} - -export class ArgumentsContext extends ParserRuleContext { - public _OPEN_PARENTHESIS: Token; - public _CLOSED_PARENTHESIS: Token; - public argument(): ArgumentContext[]; - public argument(i: number): ArgumentContext; - public argument(i?: number): ArgumentContext | ArgumentContext[] { - if (i === undefined) { - return this.getRuleContexts(ArgumentContext); - } else { - return this.getRuleContext(i, ArgumentContext); - } - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_arguments; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterArguments) listener.enterArguments(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitArguments) listener.exitArguments(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitArguments) return visitor.visitArguments(this); - else return visitor.visitChildren(this); - } -} - -export class ArgumentContext extends ParserRuleContext { - public literal(): LiteralContext | undefined { - return this.tryGetRuleContext(0, LiteralContext); - } - public objectLiteral(): ObjectLiteralContext | undefined { - return this.tryGetRuleContext(0, ObjectLiteralContext); - } - public arrayLiteral(): ArrayLiteralContext | undefined { - return this.tryGetRuleContext(0, ArrayLiteralContext); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_argument; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterArgument) listener.enterArgument(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitArgument) listener.exitArgument(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitArgument) return visitor.visitArgument(this); - else return visitor.visitChildren(this); - } -} - -export class ObjectLiteralContext extends ParserRuleContext { - public propertyNameAndValueList(): PropertyNameAndValueListContext | undefined { - return this.tryGetRuleContext(0, PropertyNameAndValueListContext); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_objectLiteral; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterObjectLiteral) listener.enterObjectLiteral(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitObjectLiteral) listener.exitObjectLiteral(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitObjectLiteral) return visitor.visitObjectLiteral(this); - else return visitor.visitChildren(this); - } -} - -export class ArrayLiteralContext extends ParserRuleContext { - public elementList(): ElementListContext | undefined { - return this.tryGetRuleContext(0, ElementListContext); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_arrayLiteral; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterArrayLiteral) listener.enterArrayLiteral(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitArrayLiteral) listener.exitArrayLiteral(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitArrayLiteral) return visitor.visitArrayLiteral(this); - else return visitor.visitChildren(this); - } -} - -export class ElementListContext extends ParserRuleContext { - public propertyValue(): PropertyValueContext[]; - public propertyValue(i: number): PropertyValueContext; - public propertyValue(i?: number): PropertyValueContext | PropertyValueContext[] { - if (i === undefined) { - return this.getRuleContexts(PropertyValueContext); - } else { - return this.getRuleContext(i, PropertyValueContext); - } - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_elementList; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterElementList) listener.enterElementList(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitElementList) listener.exitElementList(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitElementList) return visitor.visitElementList(this); - else return visitor.visitChildren(this); - } -} - -export class PropertyNameAndValueListContext extends ParserRuleContext { - public propertyAssignment(): PropertyAssignmentContext[]; - public propertyAssignment(i: number): PropertyAssignmentContext; - public propertyAssignment(i?: number): PropertyAssignmentContext | PropertyAssignmentContext[] { - if (i === undefined) { - return this.getRuleContexts(PropertyAssignmentContext); - } else { - return this.getRuleContext(i, PropertyAssignmentContext); - } - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_propertyNameAndValueList; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterPropertyNameAndValueList) listener.enterPropertyNameAndValueList(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitPropertyNameAndValueList) listener.exitPropertyNameAndValueList(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitPropertyNameAndValueList) return visitor.visitPropertyNameAndValueList(this); - else return visitor.visitChildren(this); - } -} - -export class PropertyAssignmentContext extends ParserRuleContext { - public propertyName(): PropertyNameContext { - return this.getRuleContext(0, PropertyNameContext); - } - public propertyValue(): PropertyValueContext { - return this.getRuleContext(0, PropertyValueContext); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_propertyAssignment; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterPropertyAssignment) listener.enterPropertyAssignment(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitPropertyAssignment) listener.exitPropertyAssignment(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitPropertyAssignment) return visitor.visitPropertyAssignment(this); - else return visitor.visitChildren(this); - } -} - -export class PropertyValueContext extends ParserRuleContext { - public literal(): LiteralContext | undefined { - return this.tryGetRuleContext(0, LiteralContext); - } - public objectLiteral(): ObjectLiteralContext | undefined { - return this.tryGetRuleContext(0, ObjectLiteralContext); - } - public arrayLiteral(): ArrayLiteralContext | undefined { - return this.tryGetRuleContext(0, ArrayLiteralContext); - } - public functionCall(): FunctionCallContext | undefined { - return this.tryGetRuleContext(0, FunctionCallContext); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_propertyValue; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterPropertyValue) listener.enterPropertyValue(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitPropertyValue) listener.exitPropertyValue(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitPropertyValue) return visitor.visitPropertyValue(this); - else return visitor.visitChildren(this); - } -} - -export class LiteralContext extends ParserRuleContext { - public NullLiteral(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.NullLiteral, 0); - } - public BooleanLiteral(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.BooleanLiteral, 0); - } - public StringLiteral(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.StringLiteral, 0); - } - public RegexLiteral(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.RegexLiteral, 0); - } - public NumericLiteral(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.NumericLiteral, 0); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_literal; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterLiteral) listener.enterLiteral(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitLiteral) listener.exitLiteral(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitLiteral) return visitor.visitLiteral(this); - else return visitor.visitChildren(this); - } -} - -export class PropertyNameContext extends ParserRuleContext { - public StringLiteral(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.StringLiteral, 0); - } - public IDENTIFIER(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.IDENTIFIER, 0); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_propertyName; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterPropertyName) listener.enterPropertyName(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitPropertyName) listener.exitPropertyName(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitPropertyName) return visitor.visitPropertyName(this); - else return visitor.visitChildren(this); - } -} - -export class CommentContext extends ParserRuleContext { - public SingleLineComment(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.SingleLineComment, 0); - } - public MultiLineComment(): TerminalNode | undefined { - return this.tryGetToken(mongoParser.MultiLineComment, 0); - } - constructor(parent: ParserRuleContext, invokingState: number); - constructor(parent: ParserRuleContext, invokingState: number) { - super(parent, invokingState); - } - @Override public get ruleIndex(): number { - return mongoParser.RULE_comment; - } - @Override - public enterRule(listener: mongoListener): void { - if (listener.enterComment) listener.enterComment(this); - } - @Override - public exitRule(listener: mongoListener): void { - if (listener.exitComment) listener.exitComment(this); - } - @Override - public accept(visitor: mongoVisitor): Result { - if (visitor.visitComment) return visitor.visitComment(this); - else return visitor.visitChildren(this); - } -} diff --git a/src/documentdb/grammar/mongoVisitor.ts b/src/documentdb/grammar/mongoVisitor.ts deleted file mode 100644 index 34f9bf665..000000000 --- a/src/documentdb/grammar/mongoVisitor.ts +++ /dev/null @@ -1,160 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Generated from ./grammar/mongo.g4 by ANTLR 4.6-SNAPSHOT - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type ParseTreeVisitor } from 'antlr4ts/tree/ParseTreeVisitor'; -import { - type ArgumentContext, - type ArgumentsContext, - type ArrayLiteralContext, - type CollectionContext, - type CommandContext, - type CommandsContext, - type CommentContext, - type ElementListContext, - type EmptyCommandContext, - type FunctionCallContext, - type LiteralContext, - type MongoCommandsContext, - type ObjectLiteralContext, - type PropertyAssignmentContext, - type PropertyNameAndValueListContext, - type PropertyNameContext, - type PropertyValueContext, -} from './mongoParser'; - -/** - * This interface defines a complete generic visitor for a parse tree produced - * by `mongoParser`. - * - * @param The return type of the visit operation. Use `void` for - * operations with no return type. - */ -export interface mongoVisitor extends ParseTreeVisitor { - /** - * Visit a parse tree produced by `mongoParser.mongoCommands`. - * @param ctx the parse tree - * @return the visitor result - */ - visitMongoCommands?: (ctx: MongoCommandsContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.commands`. - * @param ctx the parse tree - * @return the visitor result - */ - visitCommands?: (ctx: CommandsContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.command`. - * @param ctx the parse tree - * @return the visitor result - */ - visitCommand?: (ctx: CommandContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.emptyCommand`. - * @param ctx the parse tree - * @return the visitor result - */ - visitEmptyCommand?: (ctx: EmptyCommandContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.collection`. - * @param ctx the parse tree - * @return the visitor result - */ - visitCollection?: (ctx: CollectionContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.functionCall`. - * @param ctx the parse tree - * @return the visitor result - */ - visitFunctionCall?: (ctx: FunctionCallContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.arguments`. - * @param ctx the parse tree - * @return the visitor result - */ - visitArguments?: (ctx: ArgumentsContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.argument`. - * @param ctx the parse tree - * @return the visitor result - */ - visitArgument?: (ctx: ArgumentContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.objectLiteral`. - * @param ctx the parse tree - * @return the visitor result - */ - visitObjectLiteral?: (ctx: ObjectLiteralContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.arrayLiteral`. - * @param ctx the parse tree - * @return the visitor result - */ - visitArrayLiteral?: (ctx: ArrayLiteralContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.elementList`. - * @param ctx the parse tree - * @return the visitor result - */ - visitElementList?: (ctx: ElementListContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.propertyNameAndValueList`. - * @param ctx the parse tree - * @return the visitor result - */ - visitPropertyNameAndValueList?: (ctx: PropertyNameAndValueListContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.propertyAssignment`. - * @param ctx the parse tree - * @return the visitor result - */ - visitPropertyAssignment?: (ctx: PropertyAssignmentContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.propertyValue`. - * @param ctx the parse tree - * @return the visitor result - */ - visitPropertyValue?: (ctx: PropertyValueContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.literal`. - * @param ctx the parse tree - * @return the visitor result - */ - visitLiteral?: (ctx: LiteralContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.propertyName`. - * @param ctx the parse tree - * @return the visitor result - */ - visitPropertyName?: (ctx: PropertyNameContext) => Result; - - /** - * Visit a parse tree produced by `mongoParser.comment`. - * @param ctx the parse tree - * @return the visitor result - */ - visitComment?: (ctx: CommentContext) => Result; -} diff --git a/src/documentdb/grammar/visitors.ts b/src/documentdb/grammar/visitors.ts deleted file mode 100644 index 053c101bd..000000000 --- a/src/documentdb/grammar/visitors.ts +++ /dev/null @@ -1,90 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type ParserRuleContext } from 'antlr4ts/ParserRuleContext'; -import { type ErrorNode } from 'antlr4ts/tree/ErrorNode'; -import { type ParseTree } from 'antlr4ts/tree/ParseTree'; -import { type TerminalNode } from 'antlr4ts/tree/TerminalNode'; -import { - type ArgumentContext, - type ArgumentsContext, - type CollectionContext, - type CommandContext, - type CommandsContext, - type FunctionCallContext, - type MongoCommandsContext, -} from './mongoParser'; -import { type mongoVisitor } from './mongoVisitor'; - -export class MongoVisitor implements mongoVisitor { - visitMongoCommands(ctx: MongoCommandsContext): T { - return this.visitChildren(ctx); - } - - visitCommands(ctx: CommandsContext): T { - return this.visitChildren(ctx); - } - - visitCommand(ctx: CommandContext): T { - return this.visitChildren(ctx); - } - - visitCollection(ctx: CollectionContext): T { - return this.visitChildren(ctx); - } - - visitFunctionCall(ctx: FunctionCallContext): T { - return this.visitChildren(ctx); - } - - visitArgument(ctx: ArgumentContext): T { - return this.visitChildren(ctx); - } - - visitArguments(ctx: ArgumentsContext): T { - return this.visitChildren(ctx); - } - - visit(tree: ParseTree): T { - return tree.accept(this); - } - - visitChildren(ctx: ParserRuleContext): T { - let result = this.defaultResult(ctx); - const n = ctx.childCount; - for (let i = 0; i < n; i++) { - if (!this.shouldVisitNextChild(ctx, result)) { - break; - } - - const childNode = ctx.getChild(i); - const childResult = childNode.accept(this); - result = this.aggregateResult(result, childResult); - } - return result; - } - - visitTerminal(node: TerminalNode): T { - return this.defaultResult(node); - } - - visitErrorNode(node: ErrorNode): T { - return this.defaultResult(node); - } - - protected defaultResult(_node: ParseTree): T { - // grandfathered-in. Unclear why this is null instead of type T - return (null); - } - - protected aggregateResult(aggregate: T, nextResult: T): T { - return !nextResult ? aggregate : nextResult; - } - - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - shouldVisitNextChild(_node, _currentResult: T): boolean { - return true; - } -} diff --git a/src/documentdb/scrapbook/mongoConnectionStrings.test.ts b/src/documentdb/mongoConnectionStrings.test.ts similarity index 99% rename from src/documentdb/scrapbook/mongoConnectionStrings.test.ts rename to src/documentdb/mongoConnectionStrings.test.ts index 4fabc88ec..68af20c79 100644 --- a/src/documentdb/scrapbook/mongoConnectionStrings.test.ts +++ b/src/documentdb/mongoConnectionStrings.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { wellKnownEmulatorPassword } from '../../constants'; +import { wellKnownEmulatorPassword } from '../constants'; import { isCosmosEmulatorConnectionString } from './connectToClient'; import { addDatabaseToAccountConnectionString, diff --git a/src/documentdb/scrapbook/mongoConnectionStrings.ts b/src/documentdb/mongoConnectionStrings.ts similarity index 97% rename from src/documentdb/scrapbook/mongoConnectionStrings.ts rename to src/documentdb/mongoConnectionStrings.ts index 26166f8e0..cbeb57ab0 100644 --- a/src/documentdb/scrapbook/mongoConnectionStrings.ts +++ b/src/documentdb/mongoConnectionStrings.ts @@ -5,8 +5,8 @@ import { appendExtensionUserAgent, parseError, type IParsedError } from '@microsoft/vscode-azext-utils'; import { type MongoClient } from 'mongodb'; -import { ParsedConnectionString } from '../../ParsedConnectionString'; -import { nonNullValue } from '../../utils/nonNull'; +import { ParsedConnectionString } from '../ParsedConnectionString'; +import { nonNullValue } from '../utils/nonNull'; import { connectToClient } from './connectToClient'; // Connection strings follow the following format (https://docs.mongodb.com/manual/reference/connection-string/): diff --git a/src/documentdb/scrapbook/MongoCommand.ts b/src/documentdb/scrapbook/MongoCommand.ts deleted file mode 100644 index bb54e498c..000000000 --- a/src/documentdb/scrapbook/MongoCommand.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type RecognitionException } from 'antlr4ts'; -import type * as vscode from 'vscode'; - -export interface MongoCommand { - range: vscode.Range; - text: string; - collection?: string; - name?: string; - arguments?: string[]; - argumentObjects?: object[]; - errors?: ErrorDescription[]; - chained?: boolean; -} - -export interface ErrorDescription { - range: vscode.Range; - message: string; - exception?: RecognitionException; -} diff --git a/src/documentdb/scrapbook/ScrapbookHelpers.ts b/src/documentdb/scrapbook/ScrapbookHelpers.ts deleted file mode 100644 index b2bca937e..000000000 --- a/src/documentdb/scrapbook/ScrapbookHelpers.ts +++ /dev/null @@ -1,466 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { parseError, type IParsedError } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import { ANTLRInputStream as InputStream } from 'antlr4ts/ANTLRInputStream'; -import { CommonTokenStream } from 'antlr4ts/CommonTokenStream'; -import { ErrorNode } from 'antlr4ts/tree/ErrorNode'; -import { type ParseTree } from 'antlr4ts/tree/ParseTree'; -import { TerminalNode } from 'antlr4ts/tree/TerminalNode'; -import { EJSON, ObjectId } from 'bson'; -import * as vscode from 'vscode'; -import { filterType, findType } from '../../utils/array'; -import { nonNullProp, nonNullValue } from '../../utils/nonNull'; -import { mongoLexer } from '../grammar/mongoLexer'; -import * as mongoParser from '../grammar/mongoParser'; -import { MongoVisitor } from '../grammar/visitors'; -import { LexerErrorListener, ParserErrorListener } from './errorListeners'; -import { type ErrorDescription, type MongoCommand } from './MongoCommand'; - -export function stripQuotes(term: string): string { - if ((term.startsWith("'") && term.endsWith("'")) || (term.startsWith('"') && term.endsWith('"'))) { - return term.substring(1, term.length - 1); - } - return term; -} - -export function getAllErrorsFromTextDocument(document: vscode.TextDocument): vscode.Diagnostic[] { - const commands = getAllCommandsFromText(document.getText()); - const errors: vscode.Diagnostic[] = []; - for (const command of commands) { - for (const error of command.errors || []) { - const diagnostic = new vscode.Diagnostic(error.range, error.message); - errors.push(diagnostic); - } - } - - return errors; -} - -export function getAllCommandsFromText(content: string): MongoCommand[] { - const lexer = new mongoLexer(new InputStream(content)); - const lexerListener = new LexerErrorListener(); - lexer.removeErrorListeners(); // Default listener outputs to the console - lexer.addErrorListener(lexerListener); - const tokens: CommonTokenStream = new CommonTokenStream(lexer); - - const parser = new mongoParser.mongoParser(tokens); - const parserListener = new ParserErrorListener(); - parser.removeErrorListeners(); // Default listener outputs to the console - parser.addErrorListener(parserListener); - - const commandsContext: mongoParser.MongoCommandsContext = parser.mongoCommands(); - const commands = new FindMongoCommandsVisitor().visit(commandsContext); - - // Match errors with commands based on location - const errors = lexerListener.errors.concat(parserListener.errors); - errors.sort((a, b) => { - const linediff = a.range.start.line - b.range.start.line; - const chardiff = a.range.start.character - b.range.start.character; - return linediff || chardiff; - }); - for (const err of errors) { - const associatedCommand = findCommandAtPosition(commands, err.range.start); - if (associatedCommand) { - associatedCommand.errors = associatedCommand.errors || []; - associatedCommand.errors.push(err); - } else { - // Create a new command to hook this up to - const emptyCommand: MongoCommand = { - collection: undefined, - name: undefined, - range: err.range, - text: '', - }; - emptyCommand.errors = [err]; - commands.push(emptyCommand); - } - } - - return commands; -} - -export function findCommandAtPosition(commands: MongoCommand[], position?: vscode.Position): MongoCommand { - let lastCommandOnSameLine: MongoCommand | undefined; - let lastCommandBeforePosition: MongoCommand | undefined; - if (position) { - for (const command of commands) { - if (command.range.contains(position)) { - return command; - } - if (command.range.end.line === position.line) { - lastCommandOnSameLine = command; - } - if (command.range.end.isBefore(position)) { - lastCommandBeforePosition = command; - } - } - } - return lastCommandOnSameLine || lastCommandBeforePosition || commands[commands.length - 1]; -} - -class FindMongoCommandsVisitor extends MongoVisitor { - private commands: MongoCommand[] = []; - - public visitCommand(ctx: mongoParser.CommandContext): MongoCommand[] { - const funcCallCount: number = filterType(ctx.children, mongoParser.FunctionCallContext).length; - const stop = nonNullProp(ctx, 'stop', 'ctx.stop', 'ScrapbookHelpers.ts'); - this.commands.push({ - range: new vscode.Range( - ctx.start.line - 1, - ctx.start.charPositionInLine, - stop.line - 1, - stop.charPositionInLine, - ), - text: ctx.text, - name: '', - arguments: [], - argumentObjects: [], - chained: funcCallCount > 1 ? true : false, - }); - return super.visitCommand(ctx); - } - - public visitCollection(ctx: mongoParser.CollectionContext): MongoCommand[] { - this.commands[this.commands.length - 1].collection = ctx.text; - return super.visitCollection(ctx); - } - - public visitFunctionCall(ctx: mongoParser.FunctionCallContext): MongoCommand[] { - if (ctx.parent instanceof mongoParser.CommandContext) { - this.commands[this.commands.length - 1].name = (ctx._FUNCTION_NAME && ctx._FUNCTION_NAME.text) || ''; - } - return super.visitFunctionCall(ctx); - } - - public visitArgument(ctx: mongoParser.ArgumentContext): MongoCommand[] { - try { - const argumentsContext = ctx.parent; - if (argumentsContext) { - const functionCallContext = argumentsContext.parent; - if (functionCallContext && functionCallContext.parent instanceof mongoParser.CommandContext) { - const lastCommand = this.commands[this.commands.length - 1]; - const argAsObject = this.contextToObject(ctx); - - const argText = EJSON.stringify(argAsObject); - nonNullProp(lastCommand, 'arguments', 'lastCommand.arguments', 'ScrapbookHelpers.ts').push(argText); - const escapeHandled = this.deduplicateEscapesForRegex(argText); - let ejsonParsed = {}; - try { - // eslint-disable-next-line , @typescript-eslint/no-unsafe-assignment - ejsonParsed = EJSON.parse(escapeHandled); - } catch (error) { - //EJSON parse failed due to a wrong flag, etc. - const parsedError: IParsedError = parseError(error); - this.addErrorToCommand(parsedError.message, ctx); - } - nonNullProp( - lastCommand, - 'argumentObjects', - 'lastCommand.argumentObjects', - 'ScrapbookHelpers.ts', - ).push(ejsonParsed); - } - } - } catch (error) { - const parsedError: IParsedError = parseError(error); - this.addErrorToCommand(parsedError.message, ctx); - } - return super.visitArgument(ctx); - } - - protected defaultResult(_node: ParseTree): MongoCommand[] { - return this.commands; - } - - //eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - private contextToObject(ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext): Object { - if (!ctx || ctx.childCount === 0) { - //Base case and malformed statements - return {}; - } - // In a well formed expression, Argument and propertyValue tokens should have exactly one child, from their definitions in mongo.g4 - const child: ParseTree = nonNullProp(ctx, 'children', 'ctx.children', 'ScrapbookHelpers.ts')[0]; - if (child instanceof mongoParser.LiteralContext) { - return this.literalContextToObject(child, ctx); - } else if (child instanceof mongoParser.ObjectLiteralContext) { - return this.objectLiteralContextToObject(child); - } else if (child instanceof mongoParser.ArrayLiteralContext) { - return this.arrayLiteralContextToObject(child); - } else if (child instanceof mongoParser.FunctionCallContext) { - return this.functionCallContextToObject(child, ctx); - } else if (child instanceof ErrorNode) { - return {}; - } else { - this.addErrorToCommand( - l10n.t('Unrecognized node type encountered. We could not parse {text}', { text: child.text }), - ctx, - ); - return {}; - } - } - - private literalContextToObject( - child: mongoParser.LiteralContext, - ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext, - //eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - ): Object { - const text = child.text; - const tokenType = child.start.type; - const nonStringLiterals = [ - mongoParser.mongoParser.NullLiteral, - mongoParser.mongoParser.BooleanLiteral, - mongoParser.mongoParser.NumericLiteral, - ]; - if (tokenType === mongoParser.mongoParser.StringLiteral) { - return stripQuotes(text); - } else if (tokenType === mongoParser.mongoParser.RegexLiteral) { - return this.regexLiteralContextToObject(ctx, text); - } else if (nonStringLiterals.indexOf(tokenType) > -1) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return JSON.parse(text); - } else { - this.addErrorToCommand(l10n.t('Unrecognized token. Token text: {text}', { text }), ctx); - return {}; - } - } - - private objectLiteralContextToObject(child: mongoParser.ObjectLiteralContext): object { - const propertyNameAndValue = findType(child.children, mongoParser.PropertyNameAndValueListContext); - if (!propertyNameAndValue) { - // Argument is {} - return {}; - } else { - const parsedObject: object = {}; - const propertyAssignments = filterType( - propertyNameAndValue.children, - mongoParser.PropertyAssignmentContext, - ); - for (const propertyAssignment of propertyAssignments) { - const propertyAssignmentChildren = nonNullProp( - propertyAssignment, - 'children', - 'propertyAssignment.children', - 'ScrapbookHelpers.ts', - ); - const propertyName = propertyAssignmentChildren[0]; - const propertyValue = propertyAssignmentChildren[2]; - parsedObject[stripQuotes(propertyName.text)] = this.contextToObject(propertyValue); - } - return parsedObject; - } - } - - private arrayLiteralContextToObject(child: mongoParser.ArrayLiteralContext) { - const elementList = findType(child.children, mongoParser.ElementListContext); - if (elementList) { - const elementItems = filterType(elementList.children, mongoParser.PropertyValueContext); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return elementItems.map(this.contextToObject.bind(this)); - } else { - return []; - } - } - - private functionCallContextToObject( - child: mongoParser.FunctionCallContext, - ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext, - // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - ): Object { - const functionTokens = child.children; - const constructorCall: TerminalNode = nonNullValue( - findType(functionTokens, TerminalNode), - 'constructorCall', - 'ScrapbookHelpers.ts', - ); - const argumentsToken: mongoParser.ArgumentsContext = nonNullValue( - findType(functionTokens, mongoParser.ArgumentsContext), - 'argumentsToken', - 'ScrapbookHelpers.ts', - ); - if (!(argumentsToken._CLOSED_PARENTHESIS && argumentsToken._OPEN_PARENTHESIS)) { - //argumentsToken does not have '(' or ')' - this.addErrorToCommand( - l10n.t('Expecting parentheses or quotes at "{text}"', { text: constructorCall.text }), - ctx, - ); - return {}; - } - - const argumentContextArray: mongoParser.ArgumentContext[] = filterType( - argumentsToken.children, - mongoParser.ArgumentContext, - ); - if (argumentContextArray.length > 1) { - this.addErrorToCommand( - l10n.t('Too many arguments. Expecting 0 or 1 argument(s) to {constructorCall}', { - constructorCall: constructorCall.text, - }), - ctx, - ); - return {}; - } - - const tokenText: string | undefined = argumentContextArray.length ? argumentContextArray[0].text : undefined; - switch (constructorCall.text) { - case 'ObjectId': - return this.objectIdToObject(ctx, tokenText); - case 'ISODate': - return this.isodateToObject(ctx, tokenText); - case 'Date': - return this.dateToObject(ctx, tokenText); - default: - this.addErrorToCommand( - l10n.t( - 'Unrecognized node type encountered. Could not parse {constructorCall} as part of {functionCall}', - { constructorCall: constructorCall.text, functionCall: child.text }, - ), - ctx, - ); - return {}; - } - } - - private dateToObject( - ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext, - tokenText?: string, - // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - ): { $date: string } | Object { - // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - const date: Date | Object = this.tryToConstructDate(ctx, tokenText); - if (date instanceof Date) { - return { $date: date.toString() }; - } else { - return date; - } - } - - private isodateToObject( - ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext, - tokenText?: string, - // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - ): { $date: string } | Object { - // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - const date: Date | Object = this.tryToConstructDate(ctx, tokenText, true); - - if (date instanceof Date) { - return { $date: date.toISOString() }; - } else { - return date; - } - } - - private tryToConstructDate( - ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext, - tokenText?: string, - isIsodate: boolean = false, - // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - ): Date | Object { - if (!tokenText) { - // usage : ObjectID() - return new Date(); - } else { - try { - tokenText = stripQuotes(tokenText); - - // if the tokenText was an isodate, the last char must be Z - if (isIsodate) { - if (tokenText[tokenText.length - 1] !== 'Z') { - tokenText += 'Z'; - } - } - - return new Date(tokenText); - } catch (error) { - const parsedError: IParsedError = parseError(error); - this.addErrorToCommand(parsedError.message, ctx); - return {}; - } - } - } - - private objectIdToObject( - ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext, - tokenText?: string, - // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - ): Object { - let hexID: string; - let constructedObject: ObjectId; - if (!tokenText) { - // usage : ObjectID() - constructedObject = new ObjectId(); - } else { - hexID = stripQuotes(tokenText); - try { - constructedObject = new ObjectId(hexID); - } catch (error) { - const parsedError: IParsedError = parseError(error); - this.addErrorToCommand(parsedError.message, ctx); - return {}; - } - } - return { $oid: constructedObject.toString() }; - } - - private regexLiteralContextToObject( - ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext, - text: string, - // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - ): Object { - const separator = text.lastIndexOf('/'); - const flags = separator !== text.length - 1 ? text.substring(separator + 1) : ''; - const pattern = text.substring(1, separator); - try { - // validate the pattern and flags. - // It is intended for the errors thrown here to be handled by the catch block. - new RegExp(pattern, flags); - // we are passing back a $regex annotation, hence we ensure parity wit the $regex syntax - return { $regex: this.regexToStringNotation(pattern), $options: flags }; - } catch (error) { - //User may not have finished typing - const parsedError: IParsedError = parseError(error); - this.addErrorToCommand(parsedError.message, ctx); - return {}; - } - } - - private addErrorToCommand( - errorMessage: string, - ctx: mongoParser.ArgumentContext | mongoParser.PropertyValueContext, - ): void { - const command = this.commands[this.commands.length - 1]; - command.errors = command.errors || []; - const stop = nonNullProp(ctx, 'stop', 'ctx.stop', 'ScrapbookHelpers.ts'); - const currentErrorDesc: ErrorDescription = { - message: errorMessage, - range: new vscode.Range( - ctx.start.line - 1, - ctx.start.charPositionInLine, - stop.line - 1, - stop.charPositionInLine, - ), - }; - command.errors.push(currentErrorDesc); - } - - private regexToStringNotation(pattern: string): string { - // The equivalence: - // /ker\b/ <=> $regex: "ker\\b", /ker\\b/ <=> "ker\\\\b" - return pattern.replace(/\\([0-9a-z.*])/i, '\\\\$1'); - } - - private deduplicateEscapesForRegex(argAsString: string): string { - const removeDuplicatedBackslash = /\\{4}([0-9a-z.*])/gi; - /* - We remove duplicate backslashes due the behavior of '\b' - \b in a regex denotes word boundary, while \b in a string denotes backspace. - $regex syntax uses a string. Strings require slashes to be escaped, while /regex/ does not. Eg. /abc+\b/ is equivalent to {$regex: "abc+\\b"}. - {$regex: "abc+\b"} with an unescaped slash gets parsed as {$regex: }. The user can only type '\\b' (which is encoded as '\\\\b'). - We need to convert this appropriately. Other special characters (\n, \t, \r) don't carry significance in regexes - we don't handle those - What the regex does: '\\{4}' looks for the escaped slash 4 times. Lookahead checks if the character being escaped has a special meaning. - */ - return argAsString.replace(removeDuplicatedBackslash, '\\\\$1'); - } -} diff --git a/src/documentdb/scrapbook/ScrapbookService.ts b/src/documentdb/scrapbook/ScrapbookService.ts deleted file mode 100644 index b59639798..000000000 --- a/src/documentdb/scrapbook/ScrapbookService.ts +++ /dev/null @@ -1,295 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { openReadOnlyContent, type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import { EOL } from 'os'; -import * as vscode from 'vscode'; -import { ext } from '../../extensionVariables'; -import { type BaseClusterModel, type TreeCluster } from '../../tree/models/BaseClusterModel'; -import { type EmulatorConfiguration } from '../../utils/emulatorConfiguration'; -import { type DatabaseItemModel } from '../ClustersClient'; -import { CredentialCache } from '../CredentialCache'; -import { AuthMethodId } from '../auth/AuthMethod'; -import { type MongoCommand } from './MongoCommand'; -import { findCommandAtPosition, getAllCommandsFromText } from './ScrapbookHelpers'; -import { ShellScriptRunner } from './ShellScriptRunner'; -import { MongoCodeLensProvider } from './services/MongoCodeLensProvider'; - -export class ScrapbookServiceImpl { - //-------------------------------------------------------------------------------- - // Connection Management - //-------------------------------------------------------------------------------- - - private _cluster: TreeCluster | undefined; - private _database: DatabaseItemModel | undefined; - private readonly _mongoCodeLensProvider = new MongoCodeLensProvider(); - - /** - * Provides a CodeLens provider for the workspace. - */ - public getCodeLensProvider(): MongoCodeLensProvider { - return this._mongoCodeLensProvider; - } - - /** - * Sets the current cluster and database, updating the CodeLens provider. - */ - public async setConnectedCluster(cluster: TreeCluster, database: DatabaseItemModel) { - if (CredentialCache.getCredentials(cluster.clusterId)?.authMechanism !== AuthMethodId.NativeAuth) { - throw Error( - l10n.t('Unsupported authentication mechanism. Only SCRAM-SHA-256 (username/password) is supported.'), - ); - } - - // Update information - this._cluster = cluster; - this._database = database; - this._mongoCodeLensProvider.updateCodeLens(); - - // Update the Language Client/Server - // The language server needs credentials to connect to the cluster.. - // emulatorConfiguration is only available on ConnectionClusterModel (Connections View) - const emulatorConfig = - 'emulatorConfiguration' in cluster - ? (cluster.emulatorConfiguration as EmulatorConfiguration | undefined) - : undefined; - await ext.mongoLanguageClient.connect( - CredentialCache.getConnectionStringWithPassword(this._cluster.clusterId), - this._database.name, - emulatorConfig, - ); - } - - /** - * Clears the current connection. - */ - public async clearConnection() { - this._cluster = undefined; - this._database = undefined; - this._mongoCodeLensProvider.updateCodeLens(); - await ext.mongoLanguageClient.disconnect(); - } - - /** - * Returns true if a cluster and database are set. - */ - public isConnected(): boolean { - return !!this._cluster && !!this._database; - } - - /** - * Returns the current database name. - */ - public getDatabaseName(): string | undefined { - return this._database?.name; - } - - /** - * Returns the current cluster ID (stable identifier for caching). - */ - public getClusterId(): string | undefined { - return this._cluster?.clusterId; - } - - /** - * Returns a friendly display name of the connected cluster/database. - */ - public getDisplayName(): string | undefined { - return this._cluster && this._database ? `${this._cluster.name}/${this._database.name}` : undefined; - } - - //-------------------------------------------------------------------------------- - // Command Execution - //-------------------------------------------------------------------------------- - - private _isExecutingAllCommands: boolean = false; - private _singleCommandInExecution: MongoCommand | undefined; - - /** - * Executes all Mongo commands in the given document. - * - * Note: This method will call use() before executing the commands to - * ensure that the commands are run in the correct database. It's done for backwards - * compatibility with the previous behavior. - */ - public async executeAllCommands(context: IActionContext, document: vscode.TextDocument): Promise { - if (!this.isConnected()) { - throw new Error(l10n.t('Please connect to a MongoDB database before running a Scrapbook command.')); - } - - const commands: MongoCommand[] = getAllCommandsFromText(document.getText()); - if (!commands.length) { - void vscode.window.showInformationMessage(l10n.t('No commands found in this document.')); - return; - } - - this.setExecutingAllCommandsFlag(true); - try { - const label = 'Scrapbook-run-all-results'; - const fullId = `${this.getDisplayName()}/${label}`; - - const readOnlyContent = await openReadOnlyContent({ label, fullId }, '', '.json', { - viewColumn: vscode.ViewColumn.Beside, - preserveFocus: true, - }); - - const shellRunner = await ShellScriptRunner.createShell(context, { - connectionString: CredentialCache.getConnectionStringWithPassword(this.getClusterId()!), - emulatorConfiguration: CredentialCache.getEmulatorConfiguration(this.getClusterId()!), - }); - - try { - // preselect the database for the user - // this is done for backwards compatibility with the previous behavior - await shellRunner.executeScript(`use(\`${ScrapbookService.getDatabaseName()}\`)`); - - for (const cmd of commands) { - await this.executeSingleCommand(context, cmd, readOnlyContent, shellRunner); - } - } finally { - shellRunner.dispose(); - } - } finally { - this.setExecutingAllCommandsFlag(false); - } - } - - /** - * Executes a single Mongo command defined at the specified position in the document. - * - * Note: This method will call use() before executing the command to - * ensure that the command are is in the correct database. It's done for backwards - * compatibility with the previous behavior. - */ - public async executeCommandAtPosition( - context: IActionContext, - document: vscode.TextDocument, - position: vscode.Position, - ): Promise { - if (!this.isConnected()) { - throw new Error(l10n.t('Please connect to a MongoDB database before running a Scrapbook command.')); - } - - const commands = getAllCommandsFromText(document.getText()); - const command = findCommandAtPosition(commands, position); - - const label = 'Scrapbook-run-command-results'; - const fullId = `${this.getDisplayName()}/${label}`; - const readOnlyContent = await openReadOnlyContent({ label, fullId }, '', '.json', { - viewColumn: vscode.ViewColumn.Beside, - preserveFocus: true, - }); - - await this.executeSingleCommand(context, command, readOnlyContent, undefined, this.getDatabaseName()); - } - - /** - * Indicates whether multiple commands are being executed at once. - */ - public isExecutingAllCommands(): boolean { - return this._isExecutingAllCommands; - } - - /** - * Records the state for whether all commands are executing. - */ - public setExecutingAllCommandsFlag(state: boolean): void { - this._isExecutingAllCommands = state; - this._mongoCodeLensProvider.updateCodeLens(); - } - - /** - * Returns the command currently in execution, if any. - */ - public getSingleCommandInExecution(): MongoCommand | undefined { - return this._singleCommandInExecution; - } - - /** - * Sets or clears the command currently being executed. - */ - public setSingleCommandInExecution(command: MongoCommand | undefined): void { - this._singleCommandInExecution = command; - this._mongoCodeLensProvider.updateCodeLens(); - } - - //-------------------------------------------------------------------------------- - // Internal Helpers - //-------------------------------------------------------------------------------- - - /** - * Runs a single command against the Mongo shell. If a shell instance is not provided, - * this method creates its own, executes the command, then disposes the shell. This - * includes error handling for parse problems, ephemeral shell usage, and optional - * output to a read-only content view. - */ - private async executeSingleCommand( - context: IActionContext, - command: MongoCommand, - readOnlyContent?: { append(value: string): Promise }, - shellRunner?: ShellScriptRunner, - preselectedDatabase?: string, // this will run the 'use ' command before the actual command. - ): Promise { - if (!this.isConnected()) { - throw new Error(l10n.t('Not connected to any MongoDB database.')); - } - - if (command.errors?.length) { - const firstErr = command.errors[0]; - throw new Error( - l10n.t('Unable to parse syntax near line {line}, col {column}: {message}', { - line: firstErr.range.start.line + 1, - column: firstErr.range.start.character + 1, - message: firstErr.message, - }), - ); - } - - this.setSingleCommandInExecution(command); - let ephemeralShell = false; - - try { - if (!shellRunner) { - shellRunner = await ShellScriptRunner.createShell(context, { - connectionString: CredentialCache.getConnectionStringWithPassword(this.getClusterId()!), - emulatorConfiguration: CredentialCache.getEmulatorConfiguration( - this.getClusterId()!, - ) as EmulatorConfiguration, - }); - ephemeralShell = true; - } - - if (preselectedDatabase) { - await shellRunner.executeScript(`use(\`${preselectedDatabase}\`)`); - } - - const result = await shellRunner.executeScript(command.text); - if (!result) { - throw new Error(l10n.t('No result returned from the MongoDB shell.')); - } - - if (readOnlyContent) { - await readOnlyContent.append(result + EOL + EOL); - } else { - const fallbackLabel = 'Scrapbook-results'; - const fallbackId = `${this.getDatabaseName()}/${fallbackLabel}`; - await openReadOnlyContent({ label: fallbackLabel, fullId: fallbackId }, result, '.json', { - viewColumn: vscode.ViewColumn.Beside, - preserveFocus: true, - }); - } - } finally { - this.setSingleCommandInExecution(undefined); - - if (ephemeralShell) { - shellRunner?.dispose(); - } - } - } -} - -// Export a single instance that the rest of your extension can import -export const ScrapbookService = new ScrapbookServiceImpl(); diff --git a/src/documentdb/scrapbook/ShellScriptRunner.ts b/src/documentdb/scrapbook/ShellScriptRunner.ts deleted file mode 100644 index 61972a432..000000000 --- a/src/documentdb/scrapbook/ShellScriptRunner.ts +++ /dev/null @@ -1,486 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { parseError, UserCancelledError, type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import * as fs from 'node:fs/promises'; -import * as os from 'os'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { ext } from '../../extensionVariables'; -import * as cpUtils from '../../utils/cp'; -import { type EmulatorConfiguration } from '../../utils/emulatorConfiguration'; -import { pathExists } from '../../utils/fs/pathExists'; -import { InteractiveChildProcess } from '../../utils/InteractiveChildProcess'; -import { nonNullValue } from '../../utils/nonNull'; -import { randomUtils } from '../../utils/randomUtils'; -import { getBatchSizeSetting } from '../../utils/workspacUtils'; -import { wrapError } from '../../utils/wrapError'; - -const mongoExecutableFileName = process.platform === 'win32' ? 'mongo.exe' : 'mongosh'; - -const timeoutMessage = l10n.t( - "Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting.", -); - -const mongoShellMoreMessage = l10n.t('Type "it" for more'); -const extensionMoreMessage = '(More)'; - -const sentinelBase = 'EXECUTION COMPLETED'; -const sentinelRegex = /"?EXECUTION COMPLETED [0-9a-fA-F]{10}"?/; -function createSentinel(): string { - return `${sentinelBase} ${randomUtils.getRandomHexString(10)}`; -} - -export class ShellScriptRunner extends vscode.Disposable { - private static _previousShellPathSetting: string | undefined; - private static _cachedShellPathOrCmd: string | undefined; - - private constructor( - private _process: InteractiveChildProcess, - private _timeoutSeconds: number, - ) { - super(() => this.dispose()); - } - - public static async createShellProcessHelper( - execPath: string, - execArgs: string[], - connectionString: string, - outputChannel: vscode.OutputChannel, - timeoutSeconds: number, - emulatorConfiguration?: EmulatorConfiguration, - ): Promise { - try { - const args: string[] = execArgs.slice() || []; // Snapshot since we modify it - args.push(connectionString); - - if ( - emulatorConfiguration && - emulatorConfiguration.isEmulator && - emulatorConfiguration.disableEmulatorSecurity - ) { - // Without these the connection will fail due to the self-signed Document DB certificate - if (args.indexOf('--tlsAllowInvalidCertificates') < 0) { - args.push('--tlsAllowInvalidCertificates'); - } - } - - const process: InteractiveChildProcess = await InteractiveChildProcess.create({ - outputChannel: outputChannel, - command: execPath, - args, - outputFilterSearch: sentinelRegex, - outputFilterReplace: '', - }); - const shell: ShellScriptRunner = new ShellScriptRunner(process, timeoutSeconds); - - /** - * The 'unwrapIfCursor' helper is used to safely handle MongoDB queries in the shell, - * especially for commands like db.movies.find() that return a cursor. - * - * When a user runs a command returning a cursor, it points to a query's result set - * and exposes methods such as hasNext and next. Attempting to stringify - * the raw cursor directly with EJSON.stringify can fail due to circular references - * and other internal structures. - * - * To avoid this issue, 'unwrapIfCursor' checks if the returned object is indeed a - * cursor. If it is, we manually iterate up to a fixed limit of documents, and - * return those as a plain array. This prevents the shell from crashing or throwing - * errors about circular structures, while still returning actual document data in - * JSON format. - * - * For non-cursor commands (like db.hostInfo() or db.movies.findOne()), we - * simply return the object unchanged. - */ - const unwrapIfCursorFunction = - 'function unwrapIfCursor(value) {\n' + - " if (value && typeof value.hasNext === 'function' && typeof value.next === 'function') {\n" + - ' const docs = [];\n' + - ' const MAX_DOCS = 50;\n' + - ' let count = 0;\n' + - ' while (value.hasNext() && count < MAX_DOCS) {\n' + - ' docs.push(value.next());\n' + - ' count++;\n' + - ' }\n' + - ' if (value.hasNext()) {\n' + - ' docs.push({ cursor: "omitted", note: "Additional results are not displayed." });\n' + - ' }\n' + - ' return docs;\n' + - ' }\n' + - ' return value;\n' + - '}'; - process.writeLine(`${convertToSingleLine(unwrapIfCursorFunction)}`); - - // Try writing an empty script to verify the process is running correctly and allow us - // to catch any errors related to the start-up of the process before trying to write to it. - await shell.executeScript(''); - - ext.outputChannel.appendLine(l10n.t('Mongo Shell connected.')); - - // Configure the batch size - await shell.executeScript(`config.set("displayBatchSize", ${getBatchSizeSetting()})`); - - return shell; - } catch (error) { - throw wrapCheckOutputWindow(error); - } - } - - public static async createShell( - context: IActionContext, - connectionInfo: { connectionString: string; emulatorConfiguration?: EmulatorConfiguration }, - ): Promise { - const config = vscode.workspace.getConfiguration(); - let shellPath: string | undefined = config.get(ext.settingsKeys.shellPath); - const shellArgs: string[] = config.get(ext.settingsKeys.shellArgs, []); - - if ( - !shellPath || - !ShellScriptRunner._cachedShellPathOrCmd || - ShellScriptRunner._previousShellPathSetting !== shellPath - ) { - // Only do this if setting changed since last time - shellPath = await ShellScriptRunner._determineShellPathOrCmd(context, shellPath); - ShellScriptRunner._previousShellPathSetting = shellPath; - } - ShellScriptRunner._cachedShellPathOrCmd = shellPath; - - const timeout = - 1000 * - nonNullValue( - config.get(ext.settingsKeys.shellTimeout), - 'config.get(ext.settingsKeys.shellTimeout)', - 'ShellScriptRunner.ts', - ); - return ShellScriptRunner.createShellProcessHelper( - shellPath, - shellArgs, - connectionInfo.connectionString, - ext.outputChannel, - timeout, - connectionInfo.emulatorConfiguration, - ); - } - - public dispose(): void { - this._process.kill(); - } - - public async useDatabase(database: string): Promise { - return await this.executeScript(`use ${database}`); - } - - public async executeScript(script: string): Promise { - // 1. Convert to single line (existing logic) - script = convertToSingleLine(script); - - // 2. If the user typed something, wrap it in EJSON.stringify(...) - // This assumes the user has typed exactly one expression that - // returns something (e.g. db.hostInfo(), db.myCollection.find(), etc.) - if (script.trim().length > 0 && !script.startsWith('print(EJSON.stringify(')) { - // Remove trailing semicolons plus any trailing space - // e.g. "db.hostInfo(); " => "db.hostInfo()" - script = script.replace(/;+\s*$/, ''); - - // Wrap in EJSON.stringify() and unwrapIfCursor - script = `print(EJSON.stringify(unwrapIfCursor(${script}), null, 4))`; - } - - let stdOut = ''; - const sentinel = createSentinel(); - - const disposables: vscode.Disposable[] = []; - try { - // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor - const result = await new Promise(async (resolve, reject) => { - try { - startScriptTimeout(this._timeoutSeconds, reject); - - // Hook up events - disposables.push( - this._process.onStdOut((text) => { - stdOut += text; - // eslint-disable-next-line prefer-const - let { text: stdOutNoSentinel, removed } = removeSentinel(stdOut, sentinel); - if (removed) { - // The sentinel was found, which means we are done. - - // Change the "type 'it' for more" message to one that doesn't ask users to type anything, - // since we're not currently interactive like that. - // CONSIDER: Ideally we would allow users to click a button to iterate through more data, - // or even just do it for them - stdOutNoSentinel = stdOutNoSentinel.replace( - mongoShellMoreMessage, - extensionMoreMessage, - ); - - const responseText = removePromptLeadingAndTrailing(stdOutNoSentinel); - - resolve(responseText); - } - }), - ); - disposables.push( - this._process.onStdErr((text) => { - // Mongo shell only writes to STDERR for errors relating to starting up. Script errors go to STDOUT. - // So consider this an error. - // (It's okay if we fire this multiple times, the first one wins.) - - // Split the stderr text into lines, trim them, and remove empty lines - const lines: string[] = text - .split(/\r?\n/) - .map((l) => l.trim()) - .filter(Boolean); - - // Filter out lines recognized as benign debug/telemetry info - const unknownErrorLines: string[] = lines.filter( - (line) => !this.isNonErrorMongoshStderrLine(line), - ); - - // If there are any lines left after filtering, assume they are real errors - if (unknownErrorLines.length > 0) { - for (const line of unknownErrorLines) { - ext.outputChannel.appendLine(l10n.t('Mongo Shell Error: {error}', line)); - } - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(wrapCheckOutputWindow(unknownErrorLines.join('\n'))); - } else { - // Otherwise, ignore the lines since they're known safe - // (e.g. "Debugger listening on ws://..." or "Using Mongosh: 1.9.0", etc.) - } - }), - ); - disposables.push( - this._process.onError((error) => { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(error); - }), - ); - - // Write the script to STDIN - if (script) { - this._process.writeLine(script); - } - - // Mark end of result by sending the sentinel wrapped in quotes so the console will spit - // it back out as a string value after it's done processing the script - const quotedSentinel = `"${sentinel}"`; - this._process.writeLine(quotedSentinel); // (Don't display the sentinel) - } catch (error) { - // new Promise() doesn't seem to catch exceptions in an async function, we need to explicitly reject it - - if ((<{ code?: string }>error).code === 'EPIPE') { - // Give a chance for start-up errors to show up before rejecting with this more general error message - await delay(500); - // eslint-disable-next-line no-ex-assign - error = new Error(l10n.t('The process exited prematurely.')); - } - - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(wrapCheckOutputWindow(error)); - } - }); - - return result.trim(); - } finally { - // Dispose event handlers - for (const d of disposables) { - d.dispose(); - } - } - } - - /** - * Checks if the stderr line from mongosh is a known "benign" message that - * should NOT be treated as an error. - */ - private isNonErrorMongoshStderrLine(line: string): boolean { - /** - * Certain versions of mongosh can print debug or telemetry messages to stderr - * that are not actually errors (especially if VS Code auto-attach is running). - * Below is a list of known message fragments that we can safely ignore. - * - * IMPORTANT: This list is not exhaustive and may need to be updated as new - * versions of mongosh introduce new messages. - */ - const knownNonErrorSubstrings: string[] = [ - // Node.js Inspector (auto-attach) messages: - 'Debugger listening on ws://', - 'Debugger attached.', - 'For help, see: https://nodejs.org/en/docs/inspector', - - // MongoDB Shell general info messages: - 'Current Mongosh Log ID:', - 'Using Mongosh:', - 'Using MongoDB:', - - // Telemetry or analytics prompts: - 'To enable telemetry, run:', - 'Disable telemetry by running:', - - // Occasionally, devtools or local shell info: - 'DevTools listening on ws://', - 'The server generated these startup warnings:', - ]; - - return knownNonErrorSubstrings.some((pattern) => line.includes(pattern)); - } - - private static async _determineShellPathOrCmd( - context: IActionContext, - shellPathSetting: string | undefined, - ): Promise { - if (!shellPathSetting) { - // User hasn't specified the path - if (await cpUtils.commandSucceeds('mongosh', '--version')) { - // If the user already has mongo in their system path, just use that - return 'mongosh'; - } else { - // If all else fails, prompt the user for the mongo path - const openFile: vscode.MessageItem = { - title: l10n.t('Browse to {mongoExecutableFileName}', { mongoExecutableFileName }), - }; - const browse: vscode.MessageItem = { title: l10n.t('Open installation page') }; - const noMongoError: string = l10n.t( - 'This functionality requires the Mongo DB shell, but we could not find it in the path or using the documentDB.mongoShell.path setting.', - ); - const response = await context.ui.showWarningMessage( - noMongoError, - { stepName: 'promptForMongoPath' }, - browse, - openFile, - ); - if (response === openFile) { - while (true) { - const newPath: vscode.Uri[] = await context.ui.showOpenDialog({ - filters: { 'Executable Files': [process.platform === 'win32' ? 'exe' : ''] }, - openLabel: l10n.t('Select {mongoExecutableFileName}', { mongoExecutableFileName }), - stepName: 'openMongoExeFile', - }); - const fsPath = newPath[0].fsPath; - const baseName = path.basename(fsPath); - if (baseName !== mongoExecutableFileName) { - const useAnyway: vscode.MessageItem = { title: l10n.t('Use anyway') }; - const tryAgain: vscode.MessageItem = { title: l10n.t('Try again') }; - const response2 = await context.ui.showWarningMessage( - l10n.t( - 'Expected a file name "{0}", but the selected filename is "{1}"', - mongoExecutableFileName, - baseName, - ), - { stepName: 'confirmMongoExeFile' }, - useAnyway, - tryAgain, - ); - if (response2 === tryAgain) { - continue; - } - } - - await vscode.workspace - .getConfiguration() - .update(ext.settingsKeys.shellPath, fsPath, vscode.ConfigurationTarget.Global); - return fsPath; - } - } else if (response === browse) { - void vscode.commands.executeCommand( - 'vscode.open', - vscode.Uri.parse('https://docs.mongodb.com/manual/installation/'), - ); - // default down to cancel error because MongoShell.create errors out if undefined is passed as the shellPath - } - - throw new UserCancelledError('createShell'); - } - } else { - // User has specified the path or command. Sometimes they set the folder instead of a path to the file, let's check that and auto fix - if (await pathExists(shellPathSetting)) { - const stat = await fs.stat(shellPathSetting); - if (stat.isDirectory()) { - return path.join(shellPathSetting, mongoExecutableFileName); - } - } - - return shellPathSetting; - } - } -} - -function startScriptTimeout(timeoutSeconds: number, reject: (err: unknown) => void): void { - if (timeoutSeconds > 0) { - setTimeout(() => { - reject(timeoutMessage); - }, timeoutSeconds * 1000); - } -} - -function convertToSingleLine(script: string): string { - return script - .split(os.EOL) - .map((line) => line.trim()) - .join(''); -} - -function removeSentinel(text: string, sentinel: string): { text: string; removed: boolean } { - const index = text.indexOf(sentinel); - if (index >= 0) { - return { text: text.slice(0, index), removed: true }; - } else { - return { text, removed: false }; - } -} - -/** - * Removes a Mongo shell prompt line if it exists at the very start or the very end of `text`. - */ -function removePromptLeadingAndTrailing(text: string): string { - // Trim trailing spaces/newlines, but keep internal newlines. - text = text.replace(/\s+$/, ''); - - // Regex to detect standard MongoDB shell prompts: - // 1) [mongos] secondDb> - // 2) [mongo] test> - // 3) globaldb [primary] SampleDB> - const promptRegex = /^(\[mongo.*?\].*?>|.*?\[.*?\]\s+\S+>)$/; - - // Check if the *first line* contains a prompt - const firstNewlineIndex = text.indexOf('\n'); - if (firstNewlineIndex === -1) { - return text.replace(promptRegex, '').trim(); - } - - // Extract the first line - const firstLine = text.substring(0, firstNewlineIndex).trim(); - if (promptRegex.test(firstLine)) { - // Remove the prompt from the first line - text = text.replace(firstLine, firstLine.replace(promptRegex, '').trim()); - } - - // Check if the *last line* contains a prompt - const lastNewlineIndex = text.lastIndexOf('\n'); - if (lastNewlineIndex === -1) { - return text.replace(promptRegex, '').trim(); - } - - const lastLine = text.substring(lastNewlineIndex + 1).trim(); - if (promptRegex.test(lastLine)) { - // Remove the prompt from the last line - text = text.replace(lastLine, lastLine.replace(promptRegex, '').trim()); - } - - return text; -} - -async function delay(milliseconds: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, milliseconds); - }); -} - -function wrapCheckOutputWindow(error: unknown): unknown { - const checkOutputMsg = l10n.t('The output window may contain additional information.'); - return parseError(error).message.includes(checkOutputMsg) ? error : wrapError(error, checkOutputMsg); -} diff --git a/src/documentdb/scrapbook/errorListeners.ts b/src/documentdb/scrapbook/errorListeners.ts deleted file mode 100644 index ea4c11ff6..000000000 --- a/src/documentdb/scrapbook/errorListeners.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type ANTLRErrorListener } from 'antlr4ts/ANTLRErrorListener'; -import { type RecognitionException } from 'antlr4ts/RecognitionException'; -import { type Recognizer } from 'antlr4ts/Recognizer'; -import { type Token } from 'antlr4ts/Token'; -import * as vscode from 'vscode'; -import { type ErrorDescription } from './MongoCommand'; - -export class ParserErrorListener implements ANTLRErrorListener { - private _errors: ErrorDescription[] = []; - - public get errors(): ErrorDescription[] { - return this._errors; - } - - public syntaxError( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _recognizer: Recognizer, - _offendingSymbol: Token | undefined, - line: number, - charPositionInLine: number, - msg: string, - e: RecognitionException | undefined, - ): void { - const position = new vscode.Position(line - 1, charPositionInLine); // Symbol lines are 1-indexed. Position lines are 0-indexed - const range = new vscode.Range(position, position); - - const error: ErrorDescription = { - message: msg, - range: range, - exception: e, - }; - this._errors.push(error); - } -} - -export class LexerErrorListener implements ANTLRErrorListener { - private _errors: ErrorDescription[] = []; - - public get errors(): ErrorDescription[] { - return this._errors; - } - - public syntaxError( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _recognizer: Recognizer, - _offendingSymbol: number | undefined, - line: number, - charPositionInLine: number, - msg: string, - e: RecognitionException | undefined, - ): void { - const position = new vscode.Position(line - 1, charPositionInLine); // Symbol lines are 1-indexed. Position lines are 0-indexed - const range = new vscode.Range(position, position); - - const error: ErrorDescription = { - message: msg, - range: range, - exception: e, - }; - this._errors.push(error); - } -} diff --git a/src/documentdb/scrapbook/languageClient.ts b/src/documentdb/scrapbook/languageClient.ts deleted file mode 100644 index 75b5afd4c..000000000 --- a/src/documentdb/scrapbook/languageClient.ts +++ /dev/null @@ -1,88 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* eslint-disable import/no-internal-modules */ - -import { appendExtensionUserAgent } from '@microsoft/vscode-azext-utils'; -import * as path from 'path'; -import { - LanguageClient, - TransportKind, - type LanguageClientOptions, - type ServerOptions, -} from 'vscode-languageclient/node'; -import { ext } from '../../extensionVariables'; -import { type EmulatorConfiguration } from '../../utils/emulatorConfiguration'; -import { type IConnectionParams } from './services/IConnectionParams'; - -export class MongoDBLanguageClient { - public client: LanguageClient; - - constructor() { - // The server is implemented in node - const serverPath = ext.isBundle - ? path.join('vscode-documentdb-scrapbook-language-languageServer.bundle.js') // Run with webpack - : path.join('out', 'src', 'documentdb', 'scrapbook', 'languageServer.js'); // Run without webpack - const serverModule = ext.context.asAbsolutePath(serverPath); - // The debug options for the server - const debugOptions = { execArgv: ['--nolazy', '--inspect=6005'] }; - - // If the extension is launch in debug mode the debug server options are use - // Otherwise the run options are used - const serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, - }; - - // Options to control the language client - const clientOptions: LanguageClientOptions = { - // Register the server for mongo javascript documents - documentSelector: [ - { language: 'vscode-documentdb-scrapbook-language', scheme: 'file' }, - { language: 'vscode-documentdb-scrapbook-language', scheme: 'untitled' }, - ], - }; - - // Create the language client. - this.client = new LanguageClient( - 'vscode-documentdb-scrapbook-language', - 'DocumentDB Language Server', - serverOptions, - clientOptions, - ); - - // Push the disposable to the context's subscriptions so that the - // client can be deactivated on extension deactivation - ext.context.subscriptions.push({ - dispose: async () => { - try { - await this.client?.stop(); - } catch (error) { - console.error('Failed to stop the language client:', error); - } - }, - }); - - // Start the client. This will also launch the server - void this.client.start(); - } - - public async connect( - connectionString: string, - databaseName: string, - emulatorConfiguration?: EmulatorConfiguration, - ): Promise { - await this.client.sendRequest('connect', { - connectionString: connectionString, - databaseName: databaseName, - extensionUserAgent: appendExtensionUserAgent(), - emulatorConfiguration: emulatorConfiguration, - }); - } - - public async disconnect(): Promise { - await this.client.sendRequest('disconnect'); - } -} diff --git a/src/documentdb/scrapbook/registerScrapbookCommands.ts b/src/documentdb/scrapbook/registerScrapbookCommands.ts deleted file mode 100644 index c985fa817..000000000 --- a/src/documentdb/scrapbook/registerScrapbookCommands.ts +++ /dev/null @@ -1,116 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { - callWithTelemetryAndErrorHandling, - registerCommandWithTreeNodeUnwrapping, - registerErrorHandler, - registerEvent, - type IActionContext, - type IErrorHandlerContext, -} from '@microsoft/vscode-azext-utils'; -import * as vscode from 'vscode'; -import { connectCluster } from '../../commands/scrapbook-commands/connectCluster'; -import { createScrapbook } from '../../commands/scrapbook-commands/createScrapbook'; -import { executeAllCommand } from '../../commands/scrapbook-commands/executeAllCommand'; -import { executeCommand } from '../../commands/scrapbook-commands/executeCommand'; -import { ext } from '../../extensionVariables'; -import { withTreeNodeCommandCorrelation } from '../../utils/commandTelemetry'; -import { MongoConnectError } from './connectToClient'; -import { MongoDBLanguageClient } from './languageClient'; -import { getAllErrorsFromTextDocument } from './ScrapbookHelpers'; -import { ScrapbookService } from './ScrapbookService'; - -let diagnosticsCollection: vscode.DiagnosticCollection; -const scrapbookLanguageId: string = 'vscode-documentdb-scrapbook-language'; - -export function registerScrapbookCommands(): void { - ext.mongoLanguageClient = new MongoDBLanguageClient(); - - ext.context.subscriptions.push( - vscode.languages.registerCodeLensProvider(scrapbookLanguageId, ScrapbookService.getCodeLensProvider()), - ); - - diagnosticsCollection = vscode.languages.createDiagnosticCollection('documentDB.vscode-documentdb-scrapbook'); - ext.context.subscriptions.push(diagnosticsCollection); - - setUpErrorReporting(); - - registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.scrapbook.new', - withTreeNodeCommandCorrelation(createScrapbook), - ); - registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.scrapbook.executeCommand', - withTreeNodeCommandCorrelation(executeCommand), - ); - registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.scrapbook.executeAllCommands', - withTreeNodeCommandCorrelation(executeAllCommand), - ); - - // #region Database command - - registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.scrapbook.connect', - withTreeNodeCommandCorrelation(connectCluster), - ); - - // #endregion -} - -function setUpErrorReporting(): void { - // Update errors immediately in case a scrapbook is already open - void callWithTelemetryAndErrorHandling( - 'scrapbook.initialUpdateErrorsInActiveDocument', - async (context: IActionContext) => { - updateErrorsInScrapbook(context, vscode.window.activeTextEditor?.document); - }, - ); - - // Update errors when document opened/changed - registerEvent( - 'vscode.workspace.onDidOpenTextDocument', - vscode.workspace.onDidOpenTextDocument, - updateErrorsInScrapbook, - ); - registerEvent( - 'vscode.workspace.onDidChangeTextDocument', - vscode.workspace.onDidChangeTextDocument, - async (context: IActionContext, event: vscode.TextDocumentChangeEvent) => { - // Always suppress success telemetry - event happens on every keystroke - context.telemetry.suppressIfSuccessful = true; - - updateErrorsInScrapbook(context, event.document); - }, - ); - registerEvent( - 'vscode.workspace.onDidCloseTextDocument', - vscode.workspace.onDidCloseTextDocument, - async (context: IActionContext, document: vscode.TextDocument) => { - // Remove errors when closed - if (document?.languageId === scrapbookLanguageId) { - diagnosticsCollection.set(document.uri, []); - } else { - context.telemetry.suppressIfSuccessful = true; - } - }, - ); - - registerErrorHandler((context: IErrorHandlerContext) => { - if (context.error instanceof MongoConnectError) { - context.errorHandling.suppressReportIssue = true; - } - }); -} - -function updateErrorsInScrapbook(context: IActionContext, document: vscode.TextDocument | undefined): void { - if (document?.languageId === scrapbookLanguageId) { - const errors = getAllErrorsFromTextDocument(document); - diagnosticsCollection.set(document.uri, errors); - } else { - context.telemetry.suppressIfSuccessful = true; - } -} diff --git a/src/documentdb/scrapbook/services/IConnectionParams.ts b/src/documentdb/scrapbook/services/IConnectionParams.ts deleted file mode 100644 index 51ed0c778..000000000 --- a/src/documentdb/scrapbook/services/IConnectionParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type EmulatorConfiguration } from '../../../utils/emulatorConfiguration'; - -export interface IConnectionParams { - connectionString: string; - databaseName: string; - extensionUserAgent: string; - emulatorConfiguration?: EmulatorConfiguration; -} diff --git a/src/documentdb/scrapbook/services/MongoCodeLensProvider.ts b/src/documentdb/scrapbook/services/MongoCodeLensProvider.ts deleted file mode 100644 index 0372c23e9..000000000 --- a/src/documentdb/scrapbook/services/MongoCodeLensProvider.ts +++ /dev/null @@ -1,112 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import * as vscode from 'vscode'; -import { getAllCommandsFromText } from '../ScrapbookHelpers'; -import { ScrapbookService } from '../ScrapbookService'; - -/** - * Provides Code Lens functionality for the Mongo Scrapbook editor. - * - * @remarks - * This provider enables several helpful actions directly within the editor: - * - * 1. **Connection Status Lens**: - * - Displays the current database connection state (e.g., connecting, connected). - * - Offers the ability to connect to a MongoDB database if one is not yet connected. - * - * 2. **Execute All Commands Lens**: - * - Runs all detected MongoDB commands in the scrapbook document at once when triggered. - * - * 3. **Execute Single Command Lens**: - * - Appears for each individual MongoDB command found in the scrapbook. - * - Invokes execution of the command located at the specified range in the document. - */ -export class MongoCodeLensProvider implements vscode.CodeLensProvider { - private _onDidChangeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); - - /** - * An event to signal that the code lenses from this provider have changed. - */ - public get onDidChangeCodeLenses(): vscode.Event { - return this._onDidChangeEmitter.event; - } - - public updateCodeLens(): void { - this._onDidChangeEmitter.fire(); - } - public provideCodeLenses( - document: vscode.TextDocument, - _token: vscode.CancellationToken, - ): vscode.ProviderResult { - return callWithTelemetryAndErrorHandling('scrapbook.provideCodeLenses', (context: IActionContext) => { - context.telemetry.suppressIfSuccessful = true; - - const lenses: vscode.CodeLens[] = []; - - // Create connection status lens - lenses.push(this.createConnectionStatusLens()); - - // Create run-all lens - lenses.push(this.createRunAllCommandsLens()); - - // Create lenses for each individual command - const commands = getAllCommandsFromText(document.getText()); - lenses.push(...this.createIndividualCommandLenses(commands)); - - return lenses; - }); - } - - private createConnectionStatusLens(): vscode.CodeLens { - const title = ScrapbookService.isConnected() - ? l10n.t('Connected to "{name}"', { name: ScrapbookService.getDisplayName() ?? '' }) - : l10n.t('Connect to a database'); - - const shortenedTitle = - title.length > 64 ? title.slice(0, 64 / 2) + 'โ€ฆ' + title.slice(-(64 - 3 - 64 / 2)) : title; - - return { - command: { - title: '๐ŸŒ ' + shortenedTitle, - tooltip: title, - command: 'vscode-documentdb.command.scrapbook.connect', - }, - range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), - }; - } - - private createRunAllCommandsLens(): vscode.CodeLens { - const title = ScrapbookService.isExecutingAllCommands() ? l10n.t('โณ Running Allโ€ฆ') : l10n.t('โฉ Run All'); - - return { - command: { - title, - command: 'vscode-documentdb.command.scrapbook.executeAllCommands', - }, - range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), - }; - } - - private createIndividualCommandLenses(commands: { range: vscode.Range }[]): vscode.CodeLens[] { - const currentCommandInExectution = ScrapbookService.getSingleCommandInExecution(); - - return commands.map((cmd) => { - const running = currentCommandInExectution && cmd.range.isEqual(currentCommandInExectution.range); - const title = running ? l10n.t('โณ Running Commandโ€ฆ') : l10n.t('โ–ถ๏ธ Run Command'); - - return { - command: { - title, - command: 'vscode-documentdb.command.scrapbook.executeCommand', - arguments: [cmd.range.start], - }, - range: cmd.range, - }; - }); - } -} diff --git a/src/documentdb/scrapbook/services/completionItemProvider.ts b/src/documentdb/scrapbook/services/completionItemProvider.ts deleted file mode 100644 index ad94fe2ff..000000000 --- a/src/documentdb/scrapbook/services/completionItemProvider.ts +++ /dev/null @@ -1,507 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -import { ParserRuleContext } from 'antlr4ts/ParserRuleContext'; -import { ErrorNode } from 'antlr4ts/tree/ErrorNode'; -import { type ParseTree } from 'antlr4ts/tree/ParseTree'; -import { TerminalNode } from 'antlr4ts/tree/TerminalNode'; -import { type Db } from 'mongodb'; -import { type LanguageService as JsonLanguageService } from 'vscode-json-languageservice'; -import { CompletionItemKind, Position, Range, type CompletionItem } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { mongoLexer } from '../../grammar/mongoLexer'; -import * as mongoParser from '../../grammar/mongoParser'; -import { MongoVisitor } from '../../grammar/visitors'; -import { type SchemaService } from './schemaService'; - -export class CompletionItemsVisitor extends MongoVisitor> { - private at: Position; - - constructor( - private textDocument: TextDocument, - private db: Db, - private offset: number, - private schemaService: SchemaService, - private jsonLanguageService: JsonLanguageService, - ) { - super(); - this.at = this.textDocument.positionAt(this.offset); - } - - public visitCommands(ctx: mongoParser.CommandsContext): Promise { - return this.thenable(this.createDbKeywordCompletion(this.createRange(ctx))); - } - - public visitEmptyCommand(ctx: mongoParser.EmptyCommandContext): Promise { - return this.thenable(this.createDbKeywordCompletion(this.createRangeAfter(ctx))); - } - - public visitCommand(ctx: mongoParser.CommandContext): Promise { - if (ctx.childCount === 0) { - return this.thenable(this.createDbKeywordCompletion(this.createRange(ctx))); - } - - const lastTerminalNode = this.getLastTerminalNode(ctx); - if (lastTerminalNode) { - return this.getCompletionItemsFromTerminalNode(lastTerminalNode); - } - return this.thenable(); - } - - public visitCollection(ctx: mongoParser.CollectionContext): Promise { - return Promise.all([ - this.createCollectionCompletions(this.createRange(ctx)), - this.createDbFunctionCompletions(this.createRange(ctx)), - ]).then(([collectionCompletions, dbFunctionCompletions]) => [ - ...collectionCompletions, - ...dbFunctionCompletions, - ]); - } - - public visitFunctionCall(ctx: mongoParser.FunctionCallContext): Promise { - const previousNode = this.getPreviousNode(ctx); - if (previousNode instanceof TerminalNode) { - return this.getCompletionItemsFromTerminalNode(previousNode); - } - return this.thenable(); - } - - public visitArguments(ctx: mongoParser.ArgumentsContext): Promise { - const terminalNode = this.getLastTerminalNode(ctx); - if (terminalNode && terminalNode.symbol === ctx._CLOSED_PARENTHESIS) { - return this.thenable(this.createDbKeywordCompletion(this.createRangeAfter(terminalNode))); - } - return this.thenable(); - } - - public visitArgument(ctx: mongoParser.ArgumentContext): Promise { - return ctx.parent!.accept(this); - } - - public visitObjectLiteral(ctx: mongoParser.ObjectLiteralContext): Thenable { - const functionName = this.getFunctionName(ctx); - const collectionName = this.getCollectionName(ctx); - if (collectionName && functionName) { - if ( - [ - 'find', - 'findOne', - 'findOneAndDelete', - 'findOneAndUpdate', - 'findOneAndReplace', - 'deleteOne', - 'deleteMany', - 'remove', - ].indexOf(functionName) !== -1 - ) { - return this.getArgumentCompletionItems( - this.schemaService.queryDocumentUri(collectionName), - collectionName, - ctx, - ); - } - } - return ctx.parent!.accept(this); - } - - public visitArrayLiteral(ctx: mongoParser.ArrayLiteralContext): Thenable { - const functionName = this.getFunctionName(ctx); - const collectionName = this.getCollectionName(ctx); - if (collectionName && functionName) { - if (['aggregate'].indexOf(functionName) !== -1) { - return this.getArgumentCompletionItems( - this.schemaService.aggregateDocumentUri(collectionName), - collectionName, - ctx, - ); - } - } - return ctx.parent!.accept(this); - } - - public visitElementList(ctx: mongoParser.ElementListContext): Promise { - return ctx.parent!.accept(this); - } - - public visitPropertyNameAndValueList(ctx: mongoParser.PropertyNameAndValueListContext): Promise { - return ctx.parent!.accept(this); - } - - public visitPropertyAssignment(ctx: mongoParser.PropertyAssignmentContext): Promise { - return ctx.parent!.accept(this); - } - - public visitPropertyValue(ctx: mongoParser.PropertyValueContext): Promise { - return ctx.parent!.accept(this); - } - - public visitPropertyName(ctx: mongoParser.PropertyNameContext): Promise { - return ctx.parent!.accept(this); - } - - public visitLiteral(ctx: mongoParser.LiteralContext): Promise { - return ctx.parent!.accept(this); - } - - public visitTerminal(ctx: TerminalNode): Promise { - return ctx.parent!.accept(this); - } - - public visitErrorNode(ctx: ErrorNode): Promise { - return ctx.parent!.accept(this); - } - - private async getArgumentCompletionItems( - documentUri: string, - _collectionName: string, - ctx: ParserRuleContext, - ): Promise { - const text = this.textDocument.getText(); - const document = TextDocument.create( - documentUri, - 'json', - 1, - text.substring(ctx.start.startIndex, ctx.stop!.stopIndex + 1), - ); - const positionOffset = this.textDocument.offsetAt(this.at); - const contextOffset = ctx.start.startIndex; - const position = document.positionAt(positionOffset - contextOffset); - const list = await this.jsonLanguageService.doComplete( - document, - position, - this.jsonLanguageService.parseJSONDocument(document), - ); - - if (!list) { - return []; - } - - return list.items.map((item: CompletionItem) => { - if (!item.textEdit) return item; - - const range = 'range' in item.textEdit ? item.textEdit.range : item.textEdit.replace; - const startPositionOffset = document.offsetAt(range.start); - const endPositionOffset = document.offsetAt(range.end); - const newRange = Range.create( - this.textDocument.positionAt(startPositionOffset + contextOffset), - this.textDocument.positionAt(contextOffset + endPositionOffset), - ); - - if ('range' in item.textEdit) { - item.textEdit.range = newRange; - } else { - item.textEdit.replace = newRange; - } - - return item; - }); - } - - private getFunctionName(ctx: ParseTree): string { - let parent = ctx.parent!; - if (!(parent && parent instanceof mongoParser.ArgumentContext)) { - return null!; - } - parent = parent.parent!; - if (!(parent && parent instanceof mongoParser.ArgumentsContext)) { - return null!; - } - parent = parent.parent!; - if (!(parent && parent instanceof mongoParser.FunctionCallContext)) { - return null!; - } - return (parent)._FUNCTION_NAME.text!; - } - - private getCollectionName(ctx: ParseTree): string { - let parent = ctx.parent!; - if (!(parent && parent instanceof mongoParser.ArgumentContext)) { - return null!; - } - parent = parent.parent!; - if (!(parent && parent instanceof mongoParser.ArgumentsContext)) { - return null!; - } - parent = parent.parent!; - if (!(parent && parent instanceof mongoParser.FunctionCallContext)) { - return null!; - } - let previousNode = this.getPreviousNode(parent); - if (previousNode && previousNode instanceof TerminalNode && previousNode.symbol.type === mongoLexer.DOT) { - previousNode = this.getPreviousNode(previousNode); - if (previousNode && previousNode instanceof mongoParser.CollectionContext) { - return previousNode.text; - } - } - return null!; - } - - private getCompletionItemsFromTerminalNode(node: TerminalNode): Promise { - if (node._symbol.type === mongoParser.mongoParser.DB) { - return this.thenable(this.createDbKeywordCompletion(this.createRange(node))); - } - if (node._symbol.type === mongoParser.mongoParser.SEMICOLON) { - return this.thenable(this.createDbKeywordCompletion(this.createRangeAfter(node))); - } - if (node._symbol.type === mongoParser.mongoParser.DOT) { - const previousNode = this.getPreviousNode(node); - if (previousNode && previousNode instanceof TerminalNode) { - if (previousNode._symbol.type === mongoParser.mongoParser.DB) { - return Promise.all([ - this.createCollectionCompletions(this.createRangeAfter(node)), - this.createDbFunctionCompletions(this.createRangeAfter(node)), - ]).then(([collectionCompletions, dbFunctionCompletions]) => [ - ...collectionCompletions, - ...dbFunctionCompletions, - ]); - } - } - if (previousNode instanceof mongoParser.CollectionContext) { - return this.createCollectionFunctionsCompletions(this.createRangeAfter(node)); - } - } - if (node instanceof ErrorNode) { - const previousNode = this.getPreviousNode(node); - if (previousNode) { - if (previousNode instanceof TerminalNode) { - return this.getCompletionItemsFromTerminalNode(previousNode); - } - return previousNode.accept(this); - } - } - return this.thenable(); - } - - private getLastTerminalNode(ctx: ParserRuleContext): TerminalNode { - return ctx.children ? ctx.children - .slice() - .reverse() - .filter( - (node) => - node instanceof TerminalNode && - node.symbol.stopIndex > -1 && - node.symbol.stopIndex < this.offset, - )[0] : null!; - } - - private getPreviousNode(node: ParseTree): ParseTree { - let previousNode: ParseTree = null!; - const parentNode = node.parent!; - for (let i = 0; i < parentNode.childCount; i++) { - const currentNode = parentNode.getChild(i); - if (currentNode === node) { - break; - } - previousNode = currentNode; - } - return previousNode; - } - - private createDbKeywordCompletion(range: Range): CompletionItem { - return { - textEdit: { - newText: 'db', - range, - }, - kind: CompletionItemKind.Keyword, - label: 'db', - }; - } - - private createDbFunctionCompletions(range: Range): Promise { - return this.thenable( - this.createFunctionCompletion('adminCommand', range), - this.createFunctionCompletion('auth', range), - this.createFunctionCompletion('cloneDatabase', range), - this.createFunctionCompletion('commandHelp', range), - this.createFunctionCompletion('copyDatabase', range), - this.createFunctionCompletion('createCollection', range), - this.createFunctionCompletion('createView', range), - this.createFunctionCompletion('createUser', range), - this.createFunctionCompletion('currentOp', range), - this.createFunctionCompletion('dropDatabase', range), - this.createFunctionCompletion('eval', range), - this.createFunctionCompletion('fsyncLock', range), - this.createFunctionCompletion('fsyncUnLock', range), - this.createFunctionCompletion('getCollection', range), - this.createFunctionCompletion('getCollectionInfos', range), - this.createFunctionCompletion('getCollectionNames', range), - this.createFunctionCompletion('getLastError', range), - this.createFunctionCompletion('getLastErrorObj', range), - this.createFunctionCompletion('getLogComponents', range), - this.createFunctionCompletion('getMongo', range), - this.createFunctionCompletion('getName', range), - this.createFunctionCompletion('getPrevError', range), - this.createFunctionCompletion('getProfilingLevel', range), - this.createFunctionCompletion('getProfilingStatus', range), - this.createFunctionCompletion('getReplicationInfo', range), - this.createFunctionCompletion('getSiblingDB', range), - this.createFunctionCompletion('getWriteConcern', range), - this.createFunctionCompletion('hostInfo', range), - this.createFunctionCompletion('isMaster', range), - this.createFunctionCompletion('killOp', range), - this.createFunctionCompletion('listCommands', range), - this.createFunctionCompletion('loadServerScripts', range), - this.createFunctionCompletion('logout', range), - this.createFunctionCompletion('printCollectionStats', range), - this.createFunctionCompletion('printReplicationInfo', range), - this.createFunctionCompletion('printShardingStatus', range), - this.createFunctionCompletion('printSlaveReplicationInfo', range), - this.createFunctionCompletion('dropUser', range), - this.createFunctionCompletion('repairDatabase', range), - this.createFunctionCompletion('runCommand', range), - this.createFunctionCompletion('serverStatus', range), - this.createFunctionCompletion('setLogLevel', range), - this.createFunctionCompletion('setProfilingLevel', range), - this.createFunctionCompletion('setWriteConcern', range), - this.createFunctionCompletion('unsetWriteConcern', range), - this.createFunctionCompletion('setVerboseShell', range), - this.createFunctionCompletion('shotdownServer', range), - this.createFunctionCompletion('stats', range), - this.createFunctionCompletion('version', range), - ); - } - - private createCollectionCompletions(range: Range): Promise { - if (this.db) { - return >this.db.collections().then((collections) => { - return collections.map( - (collection) => - { - textEdit: { - newText: collection.collectionName, - range, - }, - label: collection.collectionName, - kind: CompletionItemKind.Property, - filterText: collection.collectionName, - sortText: `1:${collection.collectionName}`, - }, - ); - }); - } - return Promise.resolve([]); - } - - private createCollectionFunctionsCompletions(range: Range): Promise { - return this.thenable( - this.createFunctionCompletion('bulkWrite', range), - this.createFunctionCompletion('count', range), - this.createFunctionCompletion('copyTo', range), - this.createFunctionCompletion('convertToCapped', range), - this.createFunctionCompletion('createIndex', range), - this.createFunctionCompletion('createIndexes', range), - this.createFunctionCompletion('dataSize', range), - this.createFunctionCompletion('deleteOne', range), - this.createFunctionCompletion('deleteMany', range), - this.createFunctionCompletion('distinct', range), - this.createFunctionCompletion('drop', range), - this.createFunctionCompletion('dropIndex', range), - this.createFunctionCompletion('dropIndexes', range), - this.createFunctionCompletion('ensureIndex', range), - this.createFunctionCompletion('explain', range), - this.createFunctionCompletion('reIndex', range), - this.createFunctionCompletion('find', range), - this.createFunctionCompletion('findOne', range), - this.createFunctionCompletion('findOneAndDelete', range), - this.createFunctionCompletion('findOneAndReplace', range), - this.createFunctionCompletion('findOneAndUpdate', range), - this.createFunctionCompletion('getDB', range), - this.createFunctionCompletion('getPlanCache', range), - this.createFunctionCompletion('getIndexes', range), - this.createFunctionCompletion('group', range), - this.createFunctionCompletion('insert', range), - this.createFunctionCompletion('insertOne', range), - this.createFunctionCompletion('insertMany', range), - this.createFunctionCompletion('mapReduce', range), - this.createFunctionCompletion('aggregate', range), - this.createFunctionCompletion('remove', range), - this.createFunctionCompletion('replaceOne', range), - this.createFunctionCompletion('renameCollection', range), - this.createFunctionCompletion('runCommand', range), - this.createFunctionCompletion('save', range), - this.createFunctionCompletion('stats', range), - this.createFunctionCompletion('storageSize', range), - this.createFunctionCompletion('totalIndexSize', range), - this.createFunctionCompletion('update', range), - this.createFunctionCompletion('updateOne', range), - this.createFunctionCompletion('updateMany', range), - this.createFunctionCompletion('validate', range), - this.createFunctionCompletion('getShardVersion', range), - this.createFunctionCompletion('getShardDistribution', range), - this.createFunctionCompletion('getSplitKeysForChunks', range), - this.createFunctionCompletion('getWriteConcern', range), - this.createFunctionCompletion('setWriteConcern', range), - this.createFunctionCompletion('unsetWriteConcern', range), - this.createFunctionCompletion('latencyStats', range), - ); - } - - private createFunctionCompletion(label: string, range: Range): CompletionItem { - return { - textEdit: { - newText: label, - range, - }, - kind: CompletionItemKind.Function, - label, - sortText: `2:${label}`, - }; - } - - private createRange(parserRuleContext: ParseTree): Range { - if (parserRuleContext instanceof ParserRuleContext) { - const startToken = parserRuleContext.start; - let stopToken = parserRuleContext.stop; - if (!stopToken || startToken.type === mongoParser.mongoParser.EOF) { - stopToken = startToken; - } - - const stop = stopToken.stopIndex; - return this._createRange(startToken.startIndex, stop); - } - - if (parserRuleContext instanceof TerminalNode) { - return this._createRange(parserRuleContext.symbol.startIndex, parserRuleContext.symbol.stopIndex); - } - - return null!; - } - - private createRangeAfter(parserRuleContext: ParseTree): Range { - if (parserRuleContext instanceof ParserRuleContext) { - let stopToken = parserRuleContext.stop; - if (!stopToken) { - stopToken = parserRuleContext.start; - } - - const stop = stopToken.stopIndex; - return this._createRange(stop + 1, stop + 1); - } - - if (parserRuleContext instanceof TerminalNode) { - return this._createRange(parserRuleContext.symbol.stopIndex + 1, parserRuleContext.symbol.stopIndex + 1); - } - - //currently returning an null for the sake of linting. Would prefer to throw an error, but don't want - // to introduce a regression bug. - return null!; - } - - private _createRange(start: number, end: number): Range { - const endPosition = this.textDocument.positionAt(end); - if (endPosition.line < this.at.line) { - return Range.create(Position.create(this.at.line, 0), this.at); - } - const startPosition = this.textDocument.positionAt(start); - return Range.create(startPosition, endPosition); - } - - private thenable(...completionItems: CompletionItem[]): Promise { - return Promise.resolve(completionItems || []); - } -} diff --git a/src/documentdb/scrapbook/services/languageService.ts b/src/documentdb/scrapbook/services/languageService.ts deleted file mode 100644 index 24efe7083..000000000 --- a/src/documentdb/scrapbook/services/languageService.ts +++ /dev/null @@ -1,121 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* eslint-disable import/no-internal-modules */ - -// NOTE: This file may not take a dependency on vscode or anything that takes a dependency on it (such as @microsoft/vscode-azext-utils) - -import { type Db } from 'mongodb'; -import { - getLanguageService, - type LanguageService as JsonLanguageService, - type SchemaConfiguration, -} from 'vscode-json-languageservice'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { - createConnection, - ProposedFeatures, - TextDocuments, - TextDocumentSyncKind, - type CompletionItem, - type InitializeParams, - type InitializeResult, - type TextDocumentPositionParams, -} from 'vscode-languageserver/node'; -import { connectToClient } from '../connectToClient'; -import { type IConnectionParams } from './IConnectionParams'; -import { MongoScriptDocumentManager } from './mongoScript'; -import { SchemaService } from './schemaService'; - -export class LanguageService { - private textDocuments: TextDocuments = new TextDocuments(TextDocument); - private readonly mongoDocumentsManager: MongoScriptDocumentManager; - private db: Db | null = null; - - private readonly jsonLanguageService: JsonLanguageService; - private readonly schemaService: SchemaService; - private schemas: SchemaConfiguration[]; - - constructor() { - // Create a connection for the server - const connection = createConnection(ProposedFeatures.all); - - // eslint-disable-next-line - console.log = connection.console.log.bind(connection.console); - - // eslint-disable-next-line - console.error = connection.console.error.bind(connection.console); - this.schemaService = new SchemaService(); - - // Make the text document manager listen on the connection - // for open, change and close text document events - this.textDocuments.listen(connection); - - // Listen on the connection - connection.listen(); - // After the server has started the client sends an initialize request. The server receives - // in the passed params the rootPath of the workspace plus the client capabilities. - connection.onInitialize((_params: InitializeParams): InitializeResult => { - return { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Full, // Tell the client that the server works in FULL text document sync mode - completionProvider: { triggerCharacters: ['.'] }, - }, - }; - }); - - connection.onCompletion((textDocumentPosition) => { - return this.provideCompletionItems(textDocumentPosition); - }); - - connection.onRequest('connect', (connectionParams: IConnectionParams) => { - void connectToClient( - connectionParams.connectionString, - connectionParams.extensionUserAgent, - connectionParams.emulatorConfiguration, - ).then((account) => { - this.db = account.db(connectionParams.databaseName); - void this.schemaService.registerSchemas(this.db).then((schemas) => { - this.configureSchemas(schemas); - }); - }); - }); - - connection.onRequest('disconnect', () => { - this.db = null; - for (const schema of this.schemas) { - this.jsonLanguageService.resetSchema(schema.uri); - } - }); - - this.jsonLanguageService = getLanguageService({ - schemaRequestService: (uri) => this.schemaService.resolveSchema(uri), - contributions: [], - }); - - this.mongoDocumentsManager = new MongoScriptDocumentManager(this.schemaService, this.jsonLanguageService); - } - - public provideCompletionItems(positionParams: TextDocumentPositionParams): Promise { - const textDocument = this.textDocuments.get(positionParams.textDocument.uri); - - if (!textDocument || !this.db) { - return Promise.resolve([]); - } - - const mongoScriptDocument = this.mongoDocumentsManager.getDocument(textDocument, this.db); - return mongoScriptDocument.provideCompletionItemsAt(positionParams.position); - } - - public resetSchema(uri: string): void { - this.jsonLanguageService.resetSchema(uri); - } - - public configureSchemas(schemas: SchemaConfiguration[]): void { - this.jsonLanguageService.configure({ - schemas, - }); - } -} diff --git a/src/documentdb/scrapbook/services/mongoScript.ts b/src/documentdb/scrapbook/services/mongoScript.ts deleted file mode 100644 index c1102d99c..000000000 --- a/src/documentdb/scrapbook/services/mongoScript.ts +++ /dev/null @@ -1,120 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ANTLRInputStream as InputStream } from 'antlr4ts/ANTLRInputStream'; -import { CommonTokenStream } from 'antlr4ts/CommonTokenStream'; -import { Interval } from 'antlr4ts/misc/Interval'; -import { ParserRuleContext } from 'antlr4ts/ParserRuleContext'; -import { type ParseTree } from 'antlr4ts/tree/ParseTree'; -import { TerminalNode } from 'antlr4ts/tree/TerminalNode'; -import { type Db } from 'mongodb'; -import { type LanguageService as JsonLanguageService } from 'vscode-json-languageservice'; -import { type CompletionItem, type Position } from 'vscode-languageserver'; -import { type TextDocument } from 'vscode-languageserver-textdocument'; -import { mongoLexer } from '../../grammar/mongoLexer'; -import * as mongoParser from '../../grammar/mongoParser'; -import { MongoVisitor } from '../../grammar/visitors'; -import { CompletionItemsVisitor } from './completionItemProvider'; -import { type SchemaService } from './schemaService'; - -export class MongoScriptDocumentManager { - constructor( - private schemaService: SchemaService, - private jsonLanguageService: JsonLanguageService, - ) {} - - public getDocument(textDocument: TextDocument, db: Db): MongoScriptDocument { - return new MongoScriptDocument(textDocument, db, this.schemaService, this.jsonLanguageService); - } -} - -export class MongoScriptDocument { - private readonly _lexer: mongoLexer; - - constructor( - private textDocument: TextDocument, - private db: Db, - private schemaService: SchemaService, - private jsonLanguageService: JsonLanguageService, - ) { - this._lexer = new mongoLexer(new InputStream(textDocument.getText())); - this._lexer.removeErrorListeners(); - } - - public provideCompletionItemsAt(position: Position): Promise { - const parser = new mongoParser.mongoParser(new CommonTokenStream(this._lexer)); - parser.removeErrorListeners(); - - const offset = this.textDocument.offsetAt(position); - const lastNode = new NodeFinder(offset).visit(parser.commands()); - if (lastNode) { - return new CompletionItemsVisitor( - this.textDocument, - this.db, - offset, - this.schemaService, - this.jsonLanguageService, - ).visit(lastNode); - } - return Promise.resolve([]); - } -} - -class NodeFinder extends MongoVisitor { - constructor(private offset: number) { - super(); - } - - protected defaultResult(ctx: ParseTree): ParseTree | null { - if (ctx instanceof ParserRuleContext) { - const stop = ctx.stop ? ctx.stop.stopIndex : ctx.start.stopIndex; - if (stop < this.offset) { - return ctx; - } - - return null; - } - if (ctx instanceof TerminalNode) { - if (ctx.symbol.stopIndex < this.offset) { - return ctx; - } - - return null; - } - - return null; - } - - protected aggregateResult(aggregate: ParseTree, nextResult: ParseTree): ParseTree { - if (aggregate && nextResult) { - const aggregateStart = - aggregate instanceof ParserRuleContext - ? aggregate.start.startIndex - : (aggregate).symbol.startIndex; - const aggregateStop = - aggregate instanceof ParserRuleContext - ? aggregate.start.stopIndex - : (aggregate).symbol.stopIndex; - const nextResultStart = - nextResult instanceof ParserRuleContext - ? nextResult.start.startIndex - : (nextResult).symbol.startIndex; - const nextResultStop = - nextResult instanceof ParserRuleContext - ? nextResult.start.stopIndex - : (nextResult).symbol.stopIndex; - - if ( - Interval.of(aggregateStart, aggregateStop).properlyContains( - Interval.of(nextResultStart, nextResultStop), - ) - ) { - return aggregate; - } - return nextResult; - } - return nextResult ? nextResult : aggregate; - } -} diff --git a/src/documentdb/scrapbook/services/schemaService.ts b/src/documentdb/scrapbook/services/schemaService.ts deleted file mode 100644 index bd300365d..000000000 --- a/src/documentdb/scrapbook/services/schemaService.ts +++ /dev/null @@ -1,666 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */ - -import { type Db, type FindCursor } from 'mongodb'; -import { type JSONSchema, type SchemaConfiguration } from 'vscode-json-languageservice'; - -export class SchemaService { - private _db: Db; - private _schemasCache: Map = new Map(); - - public registerSchemas(db: Db): Thenable { - this._db = db; - this._schemasCache.clear(); - return this._db.collections().then((collections) => { - const schemas: SchemaConfiguration[] = []; - for (const collection of collections) { - schemas.push( - ...[ - { - uri: this.queryCollectionSchema(collection.collectionName), - fileMatch: [this.queryDocumentUri(collection.collectionName)], - }, - { - uri: this.aggregateCollectionSchema(collection.collectionName), - fileMatch: [this.aggregateDocumentUri(collection.collectionName)], - }, - ], - ); - } - return schemas; - }); - } - - public queryCollectionSchema(collectionName: string): string { - return 'mongo://query/' + collectionName + '.schema'; - } - - public aggregateCollectionSchema(collectionName: string): string { - return 'mongo://aggregate/' + collectionName + '.schema'; - } - - public queryDocumentUri(collectionName: string): string { - return 'mongo://query/' + collectionName + '.json'; - } - - public aggregateDocumentUri(collectionName: string): string { - return 'mongo://aggregate/' + collectionName + '.json'; - } - - public resolveSchema(uri: string): Thenable { - const schema = this._schemasCache.get(uri); - if (schema) { - return Promise.resolve(schema); - } - if (uri.startsWith('mongo://query/')) { - return this._resolveQueryCollectionSchema( - uri.substring('mongo://query/'.length, uri.length - '.schema'.length), - uri, - ).then((sch) => { - this._schemasCache.set(uri, sch); - return sch; - }); - } - if (uri.startsWith('mongo://aggregate/')) { - return this._resolveAggregateCollectionSchema( - uri.substring('mongo://aggregate/'.length, uri.length - '.schema'.length), - ).then((sch) => { - this._schemasCache.set(uri, sch); - return sch; - }); - } - return Promise.resolve(''); - } - - private _resolveQueryCollectionSchema(collectionName: string, schemaUri: string): Thenable { - const collection = this._db.collection(collectionName); - const cursor = collection.find(); - return new Promise((resolve, _reject) => { - this.readNext([], cursor, 10, (result) => { - const schema: JSONSchema = { - type: 'object', - properties: {}, - }; - for (const document of result) { - this.setSchemaForDocument(null!, document, schema); - } - this.setGlobalOperatorProperties(schema); - this.setLogicalOperatorProperties(schema, schemaUri); - resolve(JSON.stringify(schema)); - }); - }); - } - - private _resolveAggregateCollectionSchema(collectionName: string): Thenable { - const collection = this._db.collection(collectionName); - const cursor = collection.find(); - return new Promise((resolve, _reject) => { - this.readNext([], cursor, 10, (_result) => { - const schema: JSONSchema = { - type: 'array', - items: this.getAggregateStagePropertiesSchema(this.queryCollectionSchema(collectionName)), - }; - resolve(JSON.stringify(schema)); - }); - }); - } - - private getMongoDocumentType(document: any): string { - return Array.isArray(document) ? 'array' : document === null ? 'null' : typeof document; - } - - private setSchemaForDocument(parent: string, document: any, schema: JSONSchema): void { - if (this.getMongoDocumentType(document) === 'object') { - //eslint-disable-next-line @typescript-eslint/no-unsafe-argument - for (const property of Object.keys(document)) { - if (!parent && ['_id'].indexOf(property) !== -1) { - continue; - } - this.setSchemaForDocumentProperty(parent, property, document, schema); - } - } - } - - private setSchemaForDocumentProperty(parent: string, property: string, document: any, schema: JSONSchema): void { - const scopedProperty = parent ? `${parent}.${property}` : property; - // eslint-disable-next-line , @typescript-eslint/no-unsafe-assignment - const value = document[property]; - const type = this.getMongoDocumentType(value); - - const propertySchema: JSONSchema = { - type: [type, 'object'], - }; - this.setOperatorProperties(type, propertySchema); - schema.properties![scopedProperty] = propertySchema; - - if (type === 'object') { - this.setSchemaForDocument(scopedProperty, value, schema); - } - - if (type === 'array') { - for (const v of value) { - this.setSchemaForDocument(scopedProperty, v, schema); - } - } - } - - private setGlobalOperatorProperties(schema: JSONSchema): void { - schema.properties!.$text = { - type: 'object', - description: 'Performs text search', - properties: { - $search: { - type: 'string', - description: - 'A string of terms that MongoDB parses and uses to query the text index. MongoDB performs a logical OR search of the terms unless specified as a phrase', - }, - $language: { - type: 'string', - description: - 'Optional. The language that determines the list of stop words for the search and the rules for the stemmer and tokenizer. If not specified, the search uses the default language of the index.\nIf you specify a language value of "none", then the text search uses simple tokenization with no list of stop words and no stemming', - }, - $caseSensitive: { - type: 'boolean', - description: - 'Optional. A boolean flag to enable or disable case sensitive search. Defaults to false; i.e. the search defers to the case insensitivity of the text index', - }, - $diacriticSensitive: { - type: 'boolean', - description: `Optional. A boolean flag to enable or disable diacritic sensitive search against version 3 text indexes.Defaults to false; i.e.the search defers to the diacritic insensitivity of the text index -Text searches against earlier versions of the text index are inherently diacritic sensitive and cannot be diacritic insensitive. As such, the $diacriticSensitive option has no effect with earlier versions of the text index`, - }, - }, - required: ['$search'], - }; - - schema.properties!.$where = { - type: 'string', - description: `Matches documents that satisfy a JavaScript expression. -Use the $where operator to pass either a string containing a JavaScript expression or a full JavaScript function to the query system`, - }; - schema.properties!.$comment = { - type: 'string', - description: 'Adds a comment to a query predicate', - }; - } - - private setLogicalOperatorProperties(schema: JSONSchema, schemaUri: string): void { - schema.properties!.$or = { - type: 'array', - description: - 'Joins query clauses with a logical OR returns all documents that match the conditions of either clause', - items: { - $ref: schemaUri, - }, - }; - schema.properties!.$and = { - type: 'array', - description: - 'Joins query clauses with a logical AND returns all documents that match the conditions of both clauses', - items: { - $ref: schemaUri, - }, - }; - schema.properties!.$nor = { - type: 'array', - description: 'Joins query clauses with a logical NOR returns all documents that fail to match both clauses', - items: { - $ref: schemaUri, - }, - }; - } - - private setOperatorProperties(type: string, schema: JSONSchema): void { - if (!schema.properties) { - schema.properties = {}; - } - - const expressionSchema = { - // eslint-disable-next-line - properties: {}, - }; - // Comparison operators - expressionSchema.properties.$eq = { - type: type, - description: 'Matches values that are equal to a specified value', - }; - expressionSchema.properties.$gt = { - type: type, - description: 'Matches values that are greater than a specified value', - }; - expressionSchema.properties.$gte = { - type: type, - description: 'Matches values that are greater than or equal to a specified value', - }; - expressionSchema.properties.$lt = { - type: type, - description: 'Matches values that are less than a specified value', - }; - expressionSchema.properties.$lte = { - type: type, - description: 'Matches values that are less than or equal to a specified value', - }; - expressionSchema.properties.$ne = { - type: type, - description: 'Matches all values that are not equal to a specified value', - }; - expressionSchema.properties.$in = { - type: 'array', - description: 'Matches any of the values specified in an array', - }; - expressionSchema.properties.$nin = { - type: 'array', - description: 'Matches none of the values specified in an array', - }; - - // Element operators - expressionSchema.properties.$exists = { - type: 'boolean', - description: 'Matches documents that have the specified field', - }; - expressionSchema.properties.$type = { - type: 'string', - description: 'Selects documents if a field is of the specified type', - }; - - // Evaluation operators - expressionSchema.properties.$mod = { - type: 'array', - description: - 'Performs a modulo operation on the value of a field and selects documents with a specified result', - maxItems: 2, - default: [2, 0], - }; - expressionSchema.properties.$regex = { - type: 'string', - description: 'Selects documents where values match a specified regular expression', - }; - - // Geospatial - const geometryPropertySchema: JSONSchema = { - type: 'object', - properties: { - type: { - type: 'string', - default: 'GeoJSON object type', - }, - coordinates: { - type: 'array', - }, - crs: { - type: 'object', - properties: { - type: { - type: 'string', - }, - properties: { - type: 'object', - }, - }, - }, - }, - }; - expressionSchema.properties.$geoWithin = { - type: 'object', - description: - 'Selects geometries within a bounding GeoJSON geometry. The 2dsphere and 2d indexes support $geoWithin', - properties: { - $geometry: geometryPropertySchema, - $box: { - type: 'array', - }, - $polygon: { - type: 'array', - }, - $center: { - type: 'array', - }, - $centerSphere: { - type: 'array', - }, - }, - }; - expressionSchema.properties.$geoIntersects = { - type: 'object', - description: - 'Selects geometries that intersect with a GeoJSON geometry. The 2dsphere index supports $geoIntersects', - properties: { - $geometry: geometryPropertySchema, - }, - }; - expressionSchema.properties.$near = { - type: 'object', - description: - 'Returns geospatial objects in proximity to a point. Requires a geospatial index. The 2dsphere and 2d indexes support $near', - properties: { - $geometry: geometryPropertySchema, - $maxDistance: { - type: 'number', - }, - $minDistance: { - type: 'number', - }, - }, - }; - expressionSchema.properties.$nearSphere = { - type: 'object', - description: - 'Returns geospatial objects in proximity to a point. Requires a geospatial index. The 2dsphere and 2d indexes support $near', - properties: { - $geometry: geometryPropertySchema, - $maxDistance: { - type: 'number', - }, - $minDistance: { - type: 'number', - }, - }, - }; - - // Array operatos - if (type === 'array') { - expressionSchema.properties.$all = { - type: 'array', - description: 'Matches arrays that contain all elements specified in the query', - }; - expressionSchema.properties.$size = { - type: 'number', - description: 'Selects documents if the array field is a specified size', - }; - } - - // Bit operators - expressionSchema.properties.$bitsAllSet = { - type: 'array', - description: 'Matches numeric or binary values in which a set of bit positions all have a value of 1', - }; - expressionSchema.properties.$bitsAnySet = { - type: 'array', - description: - 'Matches numeric or binary values in which any bit from a set of bit positions has a value of 1', - }; - expressionSchema.properties.$bitsAllClear = { - type: 'array', - description: 'Matches numeric or binary values in which a set of bit positions all have a value of 0', - }; - expressionSchema.properties.$bitsAnyClear = { - type: 'array', - description: - 'Matches numeric or binary values in which any bit from a set of bit positions has a value of 0', - }; - - // eslint-disable-next-line - schema.properties = { ...expressionSchema.properties }; - schema.properties!.$not = { - type: 'object', - description: - 'Inverts the effect of a query expression and returns documents that do not match the query expression', - // eslint-disable-next-line - properties: { ...expressionSchema.properties }, - }; - schema.properties!.$elemMatch = { - type: 'object', - }; - } - - private getAggregateStagePropertiesSchema(querySchemaUri: string): JSONSchema { - const schemas: JSONSchema[] = []; - schemas.push({ - type: 'object', - properties: { - $collStats: { - type: 'object', - description: 'Returns statistics regarding a collection or view', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $project: { - type: 'object', - description: - 'Reshapes each document in the stream, such as by adding new fields or removing existing fields. For each input document, outputs one document', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $match: { - type: 'object', - description: - 'Filters the document stream to allow only matching documents to pass unmodified into the next pipeline stage. $match uses standard MongoDB queries. For each input document, outputs either one document (a match) or zero documents (no match)', - $ref: querySchemaUri, - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $redact: { - type: 'object', - description: - 'Reshapes each document in the stream by restricting the content for each document based on information stored in the documents themselves. Incorporates the functionality of $project and $match. Can be used to implement field level redaction. For each input document, outputs either one or zero documents', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $limit: { - type: 'object', - description: - 'Passes the first n documents unmodified to the pipeline where n is the specified limit. For each input document, outputs either one document (for the first n documents) or zero documents (after the first n documents).', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $skip: { - type: 'object', - description: - 'Skips the first n documents where n is the specified skip number and passes the remaining documents unmodified to the pipeline. For each input document, outputs either zero documents (for the first n documents) or one document (if after the first n documents)', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $unwind: { - type: 'object', - description: - 'Deconstructs an array field from the input documents to output a document for each element. Each output document replaces the array with an element value. For each input document, outputs n documents where n is the number of array elements and can be zero for an empty array', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $group: { - type: 'object', - description: - 'Groups input documents by a specified identifier expression and applies the accumulator expression(s), if specified, to each group. Consumes all input documents and outputs one document per each distinct group. The output documents only contain the identifier field and, if specified, accumulated fields.', - properties: { - _id: { - type: ['string', 'object'], - }, - }, - additionalProperties: { - type: 'object', - }, - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $sample: { - type: 'object', - description: 'Randomly selects the specified number of documents from its input', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $sort: { - type: 'object', - description: - 'Reorders the document stream by a specified sort key. Only the order changes; the documents remain unmodified. For each input document, outputs one document.', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $geoNear: { - type: 'object', - description: - 'Returns an ordered stream of documents based on the proximity to a geospatial point. Incorporates the functionality of $match, $sort, and $limit for geospatial data. The output documents include an additional distance field and can include a location identifier field.', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $lookup: { - type: 'object', - description: - 'Performs a left outer join to another collection in the same database to filter in documents from the โ€œjoinedโ€ collection for processing', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $out: { - type: 'object', - description: - 'Writes the resulting documents of the aggregation pipeline to a collection. To use the $out stage, it must be the last stage in the pipeline', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $indexStats: { - type: 'object', - description: 'Returns statistics regarding the use of each index for the collection', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $facet: { - type: 'object', - description: - 'Processes multiple aggregation pipelines within a single stage on the same set of input documents. Enables the creation of multi-faceted aggregations capable of characterizing data across multiple dimensions, or facets, in a single stage', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $bucket: { - type: 'object', - description: - 'Categorizes incoming documents into groups, called buckets, based on a specified expression and bucket boundaries', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $bucketAuto: { - type: 'object', - description: - 'Categorizes incoming documents into a specific number of groups, called buckets, based on a specified expression. Bucket boundaries are automatically determined in an attempt to evenly distribute the documents into the specified number of buckets', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $sortByCount: { - type: 'object', - description: - 'Groups incoming documents based on the value of a specified expression, then computes the count of documents in each distinct group', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $addFields: { - type: 'object', - description: - 'Adds new fields to documents. Outputs documents that contain all existing fields from the input documents and newly added fields', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $replaceRoot: { - type: 'object', - description: - 'Replaces a document with the specified embedded document. The operation replaces all existing fields in the input document, including the _id field. Specify a document embedded in the input document to promote the embedded document to the top level', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $count: { - type: 'object', - description: 'Returns a count of the number of documents at this stage of the aggregation pipeline', - }, - }, - }); - schemas.push({ - type: 'object', - properties: { - $graphLookup: { - type: 'object', - description: - 'Performs a recursive search on a collection. To each output document, adds a new array field that contains the traversal results of the recursive search for that document', - }, - }, - }); - return { - type: 'object', - oneOf: schemas, - }; - } - - private readNext( - result: any[], - cursor: FindCursor, - batchSize: number, - callback: (result: any[]) => void, - ): void { - if (result.length === batchSize) { - callback(result); - return; - } - - void cursor.hasNext().then((hasNext) => { - if (!hasNext) { - callback(result); - return; - } - - void cursor.next().then((doc) => { - result.push(doc); - this.readNext(result, cursor, batchSize, callback); - }); - }); - } -} diff --git a/src/documentdb/scratchpad/ScratchpadBlockHighlighter.ts b/src/documentdb/scratchpad/ScratchpadBlockHighlighter.ts new file mode 100644 index 000000000..b1a35866a --- /dev/null +++ b/src/documentdb/scratchpad/ScratchpadBlockHighlighter.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { SCRATCHPAD_LANGUAGE_ID } from './constants'; +import { detectBlocks, findBlockAtLine } from './statementDetector'; + +/** + * Shows a vertical bar in the gutter for all code blocks in scratchpad files. + * The active block (containing the cursor) is brighter; inactive blocks are dimmed. + * Decoration types are recreated when the color theme changes to use the + * appropriate dark/light SVG variants. + */ +export class ScratchpadBlockHighlighter implements vscode.Disposable { + private _activeDecoration!: vscode.TextEditorDecorationType; + private _inactiveDecoration!: vscode.TextEditorDecorationType; + private readonly _extensionPath: string; + + private readonly _disposables: vscode.Disposable[] = []; + + constructor(extensionPath: string) { + this._extensionPath = extensionPath; + this.createDecorations(); + + this._disposables.push( + vscode.window.onDidChangeActiveColorTheme(() => { + this._activeDecoration.dispose(); + this._inactiveDecoration.dispose(); + this.createDecorations(); + // Re-apply to current editor + if (vscode.window.activeTextEditor) { + this.update(vscode.window.activeTextEditor); + } + }), + ); + + this._disposables.push( + vscode.window.onDidChangeTextEditorSelection((e) => { + this.update(e.textEditor); + }), + ); + + this._disposables.push( + vscode.window.onDidChangeActiveTextEditor((editor) => { + if (editor) { + this.update(editor); + } + }), + ); + + this._disposables.push( + vscode.workspace.onDidChangeTextDocument((e) => { + const editor = vscode.window.activeTextEditor; + if (editor && editor.document === e.document) { + this.update(editor); + } + }), + ); + + if (vscode.window.activeTextEditor) { + this.update(vscode.window.activeTextEditor); + } + } + + private createDecorations(): void { + const isLight = + vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Light || + vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.HighContrastLight; + const suffix = isLight ? '-light' : ''; + const iconsDir = path.join(this._extensionPath, 'resources', 'icons'); + + this._activeDecoration = vscode.window.createTextEditorDecorationType({ + gutterIconPath: vscode.Uri.file(path.join(iconsDir, `scratchpad-block-active${suffix}.svg`)), + gutterIconSize: 'contain', + }); + this._inactiveDecoration = vscode.window.createTextEditorDecorationType({ + gutterIconPath: vscode.Uri.file(path.join(iconsDir, `scratchpad-block-inactive${suffix}.svg`)), + gutterIconSize: 'contain', + }); + } + + private update(editor: vscode.TextEditor): void { + if (editor.document.languageId !== SCRATCHPAD_LANGUAGE_ID) { + return; + } + + const cursorLine = editor.selection.active.line; + const blocks = detectBlocks(editor.document); + const activeBlock = findBlockAtLine(blocks, cursorLine); + + const activeRanges: vscode.Range[] = []; + const inactiveRanges: vscode.Range[] = []; + + for (const block of blocks) { + const isActive = activeBlock !== undefined && block.startLine === activeBlock.startLine; + const target = isActive ? activeRanges : inactiveRanges; + for (let line = block.startLine; line <= block.endLine; line++) { + target.push(new vscode.Range(line, 0, line, 0)); + } + } + + editor.setDecorations(this._activeDecoration, activeRanges); + editor.setDecorations(this._inactiveDecoration, inactiveRanges); + } + + dispose(): void { + this._activeDecoration.dispose(); + this._inactiveDecoration.dispose(); + for (const d of this._disposables) { + d?.dispose(); + } + } +} diff --git a/src/documentdb/scratchpad/ScratchpadCodeLensProvider.test.ts b/src/documentdb/scratchpad/ScratchpadCodeLensProvider.test.ts new file mode 100644 index 000000000..fff2935f3 --- /dev/null +++ b/src/documentdb/scratchpad/ScratchpadCodeLensProvider.test.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { ScratchpadCodeLensProvider } from './ScratchpadCodeLensProvider'; +import { ScratchpadService } from './ScratchpadService'; +import { ScratchpadCommandIds } from './constants'; + +/** + * Helper to create a mock TextDocument from a multiline string. + */ +function mockDocument(text: string): vscode.TextDocument { + const lines = text.split('\n'); + return { + lineCount: lines.length, + lineAt(lineNumber: number) { + return { text: lines[lineNumber] ?? '' }; + }, + getText() { + return text; + }, + } as unknown as vscode.TextDocument; +} + +describe('ScratchpadCodeLensProvider', () => { + let provider: ScratchpadCodeLensProvider; + let service: ScratchpadService; + + beforeEach(() => { + service = ScratchpadService.getInstance(); + provider = new ScratchpadCodeLensProvider(); + }); + + afterEach(() => { + provider.dispose(); + service.dispose(); + }); + + it('provides connection status lens at line 0 when disconnected', () => { + const doc = mockDocument('db.test.find({})'); + const lenses = provider.provideCodeLenses(doc); + + // First lens should be connection status + const connectionLens = lenses[0]; + expect(connectionLens.command?.command).toBe(ScratchpadCommandIds.connect); + expect(connectionLens.command?.title).toContain('Connect to a database'); + expect(connectionLens.range.start.line).toBe(0); + }); + + it('provides connection status lens showing cluster name when connected', () => { + service.setConnection({ + clusterId: 'test-id', + clusterDisplayName: 'MyCluster', + databaseName: 'orders', + }); + + const doc = mockDocument('db.test.find({})'); + const lenses = provider.provideCodeLenses(doc); + + const connectionLens = lenses[0]; + expect(connectionLens.command?.title).toContain('MyCluster / orders'); + }); + + it('provides Run All lens at line 0', () => { + const doc = mockDocument('db.test.find({})'); + const lenses = provider.provideCodeLenses(doc); + + const runAllLens = lenses[1]; + expect(runAllLens.command?.command).toBe(ScratchpadCommandIds.runAll); + expect(runAllLens.command?.title).toContain('Run All'); + expect(runAllLens.range.start.line).toBe(0); + }); + + it('provides only top-level lenses when no active editor (per-block lens follows cursor)', () => { + const doc = mockDocument('db.users.find({});\n\ndb.orders.find({});'); + const lenses = provider.provideCodeLenses(doc); + + // Only 2 top lenses (connection + Run All) โ€” per-block lens requires active editor + expect(lenses.length).toBe(2); + }); + + it('shows running state when executing', () => { + service.setExecuting(true); + const doc = mockDocument('db.test.find({})'); + const lenses = provider.provideCodeLenses(doc); + + const runAllLens = lenses[1]; + expect(runAllLens.command?.title).toContain('Running'); + }); +}); diff --git a/src/documentdb/scratchpad/ScratchpadCodeLensProvider.ts b/src/documentdb/scratchpad/ScratchpadCodeLensProvider.ts new file mode 100644 index 000000000..a30bc1c97 --- /dev/null +++ b/src/documentdb/scratchpad/ScratchpadCodeLensProvider.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { SCRATCHPAD_LANGUAGE_ID, ScratchpadCommandIds } from './constants'; +import { ScratchpadService } from './ScratchpadService'; +import { detectBlocks, findBlockAtLine } from './statementDetector'; + +/** + * Provides CodeLens actions for DocumentDB Scratchpad files: + * 1. Connection status lens (line 0) โ€” shows connected cluster/database or "Connect" + * 2. Run All lens (line 0) โ€” runs the entire file + * 3. Per-block Run lens โ€” shown only for the block containing the cursor + * + * The per-block lens follows the cursor: when the cursor moves to a different + * block, we fire `onDidChangeCodeLenses` so VS Code re-requests lenses. + */ +export class ScratchpadCodeLensProvider implements vscode.CodeLensProvider, vscode.Disposable { + private readonly _onDidChangeCodeLenses = new vscode.EventEmitter(); + readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; + + private readonly _disposables: vscode.Disposable[] = []; + + /** Track which block the cursor is in to avoid unnecessary refreshes. */ + private _lastActiveBlockStart: number | undefined; + + /** OS-aware modifier key for shortcut labels. */ + private readonly _mod = process.platform === 'darwin' ? 'โŒ˜' : 'Ctrl'; + + constructor() { + const service = ScratchpadService.getInstance(); + + // Refresh lenses when connection/execution state changes + this._disposables.push( + service.onDidChangeState(() => { + this._onDidChangeCodeLenses.fire(); + }), + ); + + // Refresh lenses when cursor moves to a different block + this._disposables.push( + vscode.window.onDidChangeTextEditorSelection((e) => { + if (e.textEditor.document.languageId !== SCRATCHPAD_LANGUAGE_ID) { + return; + } + const cursorLine = e.selections[0].active.line; + const blocks = detectBlocks(e.textEditor.document); + const newStart = this.resolveActiveBlock(blocks, cursorLine)?.startLine; + + if (newStart !== this._lastActiveBlockStart) { + this._lastActiveBlockStart = newStart; + this._onDidChangeCodeLenses.fire(); + } + }), + ); + } + + /** + * Find the block at the cursor line. If the cursor is on a blank line + * between blocks, fall back to the nearest preceding block to avoid + * CodeLens flickering. + */ + private resolveActiveBlock( + blocks: ReturnType, + cursorLine: number, + ): ReturnType { + const direct = findBlockAtLine(blocks, cursorLine); + if (direct) { + return direct; + } + // Fall back: find the last block that ends before the cursor + for (let i = blocks.length - 1; i >= 0; i--) { + if (blocks[i].endLine < cursorLine) { + return blocks[i]; + } + } + return undefined; + } + + provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] { + const lenses: vscode.CodeLens[] = []; + const service = ScratchpadService.getInstance(); + const topRange = new vscode.Range(0, 0, 0, 0); + + // 1. Connection status lens + if (service.isConnected()) { + const displayName = service.getDisplayName()!; + lenses.push( + new vscode.CodeLens(topRange, { + title: `$(plug) ${displayName}`, + command: ScratchpadCommandIds.connect, + tooltip: l10n.t('Connected to {0}', displayName), + }), + ); + } else { + lenses.push( + new vscode.CodeLens(topRange, { + title: `$(plug) ${l10n.t('Connect to a database')}`, + command: ScratchpadCommandIds.connect, + tooltip: l10n.t('Click to learn how to connect'), + }), + ); + } + + // 2. Run All lens + const runAllTitle = service.isExecuting + ? `$(loading~spin) ${l10n.t('Runningโ€ฆ')}` + : `$(run-all) ${l10n.t('Run All')}`; + lenses.push( + new vscode.CodeLens(topRange, { + title: runAllTitle, + command: ScratchpadCommandIds.runAll, + tooltip: l10n.t('Run the entire file ({0}+Shift+Enter)', this._mod), + }), + ); + + // 3. Per-block Run lens โ€” only for the block containing the cursor + // Falls back to the nearest preceding block when cursor is between blocks + const editor = vscode.window.activeTextEditor; + if (editor && editor.document === document) { + const cursorLine = editor.selection.active.line; + const blocks = detectBlocks(document); + const activeBlock = this.resolveActiveBlock(blocks, cursorLine); + + if (activeBlock) { + const blockRange = new vscode.Range(activeBlock.startLine, 0, activeBlock.startLine, 0); + const runTitle = service.isExecuting + ? `$(loading~spin) ${l10n.t('Runningโ€ฆ')}` + : `$(play) ${l10n.t('Run')}`; + lenses.push( + new vscode.CodeLens(blockRange, { + title: runTitle, + command: ScratchpadCommandIds.runSelected, + arguments: [activeBlock.startLine, activeBlock.endLine], + tooltip: l10n.t('Run this block ({0}+Enter)', this._mod), + }), + ); + } + } + + return lenses; + } + + dispose(): void { + this._onDidChangeCodeLenses.dispose(); + for (const d of this._disposables) { + d?.dispose(); + } + } +} diff --git a/src/documentdb/scratchpad/ScratchpadEvaluator.ts b/src/documentdb/scratchpad/ScratchpadEvaluator.ts new file mode 100644 index 000000000..fccabcb95 --- /dev/null +++ b/src/documentdb/scratchpad/ScratchpadEvaluator.ts @@ -0,0 +1,586 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as l10n from '@vscode/l10n'; +import { randomUUID } from 'crypto'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { Worker } from 'worker_threads'; +import { ext } from '../../extensionVariables'; +import { CredentialCache } from '../CredentialCache'; +import { type ExecutionResult, type ScratchpadConnection } from './types'; +import { + type MainToWorkerMessage, + type SerializableExecutionResult, + type SerializableMongoClientOptions, + type WorkerToMainMessage, +} from './workerTypes'; + +/** Worker lifecycle states */ +type WorkerState = 'idle' | 'spawning' | 'ready' | 'executing'; + +/** + * Evaluates scratchpad code in a persistent worker thread. + * + * The worker owns its own database client (authenticated via credentials from + * `CredentialCache`) and stays alive between runs. This provides: + * - Infinite loop safety (main thread can kill the worker) + * - Client isolation from the Collection View + * - Zero re-auth overhead after the first run + * + * The public API is unchanged from the in-process evaluator: + * `evaluate(connection, code) โ†’ Promise` + */ +export class ScratchpadEvaluator implements vscode.Disposable { + private _worker: Worker | undefined; + private _workerState: WorkerState = 'idle'; + /** Which cluster the live worker is connected to (to detect cluster switches) */ + private _workerClusterId: string | undefined; + + /** + * Telemetry session ID โ€” generated on worker spawn, stable across evals within the + * same worker lifecycle. Resets when the worker is terminated/respawned. + * Used to correlate multiple scratchpad runs within a single "session". + */ + private _sessionId: string | undefined; + + /** Number of eval calls completed during this worker session (for usage tracking). */ + private _sessionEvalCount: number = 0; + + /** Auth mechanism used for the current worker session (for telemetry). */ + private _sessionAuthMethod: string | undefined; + + /** Pending request correlation map: requestId โ†’ { resolve, reject } */ + private _pendingRequests = new Map< + string, + { + resolve: (value: unknown) => void; + reject: (error: Error) => void; + } + >(); + + /** Telemetry accessors โ€” read by the command layer for telemetry properties. */ + get sessionId(): string | undefined { + return this._sessionId; + } + get sessionEvalCount(): number { + return this._sessionEvalCount; + } + get sessionAuthMethod(): string | undefined { + return this._sessionAuthMethod; + } + + /** Duration of the last worker init (spawn + auth), in ms. 0 if worker was already alive. */ + private _lastInitDurationMs: number = 0; + get lastInitDurationMs(): number { + return this._lastInitDurationMs; + } + + /** + * Evaluate user code against the connected database. + * + * @param connection - Active scratchpad connection (clusterId + databaseName). + * @param code - JavaScript code string to evaluate. + * @param onProgress - Optional callback for phased progress reporting. + * @returns Formatted execution result with type, printable value, and timing. + */ + async evaluate( + connection: ScratchpadConnection, + code: string, + onProgress?: (message: string) => void, + ): Promise { + // Intercept scratchpad-specific commands before they reach the worker + const trimmed = code.trim(); + const helpResult = this.tryHandleHelp(trimmed); + if (helpResult) { + return { ...helpResult, durationMs: 0 }; + } + + // Ensure worker is alive and connected to the right cluster + const needsSpawn = + !this._worker || this._workerState === 'idle' || this._workerClusterId !== connection.clusterId; + if (needsSpawn) { + onProgress?.(l10n.t('Initializingโ€ฆ')); + } + + const initStartTime = Date.now(); + await this.ensureWorker(connection, onProgress); + this._lastInitDurationMs = needsSpawn ? Date.now() - initStartTime : 0; + + // Send eval message and await result + onProgress?.(l10n.t('Running queryโ€ฆ')); + const timeoutSec = vscode.workspace.getConfiguration().get(ext.settingsKeys.shellTimeout) ?? 30; + const timeoutMs = timeoutSec * 1000; + + const result = await this.sendEval(connection, code, timeoutMs); + return result; + } + + /** + * Gracefully shut down the worker: close the database client, then terminate thread. + * Returns after the worker has confirmed shutdown or after a timeout. + */ + async shutdown(): Promise { + if (!this._worker || this._workerState === 'idle') { + return; + } + + try { + await this.sendRequest({ type: 'shutdown', requestId: '' }, 5000); + } catch { + // Shutdown timed out or failed โ€” force-kill + } + + this.terminateWorker(); + } + + /** + * Force-terminate the worker thread immediately. + * Used for infinite loop recovery (timeout) and cancellation. + */ + killWorker(): void { + this.terminateWorker(); + } + + dispose(): void { + this.terminateWorker(); + } + + // โ”€โ”€โ”€ Private: Worker lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Ensure a worker is alive and connected to the correct cluster. + * Spawns a new worker if needed (lazy), or kills and respawns if the + * cluster has changed. + */ + private async ensureWorker( + connection: ScratchpadConnection, + onProgress?: (message: string) => void, + ): Promise { + // If worker is alive but connected to a different cluster, shut it down + if (this._worker && this._workerClusterId !== connection.clusterId) { + this.terminateWorker(); + } + + // If no worker exists, spawn one + if (!this._worker || this._workerState === 'idle') { + await this.spawnWorker(connection, onProgress); + } + } + + /** + * Spawn a new worker thread and send the init message. + */ + private async spawnWorker(connection: ScratchpadConnection, onProgress?: (message: string) => void): Promise { + this._workerState = 'spawning'; + + // Resolve worker script path (same directory as the main bundle in dist/) + const workerPath = path.join(__dirname, 'scratchpadWorker.js'); + this._worker = new Worker(workerPath); + this._workerClusterId = connection.clusterId; + + // Listen for messages from the worker + this._worker.on('message', (msg: WorkerToMainMessage) => { + this.handleWorkerMessage(msg); + }); + + // Listen for worker exit (crash or termination) + this._worker.on('exit', (exitCode: number) => { + ext.outputChannel.debug(`[Scratchpad Worker] Worker exited with code ${String(exitCode)}`); + this.handleWorkerExit(); + }); + + this._worker.on('error', (error: Error) => { + ext.outputChannel.error(`[Scratchpad Worker] ${error.message}`); + }); + + // Build init message from cached credentials and send to worker. + // If init fails (bad credentials, unreachable host, etc.), tear down + // the worker so the next evaluate() call can respawn cleanly. + try { + const initMsg = this.buildInitMessage(connection); + + // Start a new telemetry session for this worker lifecycle + this._sessionId = randomUUID(); + this._sessionEvalCount = 0; + this._sessionAuthMethod = initMsg.authMechanism; + + // Send init and wait for acknowledgment + onProgress?.(l10n.t('Authenticatingโ€ฆ')); + await this.sendRequest(initMsg, 30000); + this._workerState = 'ready'; + } catch (error) { + this.terminateWorker(); + throw error; + } + } + + /** + * Build the init message from CredentialCache data. + */ + private buildInitMessage(connection: ScratchpadConnection): MainToWorkerMessage & { type: 'init' } { + const credentials = CredentialCache.getCredentials(connection.clusterId); + if (!credentials) { + throw new Error(l10n.t('No credentials found for cluster {0}', connection.clusterId)); + } + + const authMechanism = credentials.authMechanism ?? 'NativeAuth'; + + // Build connection string + let connectionString: string; + if (authMechanism === 'NativeAuth') { + connectionString = CredentialCache.getConnectionStringWithPassword(connection.clusterId); + } else { + // Entra ID: use connection string without embedded credentials + connectionString = credentials.connectionString; + } + + // Build serializable MongoClientOptions + const clientOptions: SerializableMongoClientOptions = { + serverSelectionTimeoutMS: credentials.emulatorConfiguration?.isEmulator ? 4000 : undefined, + tlsAllowInvalidCertificates: + credentials.emulatorConfiguration?.isEmulator && + credentials.emulatorConfiguration?.disableEmulatorSecurity + ? true + : undefined, + }; + + return { + type: 'init', + requestId: '', + connectionString, + clientOptions, + databaseName: connection.databaseName, + authMechanism: authMechanism as 'NativeAuth' | 'MicrosoftEntraID', + tenantId: credentials.entraIdConfig?.tenantId, + // TODO(F11): Read from documentDB.mongoShell.batchSize setting and wire in worker + displayBatchSize: 50, + }; + } + + /** + * Send an eval message to the worker and await the result. + */ + private async sendEval( + connection: ScratchpadConnection, + code: string, + timeoutMs: number, + ): Promise { + this._workerState = 'executing'; + this._sessionEvalCount++; + + const evalMsg: MainToWorkerMessage = { + type: 'eval', + requestId: '', + code, + databaseName: connection.databaseName, + }; + + try { + const result = await this.sendRequest<{ result: SerializableExecutionResult }>(evalMsg, timeoutMs); + + // Deserialize the result โ€” printable is a canonical EJSON string from the worker. + // Canonical EJSON preserves all BSON types (ObjectId, Date, Decimal128, Int32, + // Long, Double, etc.) so that SchemaAnalyzer correctly identifies field types. + const serResult = result.result; + let printable: unknown; + try { + const { EJSON } = await import('bson'); + printable = EJSON.parse(serResult.printable, { relaxed: false }); + } catch { + // Fallback to JSON.parse if EJSON fails, then raw string + try { + printable = JSON.parse(serResult.printable) as unknown; + } catch { + printable = serResult.printable; + } + } + + return { + type: serResult.type, + printable, + durationMs: serResult.durationMs, + source: serResult.source, + }; + } finally { + if (this._workerState === 'executing') { + this._workerState = 'ready'; + } + } + } + + // โ”€โ”€โ”€ Private: IPC request/response โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Send a message to the worker and return a promise that resolves + * when the corresponding response arrives. + */ + private sendRequest(msg: MainToWorkerMessage, timeoutMs: number): Promise { + if (!this._worker) { + return Promise.reject(new Error(l10n.t('Worker is not running'))); + } + + const requestId = randomUUID(); + const msgWithId = { ...msg, requestId }; + + return new Promise((resolve, reject) => { + this._pendingRequests.set(requestId, { + resolve: resolve as (value: unknown) => void, + reject, + }); + + // Timeout โ€” kills the worker for safety (infinite loop protection) + const timer = setTimeout(() => { + const pending = this._pendingRequests.get(requestId); + if (pending) { + this._pendingRequests.delete(requestId); + this.killWorker(); + pending.reject( + new Error( + l10n.t('Operation timed out after {0} seconds', String(Math.round(timeoutMs / 1000))), + ), + ); + } + }, timeoutMs); + + // Store timer reference on the pending entry so we can clear it + const entry = this._pendingRequests.get(requestId)!; + const originalResolve = entry.resolve; + const originalReject = entry.reject; + entry.resolve = (value: unknown) => { + clearTimeout(timer); + originalResolve(value); + }; + entry.reject = (error: Error) => { + clearTimeout(timer); + originalReject(error); + }; + + this._worker!.postMessage(msgWithId); + }); + } + + /** + * Handle an incoming message from the worker. + */ + private handleWorkerMessage(msg: WorkerToMainMessage): void { + switch (msg.type) { + case 'initResult': { + const pending = this._pendingRequests.get(msg.requestId); + if (pending) { + this._pendingRequests.delete(msg.requestId); + if (msg.success) { + pending.resolve(undefined); + } else { + pending.reject(new Error(msg.error ?? 'Worker init failed')); + } + } + break; + } + + case 'evalResult': { + const pending = this._pendingRequests.get(msg.requestId); + if (pending) { + this._pendingRequests.delete(msg.requestId); + pending.resolve(msg); + } + break; + } + + case 'evalError': { + const pending = this._pendingRequests.get(msg.requestId); + if (pending) { + this._pendingRequests.delete(msg.requestId); + const error = new Error(msg.error); + if (msg.stack) { + error.stack = msg.stack; + } + pending.reject(error); + } + break; + } + + case 'shutdownComplete': { + const pending = this._pendingRequests.get(msg.requestId); + if (pending) { + this._pendingRequests.delete(msg.requestId); + pending.resolve(undefined); + } + break; + } + + case 'tokenRequest': { + // Entra ID: worker needs an OIDC token โ€” delegate to main thread VS Code API + void this.handleTokenRequest(msg); + break; + } + + case 'log': { + const prefix = '[Scratchpad Worker]'; + switch (msg.level) { + case 'error': + ext.outputChannel.error(`${prefix} ${msg.message}`); + break; + case 'warn': + ext.outputChannel.warn(`${prefix} ${msg.message}`); + break; + case 'debug': + ext.outputChannel.debug(`${prefix} ${msg.message}`); + break; + default: + ext.outputChannel.trace(`${prefix} ${msg.message}`); + break; + } + break; + } + } + } + + /** + * Handle a token request from the worker (Entra ID OIDC). + * Calls VS Code's auth API on the main thread and sends the token back. + */ + private async handleTokenRequest(msg: Extract): Promise { + try { + const { getSessionFromVSCode } = await import( + // eslint-disable-next-line import/no-internal-modules + '@microsoft/vscode-azext-azureauth/out/src/getSessionFromVSCode' + ); + const session = await getSessionFromVSCode(msg.scopes as string[], msg.tenantId, { createIfNone: true }); + + if (!session) { + throw new Error('Failed to obtain Entra ID session'); + } + + const response: MainToWorkerMessage = { + type: 'tokenResponse', + requestId: msg.requestId, + accessToken: session.accessToken, + }; + this._worker?.postMessage(response); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + const response: MainToWorkerMessage = { + type: 'tokenError', + requestId: msg.requestId, + error: errorMessage, + }; + this._worker?.postMessage(response); + } + } + + // โ”€โ”€โ”€ Private: Worker cleanup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private terminateWorker(): void { + if (this._worker) { + void this._worker.terminate(); + this._worker = undefined; + } + this._workerState = 'idle'; + this._workerClusterId = undefined; + this._sessionId = undefined; + this._sessionEvalCount = 0; + this._sessionAuthMethod = undefined; + + // Reject all pending requests + for (const [, entry] of this._pendingRequests) { + entry.reject(new Error('Worker terminated')); + } + this._pendingRequests.clear(); + } + + private handleWorkerExit(): void { + this._worker = undefined; + this._workerState = 'idle'; + this._workerClusterId = undefined; + this._sessionId = undefined; + this._sessionEvalCount = 0; + this._sessionAuthMethod = undefined; + + // Reject any still-pending requests + for (const [, entry] of this._pendingRequests) { + entry.reject(new Error('Worker exited unexpectedly')); + } + this._pendingRequests.clear(); + } + + // โ”€โ”€โ”€ Help command interception โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Handle `help` command with scratchpad-specific output. + * Returns undefined if the input is not a help command. + */ + private tryHandleHelp(input: string): Omit | undefined { + if (input !== 'help' && input !== 'help()') { + return undefined; + } + + const helpText = [ + 'DocumentDB Scratchpad โ€” Quick Reference', + 'โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•', + '', + 'Collection Access:', + ' db.getCollection("name") Explicit (recommended)', + ' db.name Shorthand (also works)', + '', + 'Query Commands:', + ' db.getCollection("name").find({}) Find documents', + ' db.getCollection("name").findOne({}) Find one document', + ' db.getCollection("name").countDocuments({}) Count documents', + ' db.getCollection("name").estimatedDocumentCount() Fast count', + ' db.getCollection("name").distinct("field") Distinct values', + ' db.getCollection("name").aggregate([...]) Aggregation pipeline', + '', + 'Write Commands:', + ' db.getCollection("name").insertOne({...}) Insert a document', + ' db.getCollection("name").insertMany([...]) Insert multiple documents', + ' db.getCollection("name").updateOne({}, {$set:{}}) Update one', + ' db.getCollection("name").replaceOne({}, {...}) Replace one', + ' db.getCollection("name").deleteOne({}) Delete one', + ' db.getCollection("name").bulkWrite([...]) Batch operations', + '', + 'Index Commands:', + ' db.getCollection("name").createIndex({field:1}) Create index', + ' db.getCollection("name").getIndexes() List indexes', + ' db.getCollection("name").dropIndex("name") Drop index', + '', + 'Cursor Modifiers:', + ' .limit(n) Limit results', + ' .skip(n) Skip results', + ' .sort({field: 1}) Sort results', + ' .project({field: 1}) Field projection', + ' .toArray() Get all results', + ' .count() Count matching', + ' .explain() Query plan', + '', + 'Database Commands:', + ' show dbs List databases', + ' show collections List collections', + ' db.getCollectionNames() List collection names', + ' db.getCollectionInfos() Collection metadata', + ' db.createCollection("name") Create collection', + ' db.getCollection("name").drop() Drop collection', + ' db.runCommand({...}) Run database command', + ' db.adminCommand({...}) Run admin command', + '', + 'BSON Constructors:', + ' ObjectId("...") Create ObjectId', + ' ISODate("...") Create Date', + ' NumberDecimal("...") Create Decimal128', + '', + 'Keyboard Shortcuts:', + ` ${process.platform === 'darwin' ? 'โŒ˜' : 'Ctrl'}+Enter Run current block`, + ` ${process.platform === 'darwin' ? 'โŒ˜' : 'Ctrl'}+Shift+Enter Run entire file`, + '', + 'Tips:', + ' โ€ข Separate code blocks with blank lines', + ' โ€ข Variables persist within a block but not between separate runs', + ' โ€ข When running multiple statements, only the last result is shown', + ' โ€ข Use .toArray() to get all results (default: first 20 documents)', + ].join('\n'); + + return { type: 'Help', printable: helpText }; + } +} diff --git a/src/documentdb/scratchpad/ScratchpadService.test.ts b/src/documentdb/scratchpad/ScratchpadService.test.ts new file mode 100644 index 000000000..55688303d --- /dev/null +++ b/src/documentdb/scratchpad/ScratchpadService.test.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ScratchpadService } from './ScratchpadService'; +import { type ScratchpadConnection } from './types'; + +// Access the vscode mock (auto-mock from __mocks__/vscode.js) +import * as vscode from 'vscode'; + +describe('ScratchpadService', () => { + let service: ScratchpadService; + + beforeEach(() => { + // Reset singleton between tests + service = ScratchpadService.getInstance(); + }); + + afterEach(() => { + service.dispose(); + }); + + describe('singleton', () => { + it('returns the same instance on repeated calls', () => { + const second = ScratchpadService.getInstance(); + expect(second).toBe(service); + }); + + it('creates a new instance after dispose', () => { + service.dispose(); + const fresh = ScratchpadService.getInstance(); + expect(fresh).not.toBe(service); + service = fresh; // reassign for afterEach cleanup + }); + }); + + describe('connection management', () => { + const connection: ScratchpadConnection = { + clusterId: 'cluster-123', + clusterDisplayName: 'MyCluster', + databaseName: 'orders', + }; + + it('starts disconnected', () => { + expect(service.isConnected()).toBe(false); + expect(service.getConnection()).toBeUndefined(); + expect(service.getDisplayName()).toBeUndefined(); + }); + + it('setConnection stores the connection', () => { + service.setConnection(connection); + expect(service.isConnected()).toBe(true); + expect(service.getConnection()).toBe(connection); + }); + + it('getDisplayName returns formatted string', () => { + service.setConnection(connection); + expect(service.getDisplayName()).toBe('MyCluster / orders'); + }); + + it('clearConnection resets to disconnected', () => { + service.setConnection(connection); + service.clearConnection(); + expect(service.isConnected()).toBe(false); + expect(service.getConnection()).toBeUndefined(); + }); + + it('fires onDidChangeState on setConnection', () => { + const listener = jest.fn(); + service.onDidChangeState(listener); + service.setConnection(connection); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('fires onDidChangeState on clearConnection', () => { + service.setConnection(connection); + const listener = jest.fn(); + service.onDidChangeState(listener); + service.clearConnection(); + expect(listener).toHaveBeenCalledTimes(1); + }); + }); + + describe('execution state', () => { + it('starts not executing', () => { + expect(service.isExecuting).toBe(false); + }); + + it('setExecuting updates state and fires event', () => { + const listener = jest.fn(); + service.onDidChangeState(listener); + service.setExecuting(true); + expect(service.isExecuting).toBe(true); + expect(listener).toHaveBeenCalledTimes(1); + }); + }); + + describe('StatusBarItem', () => { + it('creates a StatusBarItem with the connect command', () => { + // The StatusBarItem is created in the constructor; verify it was configured + expect(vscode.window.createStatusBarItem).toHaveBeenCalledWith(vscode.StatusBarAlignment.Left, 100); + }); + }); +}); diff --git a/src/documentdb/scratchpad/ScratchpadService.ts b/src/documentdb/scratchpad/ScratchpadService.ts new file mode 100644 index 000000000..a6435e04a --- /dev/null +++ b/src/documentdb/scratchpad/ScratchpadService.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { SCRATCHPAD_LANGUAGE_ID, ScratchpadCommandIds } from './constants'; +import { type ScratchpadConnection } from './types'; + +/** + * Singleton service managing the active scratchpad connection and execution state. + * + * Design decisions (from 06-scrapbook-rebuild.md): + * - D1 (Option B): All scratchpad files share a single global connection + * - StatusBarItem shows connection status when a `.documentdb` file is active + * - Service emits state changes so UI components (CodeLens, StatusBar) can refresh + */ +export class ScratchpadService implements vscode.Disposable { + private static _instance: ScratchpadService | undefined; + + private _connection: ScratchpadConnection | undefined; + private _isExecuting = false; + + private readonly _onDidChangeState = new vscode.EventEmitter(); + readonly onDidChangeState: vscode.Event = this._onDidChangeState.event; + + private readonly _statusBarItem: vscode.StatusBarItem; + private readonly _disposables: vscode.Disposable[] = []; + + private constructor() { + // StatusBarItem โ€” left-aligned, shown only when a scratchpad file is the active editor + this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + this._statusBarItem.command = ScratchpadCommandIds.connect; + this._disposables.push(this._statusBarItem); + + // Update StatusBar visibility when the active editor changes + this._disposables.push( + vscode.window.onDidChangeActiveTextEditor((editor) => { + this.updateStatusBar(editor); + }), + ); + + // Also listen for state changes to refresh the bar text + this._disposables.push( + this.onDidChangeState(() => { + this.updateStatusBar(vscode.window.activeTextEditor); + }), + ); + + // Initialize with current editor + this.updateStatusBar(vscode.window.activeTextEditor); + } + + static getInstance(): ScratchpadService { + if (!ScratchpadService._instance) { + ScratchpadService._instance = new ScratchpadService(); + } + return ScratchpadService._instance; + } + + // โ”€โ”€ Connection management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + setConnection(connection: ScratchpadConnection): void { + this._connection = connection; + this._onDidChangeState.fire(); + } + + clearConnection(): void { + this._connection = undefined; + this._onDidChangeState.fire(); + } + + isConnected(): boolean { + return this._connection !== undefined; + } + + getConnection(): ScratchpadConnection | undefined { + return this._connection; + } + + /** + * Returns a human-readable display string for the active connection, + * e.g. "MyCluster / orders". Returns `undefined` if disconnected. + */ + getDisplayName(): string | undefined { + if (!this._connection) { + return undefined; + } + return `${this._connection.clusterDisplayName} / ${this._connection.databaseName}`; + } + + // โ”€โ”€ Execution state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + get isExecuting(): boolean { + return this._isExecuting; + } + + setExecuting(executing: boolean): void { + this._isExecuting = executing; + this._onDidChangeState.fire(); + } + + // โ”€โ”€ StatusBar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private updateStatusBar(editor: vscode.TextEditor | undefined): void { + if (!editor || editor.document.languageId !== SCRATCHPAD_LANGUAGE_ID) { + this._statusBarItem.hide(); + return; + } + + if (this._connection) { + const displayName = this.getDisplayName()!; + this._statusBarItem.text = `$(plug) ${displayName}`; + this._statusBarItem.tooltip = l10n.t('DocumentDB Scratchpad connected to {0}', displayName); + } else { + this._statusBarItem.text = `$(warning) ${l10n.t('No database connected')}`; + this._statusBarItem.tooltip = l10n.t( + 'Click to learn how to connect a database for the DocumentDB Scratchpad', + ); + } + + this._statusBarItem.show(); + } + + // โ”€โ”€ Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + dispose(): void { + for (const d of this._disposables) { + d?.dispose(); + } + this._onDidChangeState.dispose(); + ScratchpadService._instance = undefined; + } +} diff --git a/src/documentdb/scratchpad/constants.ts b/src/documentdb/scratchpad/constants.ts new file mode 100644 index 000000000..9089c5b65 --- /dev/null +++ b/src/documentdb/scratchpad/constants.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Language ID registered in package.json for `.documentdb` and `.documentdb.js` files. + */ +export const SCRATCHPAD_LANGUAGE_ID = 'documentdb-scratchpad'; + +/** + * Primary file extension for scratchpad files. + */ +export const SCRATCHPAD_FILE_EXTENSION = '.documentdb'; + +/** + * Secondary file extension โ€” recognized as JavaScript by tooling. + */ +export const SCRATCHPAD_FILE_EXTENSION_JS = '.documentdb.js'; + +/** + * Command IDs for scratchpad features. + */ +export const ScratchpadCommandIds = { + /** Create a new scratchpad file and optionally connect */ + new: 'vscode-documentdb.command.scratchpad.new', + /** Set the active scratchpad connection from a tree node */ + connect: 'vscode-documentdb.command.scratchpad.connect', + /** Run the entire scratchpad file */ + runAll: 'vscode-documentdb.command.scratchpad.runAll', + /** Run the selection or the statement at the cursor */ + runSelected: 'vscode-documentdb.command.scratchpad.runSelected', +} as const; diff --git a/src/documentdb/scratchpad/resultFormatter.ts b/src/documentdb/scratchpad/resultFormatter.ts new file mode 100644 index 000000000..2b21e368d --- /dev/null +++ b/src/documentdb/scratchpad/resultFormatter.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as l10n from '@vscode/l10n'; +import { EJSON } from 'bson'; +import { type ExecutionResult, type ScratchpadConnection } from './types'; + +/** + * Formats a scratchpad execution result for display in a read-only output panel. + * + * Output includes: + * - The executed code (truncated if long) + * - Result metadata (type, timing, document count) + * - Formatted result value (EJSON for documents, raw for scalars) + */ +export function formatResult(result: ExecutionResult, code: string, connection: ScratchpadConnection): string { + const lines: string[] = []; + + // Connection and timestamp + lines.push(`// ${connection.clusterDisplayName} / ${connection.databaseName}`); + lines.push(`// ${new Date().toLocaleString()}`); + lines.push('//'); + + // Code echo โ€” truncate to first 120 chars if long + const codePreview = code.length > 120 ? code.slice(0, 120) + 'โ€ฆ' : code; + for (const codeLine of codePreview.split('\n')) { + lines.push(`// โ–ถ ${codeLine}`); + } + + // Result metadata + // Result metadata โ€” state what we know from @mongosh, don't guess + const unwrapped = unwrapCursorResult(result.printable); + if (result.type === 'Cursor' && Array.isArray(unwrapped)) { + // Cursor with a known batch: "Result: Cursor (20 documents)" + lines.push(`// ${l10n.t('Result: Cursor ({0} documents)', unwrapped.length)}`); + } else if (result.type) { + // Typed result: "Result: Document", "Result: string", etc. + lines.push(`// ${l10n.t('Result: {0}', result.type)}`); + } else if (Array.isArray(unwrapped)) { + // Untyped array (e.g. .toArray()) โ€” show type and count + lines.push(`// ${l10n.t('Result: Array ({0} elements)', unwrapped.length)}`); + } + + lines.push(`// ${l10n.t('Executed in {0}ms', result.durationMs)}`); + lines.push('// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); + lines.push(''); + + // Result value โ€” unwrap cursor wrapper for clean output + lines.push(formatPrintable(unwrapCursorResult(result.printable))); + + return lines.join('\n'); +} + +/** + * Formats an error from scratchpad execution for display. + */ +export function formatError( + error: unknown, + code: string, + durationMs: number, + connection: ScratchpadConnection, +): string { + const lines: string[] = []; + + // Connection and timestamp + lines.push(`// ${connection.clusterDisplayName} / ${connection.databaseName}`); + lines.push(`// ${new Date().toLocaleString()}`); + lines.push('//'); + + const codePreview = code.length > 120 ? code.slice(0, 120) + 'โ€ฆ' : code; + for (const codeLine of codePreview.split('\n')) { + lines.push(`// โ–ถ ${codeLine}`); + } + + lines.push(`// โŒ ${l10n.t('Error executing query')}`); + lines.push(`// ${l10n.t('Executed in {0}ms', durationMs)}`); + lines.push('// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); + lines.push(''); + + const errorMessage = error instanceof Error ? error.message : String(error); + lines.push(errorMessage); + + return lines.join('\n'); +} + +/** + * Unwrap CursorIterationResult from @mongosh. + * + * @mongosh's `asPrintable()` on CursorIterationResult produces + * `{ cursorHasMore: boolean, documents: unknown[] }` instead of a plain array. + * Only unwraps when the full wrapper shape is present to avoid + * false positives on user documents that happen to have a `documents` field. + */ +function unwrapCursorResult(printable: unknown): unknown { + if ( + printable !== null && + printable !== undefined && + typeof printable === 'object' && + !Array.isArray(printable) && + 'cursorHasMore' in printable && + typeof (printable as Record).cursorHasMore === 'boolean' && + 'documents' in printable && + Array.isArray((printable as { documents: unknown }).documents) + ) { + return (printable as { documents: unknown[] }).documents; + } + return printable; +} + +function formatPrintable(printable: unknown): string { + if (printable === undefined) { + return 'undefined'; + } + if (printable === null) { + return 'null'; + } + if (typeof printable === 'string') { + return printable; + } + if (typeof printable === 'number' || typeof printable === 'boolean') { + return String(printable); + } + // Documents, arrays, cursors โ€” use EJSON for structured output. + // Fall back to JSON.stringify with circular reference handling if EJSON fails. + try { + return EJSON.stringify(printable, undefined, 2, { relaxed: true }); + } catch { + try { + return JSON.stringify(printable, undefined, 2); + } catch { + return String(printable); + } + } +} diff --git a/src/documentdb/scratchpad/scratchpadWorker.ts b/src/documentdb/scratchpad/scratchpadWorker.ts new file mode 100644 index 000000000..b33dc08e5 --- /dev/null +++ b/src/documentdb/scratchpad/scratchpadWorker.ts @@ -0,0 +1,307 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Worker thread entry point for scratchpad code evaluation. + * + * This file runs in a Node.js worker_thread, isolated from the extension host. + * It owns its own database client instance (authenticated via credentials passed from + * the main thread at init time) and evaluates user code through the @mongosh pipeline. + * + * Communication with the main thread is via postMessage (structured clone). + * See workerTypes.ts for the message protocol. + */ + +import { randomUUID } from 'crypto'; +import { type MongoClientOptions, type MongoClient as MongoClientType } from 'mongodb'; +import { parentPort } from 'worker_threads'; +import { type MainToWorkerMessage, type WorkerToMainMessage } from './workerTypes'; + +if (!parentPort) { + throw new Error('scratchpadWorker.ts must be run as a worker_thread'); +} + +// โ”€โ”€โ”€ Worker state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +let mongoClient: MongoClientType | undefined; +let currentDatabaseName: string | undefined; + +/** + * Cache for pending Entra ID token requests from the OIDC callback. + * The OIDC_CALLBACK in the worker sends a tokenRequest to the main thread + * and awaits the response via this map. + */ +const pendingTokenRequests = new Map void; reject: (err: Error) => void }>(); + +// โ”€โ”€โ”€ Logging helper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function log(level: 'trace' | 'debug' | 'info' | 'warn' | 'error', message: string): void { + const msg: WorkerToMainMessage = { type: 'log', level, message }; + parentPort!.postMessage(msg); +} + +// โ”€โ”€โ”€ Message handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +parentPort.on('message', (msg: MainToWorkerMessage) => { + switch (msg.type) { + case 'init': + void handleInit(msg).catch((err: unknown) => { + const errorMessage = err instanceof Error ? err.message : String(err); + const response: WorkerToMainMessage = { + type: 'initResult', + requestId: msg.requestId, + success: false, + error: errorMessage, + }; + parentPort!.postMessage(response); + }); + break; + + case 'eval': + void handleEval(msg).catch((err: unknown) => { + const errorMessage = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + const response: WorkerToMainMessage = { + type: 'evalError', + requestId: msg.requestId, + error: errorMessage, + stack, + }; + parentPort!.postMessage(response); + }); + break; + + case 'shutdown': + void handleShutdown(msg); + break; + + case 'tokenResponse': + handleTokenResponse(msg); + break; + + case 'tokenError': + handleTokenError(msg); + break; + } +}); + +// โ”€โ”€โ”€ Init handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +async function handleInit(msg: Extract): Promise { + log('debug', `Initializing worker (auth: ${msg.authMechanism}, db: ${msg.databaseName})`); + + // Lazy-import MongoDB driver + const { MongoClient } = await import('mongodb'); + + // Build client options from the serializable subset + const options: MongoClientOptions = { + ...msg.clientOptions, + }; + + // For Entra ID, configure OIDC callback that requests tokens via IPC + if (msg.authMechanism === 'MicrosoftEntraID') { + options.authMechanism = 'MONGODB-OIDC'; + options.tls = true; + options.authMechanismProperties = { + ALLOWED_HOSTS: ['*.azure.com'], + OIDC_CALLBACK: async (): Promise<{ accessToken: string; expiresInSeconds: number }> => { + const requestId = randomUUID(); + const tokenPromise = new Promise((resolve, reject) => { + pendingTokenRequests.set(requestId, { resolve, reject }); + }); + const tokenRequest: WorkerToMainMessage = { + type: 'tokenRequest', + requestId, + scopes: ['https://ossrdbms-aad.database.windows.net/.default'], + tenantId: msg.tenantId, + }; + parentPort!.postMessage(tokenRequest); + const accessToken = await tokenPromise; + return { accessToken, expiresInSeconds: 0 }; + }, + }; + } + + // Create and connect the database client + mongoClient = new MongoClient(msg.connectionString, options); + await mongoClient.connect(); + currentDatabaseName = msg.databaseName; + + log('debug', 'Worker initialized โ€” client connected'); + + const response: WorkerToMainMessage = { + type: 'initResult', + requestId: msg.requestId, + success: true, + }; + parentPort!.postMessage(response); +} + +// โ”€โ”€โ”€ Eval handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +async function handleEval(msg: Extract): Promise { + if (!mongoClient) { + throw new Error('Worker not initialized โ€” call init first'); + } + + const lineCount = msg.code.split('\n').length; + log( + 'trace', + `Evaluating code (${String(lineCount)} lines, ${String(msg.code.length)} chars, db: ${msg.databaseName})`, + ); + + // Lazy-import @mongosh packages + const { EventEmitter } = await import('events'); + const vm = await import('vm'); + const { NodeDriverServiceProvider } = await import('@mongosh/service-provider-node-driver'); + const { ShellInstanceState } = await import('@mongosh/shell-api'); + const { ShellEvaluator } = await import('@mongosh/shell-evaluator'); + + const startTime = Date.now(); + + // Create fresh shell context per execution (no variable leakage between runs) + const bus = new EventEmitter(); + const serviceProvider = new NodeDriverServiceProvider(mongoClient, bus, { + productDocsLink: 'https://github.com/microsoft/vscode-documentdb', + productName: 'DocumentDB for VS Code Scratchpad', + }); + const instanceState = new ShellInstanceState(serviceProvider, bus); + const evaluator = new ShellEvaluator(instanceState); + + // Set up eval context with shell globals (db, ObjectId, ISODate, etc.) + const context = {}; + instanceState.setCtx(context); + + // The eval function using vm.runInContext for @mongosh + // eslint-disable-next-line @typescript-eslint/require-await + const customEvalFn = async (code: string, ctx: object): Promise => { + const vmContext = vm.isContext(ctx) ? ctx : vm.createContext(ctx); + return vm.runInContext(code, vmContext) as unknown; + }; + + // Switch database if different from current + if (msg.databaseName !== currentDatabaseName) { + await evaluator.customEval(customEvalFn, `use(${JSON.stringify(msg.databaseName)})`, context, 'scratchpad'); + currentDatabaseName = msg.databaseName; + } else { + // Pre-select the target database (fresh context each time) + await evaluator.customEval(customEvalFn, `use(${JSON.stringify(msg.databaseName)})`, context, 'scratchpad'); + } + + // Evaluate user code + const result = await evaluator.customEval(customEvalFn, msg.code, context, 'scratchpad'); + const durationMs = Date.now() - startTime; + + // result is a ShellResult { type, printable, rawValue, source? } + const shellResult = result as { + type: string | null; + printable: unknown; + source?: { namespace?: { db: string; collection: string } }; + }; + + // Normalize the printable value for IPC transfer. + // @mongosh's ShellEvaluator wraps cursor results as { cursorHasMore, documents } + // when running in a worker context. Extract the documents array so that the + // main thread receives a clean array (matching the in-process behavior where + // printable was a CursorIterationResult array). + let printableValue: unknown = shellResult.printable; + if ( + shellResult.type === 'Cursor' && + typeof shellResult.printable === 'object' && + shellResult.printable !== null && + 'documents' in shellResult.printable && + Array.isArray((shellResult.printable as { documents?: unknown }).documents) + ) { + printableValue = (shellResult.printable as { documents: unknown[] }).documents; + } else if (Array.isArray(shellResult.printable)) { + // Array subclass (CursorIterationResult) โ€” normalize to plain Array + printableValue = Array.from(shellResult.printable as unknown[]); + } + + let printableStr: string; + try { + const { EJSON } = await import('bson'); + printableStr = EJSON.stringify(printableValue, { relaxed: false }); + } catch { + // Fallback: try JSON, then plain string + try { + printableStr = JSON.stringify(printableValue); + } catch { + printableStr = String(printableValue); + } + } + + log('trace', `Evaluation complete (${durationMs}ms, type: ${shellResult.type ?? 'null'})`); + + const response: WorkerToMainMessage = { + type: 'evalResult', + requestId: msg.requestId, + result: { + type: shellResult.type, + printable: printableStr, + durationMs, + source: shellResult.source?.namespace + ? { + namespace: { + db: shellResult.source.namespace.db, + collection: shellResult.source.namespace.collection, + }, + } + : undefined, + }, + }; + parentPort!.postMessage(response); +} + +// โ”€โ”€โ”€ Shutdown handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +async function handleShutdown(msg: Extract): Promise { + log('debug', 'Shutting down worker โ€” closing client'); + + try { + if (mongoClient) { + await mongoClient.close(); + mongoClient = undefined; + } + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : String(err); + log('warn', `Error closing client during shutdown: ${errorMessage}`); + } + + const response: WorkerToMainMessage = { + type: 'shutdownComplete', + requestId: msg.requestId, + }; + parentPort!.postMessage(response); +} + +// โ”€โ”€โ”€ Token response/error handlers (Entra ID) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function handleTokenResponse(msg: Extract): void { + const pending = pendingTokenRequests.get(msg.requestId); + if (pending) { + pending.resolve(msg.accessToken); + pendingTokenRequests.delete(msg.requestId); + } +} + +function handleTokenError(msg: Extract): void { + const pending = pendingTokenRequests.get(msg.requestId); + if (pending) { + pending.reject(new Error(msg.error)); + pendingTokenRequests.delete(msg.requestId); + } +} + +// โ”€โ”€โ”€ Uncaught exception handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +process.on('uncaughtException', (error: Error) => { + log('error', `Uncaught exception in worker: ${error.message}\n${error.stack ?? ''}`); +}); + +process.on('unhandledRejection', (reason: unknown) => { + const message = reason instanceof Error ? reason.message : String(reason); + log('error', `Unhandled rejection in worker: ${message}`); +}); diff --git a/src/documentdb/scratchpad/statementDetector.test.ts b/src/documentdb/scratchpad/statementDetector.test.ts new file mode 100644 index 000000000..c4200c041 --- /dev/null +++ b/src/documentdb/scratchpad/statementDetector.test.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { detectBlocks, detectCurrentBlock, findBlockAtLine, type CodeBlock } from './statementDetector'; + +/** + * Helper to create a mock TextDocument from a multiline string. + */ +function mockDocument(text: string): vscode.TextDocument { + const lines = text.split('\n'); + return { + lineCount: lines.length, + lineAt(lineNumber: number) { + return { text: lines[lineNumber] ?? '' }; + }, + getText(range?: vscode.Range) { + if (!range) { + return text; + } + const startOffset = + lines.slice(0, range.start.line).reduce((acc, l) => acc + l.length + 1, 0) + range.start.character; + const endOffset = + lines.slice(0, range.end.line).reduce((acc, l) => acc + l.length + 1, 0) + range.end.character; + return text.substring(startOffset, endOffset); + }, + } as unknown as vscode.TextDocument; +} + +describe('statementDetector', () => { + describe('detectBlocks', () => { + it('returns a single block for code with no blank lines', () => { + const doc = mockDocument('var a = 1;\nvar b = 2;\nvar c = 3;'); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([{ startLine: 0, endLine: 2 }]); + }); + + it('splits on blank lines', () => { + const doc = mockDocument('db.users.find({});\n\ndb.orders.find({});'); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([ + { startLine: 0, endLine: 0 }, + { startLine: 2, endLine: 2 }, + ]); + }); + + it('does NOT split on comment-only lines', () => { + const doc = mockDocument('// Variables\nvar a = 1;\n// still same block\nvar b = 2;'); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([{ startLine: 0, endLine: 3 }]); + }); + + it('splits on whitespace-only lines', () => { + const doc = mockDocument('line1\n \nline3'); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([ + { startLine: 0, endLine: 0 }, + { startLine: 2, endLine: 2 }, + ]); + }); + + it('handles multiple blank lines between blocks', () => { + const doc = mockDocument('block1\n\n\n\nblock2'); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([ + { startLine: 0, endLine: 0 }, + { startLine: 4, endLine: 4 }, + ]); + }); + + it('handles empty document', () => { + const doc = mockDocument(''); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([]); + }); + + it('handles document with only blank lines', () => { + const doc = mockDocument('\n\n\n'); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([]); + }); + + it('handles file ending without trailing newline', () => { + const doc = mockDocument('first block\n\nsecond block'); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([ + { startLine: 0, endLine: 0 }, + { startLine: 2, endLine: 2 }, + ]); + }); + + it('handles multi-line blocks', () => { + const doc = mockDocument( + [ + 'db.orders.aggregate([', + ' { $match: { status: "active" } },', + ' { $group: { _id: "$userId" } }', + ']);', + '', + 'db.users.find({});', + ].join('\n'), + ); + const blocks = detectBlocks(doc); + expect(blocks).toEqual([ + { startLine: 0, endLine: 3 }, + { startLine: 5, endLine: 5 }, + ]); + }); + }); + + describe('findBlockAtLine', () => { + const blocks: CodeBlock[] = [ + { startLine: 0, endLine: 2 }, + { startLine: 5, endLine: 7 }, + ]; + + it('finds the block containing the line', () => { + expect(findBlockAtLine(blocks, 1)).toEqual({ startLine: 0, endLine: 2 }); + expect(findBlockAtLine(blocks, 6)).toEqual({ startLine: 5, endLine: 7 }); + }); + + it('returns undefined for lines between blocks', () => { + expect(findBlockAtLine(blocks, 3)).toBeUndefined(); + expect(findBlockAtLine(blocks, 4)).toBeUndefined(); + }); + + it('returns undefined for lines after all blocks', () => { + expect(findBlockAtLine(blocks, 10)).toBeUndefined(); + }); + }); + + describe('detectCurrentBlock', () => { + it('returns the block text at cursor position', () => { + const doc = mockDocument('line1\nline2\n\nline4'); + const position = new vscode.Position(0, 0); + const result = detectCurrentBlock(doc, position); + expect(result).toBe('line1\nline2'); + }); + + it('returns empty string when cursor is on a blank line', () => { + const doc = mockDocument('line1\n\nline3'); + const position = new vscode.Position(1, 0); + const result = detectCurrentBlock(doc, position); + expect(result).toBe(''); + }); + }); +}); diff --git a/src/documentdb/scratchpad/statementDetector.ts b/src/documentdb/scratchpad/statementDetector.ts new file mode 100644 index 000000000..21ff2e821 --- /dev/null +++ b/src/documentdb/scratchpad/statementDetector.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +/** + * A detected code block in a scratchpad document. + * Blocks are separated by blank lines (whitespace-only lines). + * Comment-only lines do NOT act as block separators. + */ +export interface CodeBlock { + /** 0-based start line of the block (first non-empty line). */ + readonly startLine: number; + /** 0-based end line of the block (last non-empty line, inclusive). */ + readonly endLine: number; +} + +/** + * Detects code blocks in a scratchpad document using blank-line separation. + * + * Rules: + * - Lines that are completely empty or whitespace-only are block separators. + * - Lines containing only comments (`//` or within block comments) are NOT separators. + * - Each contiguous group of non-blank lines forms a block. + */ +export function detectBlocks(document: vscode.TextDocument): CodeBlock[] { + const blocks: CodeBlock[] = []; + let blockStart: number | undefined; + + for (let i = 0; i < document.lineCount; i++) { + const lineText = document.lineAt(i).text; + const isBlank = lineText.trim().length === 0; + + if (!isBlank) { + if (blockStart === undefined) { + blockStart = i; + } + } else { + if (blockStart !== undefined) { + blocks.push({ startLine: blockStart, endLine: i - 1 }); + blockStart = undefined; + } + } + } + + // Close final block if file doesn't end with a blank line + if (blockStart !== undefined) { + blocks.push({ startLine: blockStart, endLine: document.lineCount - 1 }); + } + + return blocks; +} + +/** + * Returns the text of the code block containing the given cursor position. + * If the cursor is on a blank line (between blocks), returns an empty string. + */ +export function detectCurrentBlock(document: vscode.TextDocument, position: vscode.Position): string { + const blocks = detectBlocks(document); + + for (const block of blocks) { + if (position.line >= block.startLine && position.line <= block.endLine) { + const range = new vscode.Range( + block.startLine, + 0, + block.endLine, + document.lineAt(block.endLine).text.length, + ); + return document.getText(range); + } + } + + return ''; +} + +/** + * Finds the block that contains the given line number. + * Returns undefined if the line is in a blank region between blocks. + */ +export function findBlockAtLine(blocks: CodeBlock[], line: number): CodeBlock | undefined { + return blocks.find((b) => line >= b.startLine && line <= b.endLine); +} diff --git a/src/documentdb/scratchpad/types.ts b/src/documentdb/scratchpad/types.ts new file mode 100644 index 000000000..4ffe4a60d --- /dev/null +++ b/src/documentdb/scratchpad/types.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents the active scratchpad connection. + * All scratchpad files share a single global connection (Decision D1: Option B). + */ +export interface ScratchpadConnection { + /** Stable cluster ID for ClustersClient/CredentialCache lookups. */ + readonly clusterId: string; + /** Human-readable cluster name for display in CodeLens/StatusBar. */ + readonly clusterDisplayName: string; + /** Target database name for query execution. */ + readonly databaseName: string; +} + +/** + * Result of executing scratchpad code via the `@mongosh` eval pipeline. + */ +export interface ExecutionResult { + /** The mongosh result type string (e.g. 'Cursor', 'Document', 'string'). */ + readonly type: string | null; + /** The printable result value โ€” already iterated for cursors. */ + readonly printable: unknown; + /** Execution duration in milliseconds. */ + readonly durationMs: number; + /** Source namespace from the `@mongosh` ShellResult, if available. */ + readonly source?: { + readonly namespace?: { + readonly db: string; + readonly collection: string; + }; + }; +} diff --git a/src/documentdb/scratchpad/workerTypes.ts b/src/documentdb/scratchpad/workerTypes.ts new file mode 100644 index 000000000..af13b0e5f --- /dev/null +++ b/src/documentdb/scratchpad/workerTypes.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * IPC message types for the scratchpad worker thread. + * + * This file is shared between the main thread (ScratchpadEvaluator) and + * the worker thread (scratchpadWorker). It must have zero runtime dependencies โ€” + * only TypeScript types and string literal unions. + * + * Communication uses Node.js `worker_threads` `postMessage()` with the + * structured clone algorithm. Functions cannot be sent โ€” this is why + * Entra ID OIDC tokens must be requested via IPC (tokenRequest/tokenResponse). + */ + +// โ”€โ”€โ”€ Serializable subset of MongoClientOptions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** + * Only the MongoClientOptions fields that can survive structured clone. + * Function-valued options (like OIDC_CALLBACK) are stripped before sending + * and reconstructed on the worker side. + */ +export interface SerializableMongoClientOptions { + readonly serverSelectionTimeoutMS?: number; + readonly tlsAllowInvalidCertificates?: boolean; + readonly appName?: string; + readonly tls?: boolean; +} + +// โ”€โ”€โ”€ Serializable execution result โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** + * The subset of ExecutionResult that can be sent via postMessage. + * BSON types are serialized to EJSON strings by the worker before sending. + */ +export interface SerializableExecutionResult { + readonly type: string | null; + /** EJSON-serialized printable value */ + readonly printable: string; + readonly durationMs: number; + readonly source?: { + readonly namespace?: { + readonly db: string; + readonly collection: string; + }; + }; +} + +// โ”€โ”€โ”€ Main โ†’ Worker messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export type MainToWorkerMessage = + | { + readonly type: 'init'; + readonly requestId: string; + /** Connection string (with embedded credentials for SCRAM, without for Entra ID) */ + readonly connectionString: string; + readonly clientOptions: SerializableMongoClientOptions; + readonly databaseName: string; + readonly authMechanism: 'NativeAuth' | 'MicrosoftEntraID'; + /** Tenant ID for Entra ID clusters */ + readonly tenantId?: string; + // TODO(F11): Wire displayBatchSize end-to-end โ€” currently sent but not read by the worker. + // See future-work.md ยงF11 for the plan to honor documentDB.mongoShell.batchSize. + readonly displayBatchSize: number; + } + | { + readonly type: 'eval'; + readonly requestId: string; + /** JavaScript code to evaluate */ + readonly code: string; + /** Target database name (may differ from init if user switched databases) */ + readonly databaseName: string; + } + | { + readonly type: 'shutdown'; + readonly requestId: string; + } + | { + readonly type: 'tokenResponse'; + readonly requestId: string; + readonly accessToken: string; + } + | { + readonly type: 'tokenError'; + readonly requestId: string; + readonly error: string; + }; + +// โ”€โ”€โ”€ Worker โ†’ Main messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export type WorkerToMainMessage = + | { + readonly type: 'initResult'; + readonly requestId: string; + readonly success: boolean; + readonly error?: string; + } + | { + readonly type: 'evalResult'; + readonly requestId: string; + readonly result: SerializableExecutionResult; + } + | { + readonly type: 'evalError'; + readonly requestId: string; + readonly error: string; + readonly stack?: string; + } + | { + readonly type: 'shutdownComplete'; + readonly requestId: string; + } + | { + readonly type: 'tokenRequest'; + readonly requestId: string; + readonly scopes: readonly string[]; + readonly tenantId?: string; + } + | { + readonly type: 'log'; + readonly level: 'trace' | 'debug' | 'info' | 'warn' | 'error'; + readonly message: string; + }; diff --git a/src/documentdb/utils/getClusterMetadata.ts b/src/documentdb/utils/getClusterMetadata.ts index 36430136c..1e433f7a4 100644 --- a/src/documentdb/utils/getClusterMetadata.ts +++ b/src/documentdb/utils/getClusterMetadata.ts @@ -108,6 +108,17 @@ async function fetchHostInfo(adminDb: Admin, result: ClusterMetadata): Promise): void { for (const [index, host] of hosts.entries()) { const telemetrySuffix = index > 0 ? `_h${index}` : ''; try { diff --git a/src/documentdb/utils/toFilterQuery.test.ts b/src/documentdb/utils/toFilterQuery.test.ts index ca8ff0352..a19caa7ef 100644 --- a/src/documentdb/utils/toFilterQuery.test.ts +++ b/src/documentdb/utils/toFilterQuery.test.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MaxKey, MinKey, UUID } from 'mongodb'; +import { Binary, Decimal128, Int32, Long, ObjectId, Timestamp } from 'bson'; +import { MaxKey, MinKey } from 'mongodb'; import { QueryError } from '../errors/QueryError'; import { toFilterQueryObj } from './toFilterQuery'; @@ -29,175 +30,164 @@ jest.mock('../../extensionVariables', () => ({ }, })); -// Basic query examples -const basicQueries = [ - { input: '{ }', expected: {} }, - { input: '{ "name": "John" }', expected: { name: 'John' } }, - { input: '{ "name": "John", "age": { "$gt": 30 } }', expected: { name: 'John', age: { $gt: 30 } } }, -]; - -// BSON function examples with different variations -const bsonFunctionTestCases = [ - // UUID cases - { - type: 'UUID', - input: '{ "id": UUID("123e4567-e89b-12d3-a456-426614174000") }', - property: 'id', - expectedClass: UUID, - expectedValue: '123e4567-e89b-12d3-a456-426614174000', - }, - { - type: 'UUID with new', - input: '{ "userId": new UUID("550e8400-e29b-41d4-a716-446655440000") }', - property: 'userId', - expectedClass: UUID, - expectedValue: '550e8400-e29b-41d4-a716-446655440000', - }, - { - type: 'UUID with single quotes', - input: '{ "id": UUID(\'123e4567-e89b-12d3-a456-426614174000\') }', - property: 'id', - expectedClass: UUID, - expectedValue: '123e4567-e89b-12d3-a456-426614174000', - }, - // MinKey cases - { - type: 'MinKey', - input: '{ "start": MinKey() }', - property: 'start', - expectedClass: MinKey, - }, - { - type: 'MinKey with new', - input: '{ "min": new MinKey() }', - property: 'min', - expectedClass: MinKey, - }, - // MaxKey cases - { - type: 'MaxKey', - input: '{ "end": MaxKey() }', - property: 'end', - expectedClass: MaxKey, - }, - { - type: 'MaxKey with new', - input: '{ "max": new MaxKey() }', - property: 'max', - expectedClass: MaxKey, - }, - // Date cases - { - type: 'Date', - input: '{ "created": new Date("2023-01-01") }', - property: 'created', - expectedClass: Date, - expectedValue: '2023-01-01T00:00:00.000Z', - }, - { - type: 'Date without new', - input: '{ "updated": Date("2023-12-31T23:59:59.999Z") }', - property: 'updated', - expectedClass: Date, - expectedValue: '2023-12-31T23:59:59.999Z', - }, -]; +describe('toFilterQuery', () => { + describe('basic queries', () => { + test('empty string returns empty object', () => { + expect(toFilterQueryObj('')).toEqual({}); + }); -// Examples of mixed BSON types -const mixedQuery = - '{ "id": UUID("123e4567-e89b-12d3-a456-426614174000"), "start": MinKey(), "end": MaxKey(), "created": new Date("2023-01-01") }'; + test('whitespace-only returns empty object', () => { + expect(toFilterQueryObj(' ')).toEqual({}); + }); -// Complex nested query -const complexQuery = - '{ "range": { "start": MinKey(), "end": MaxKey() }, "timestamp": new Date("2023-01-01"), "ids": [UUID("123e4567-e89b-12d3-a456-426614174000")] }'; + test('empty object returns empty object', () => { + expect(toFilterQueryObj('{ }')).toEqual({}); + }); -// String that contains BSON function syntax but should be treated as plain text -const textWithFunctionSyntax = '{ "userName": "A user with UUID()name and Date() format", "status": "active" }'; + test('simple string filter', () => { + expect(toFilterQueryObj('{ "name": "John" }')).toEqual({ name: 'John' }); + }); -// Error test cases -const errorTestCases = [ - { description: 'invalid JSON', input: '{ invalid json }' }, - { description: 'invalid UUID', input: '{ "id": UUID("invalid-uuid") }' }, - { description: 'invalid Date', input: '{ "date": new Date("invalid-date") }' }, - { description: 'missing parameter', input: '{ "key": UUID() }' }, -]; + test('filter with query operator', () => { + expect(toFilterQueryObj('{ "age": { "$gt": 30 } }')).toEqual({ age: { $gt: 30 } }); + }); -describe('toFilterQuery', () => { - it('converts basic query strings to objects', () => { - basicQueries.forEach((testCase) => { - expect(toFilterQueryObj(testCase.input)).toEqual(testCase.expected); + test('combined filter', () => { + expect(toFilterQueryObj('{ "name": "John", "age": { "$gt": 30 } }')).toEqual({ + name: 'John', + age: { $gt: 30 }, + }); }); }); - describe('BSON function support', () => { - test.each(bsonFunctionTestCases)('converts $type', ({ input, property, expectedClass, expectedValue }) => { - const result = toFilterQueryObj(input); - - expect(result).toHaveProperty(property); - expect(result[property]).toBeInstanceOf(expectedClass); - - if (expectedValue) { - if (result[property] instanceof UUID) { - // eslint-disable-next-line jest/no-conditional-expect - expect(result[property].toString()).toBe(expectedValue); - } else if (result[property] instanceof Date) { - // eslint-disable-next-line jest/no-conditional-expect - expect(result[property].toISOString()).toBe(expectedValue); - } - } + describe('relaxed syntax (new with shell-bson-parser)', () => { + test('unquoted keys', () => { + expect(toFilterQueryObj('{ count: 42 }')).toEqual({ count: 42 }); }); - }); - it('handles mixed BSON types in the same query', () => { - const result = toFilterQueryObj(mixedQuery); + test('single-quoted strings', () => { + expect(toFilterQueryObj("{ name: 'Alice' }")).toEqual({ name: 'Alice' }); + }); + + test('Math.min expression', () => { + const result = toFilterQueryObj('{ rating: Math.min(1.7, 2) }'); + expect(result).toEqual({ rating: 1.7 }); + }); - expect(result.id).toBeInstanceOf(UUID); - expect(result.start).toBeInstanceOf(MinKey); - expect(result.end).toBeInstanceOf(MaxKey); - expect(result.created).toBeInstanceOf(Date); + test('unquoted keys with nested operators', () => { + expect(toFilterQueryObj('{ age: { $gt: 25 } }')).toEqual({ age: { $gt: 25 } }); + }); - expect((result.id as UUID).toString()).toBe('123e4567-e89b-12d3-a456-426614174000'); - expect((result.created as Date).toISOString()).toBe('2023-01-01T00:00:00.000Z'); + test('mixed quoted and unquoted keys', () => { + expect(toFilterQueryObj('{ name: "Alice", "age": 30 }')).toEqual({ name: 'Alice', age: 30 }); + }); }); - it('handles complex nested queries with multiple BSON types', () => { - const result = toFilterQueryObj(complexQuery); + describe('BSON constructor support', () => { + test('UUID constructor', () => { + const result = toFilterQueryObj('{ id: UUID("123e4567-e89b-12d3-a456-426614174000") }'); + expect(result).toHaveProperty('id'); + // shell-bson-parser returns Binary subtype 4 for UUID + expect(result.id).toBeInstanceOf(Binary); + expect((result.id as Binary).sub_type).toBe(Binary.SUBTYPE_UUID); + }); - expect(result.range.start).toBeInstanceOf(MinKey); - expect(result.range.end).toBeInstanceOf(MaxKey); - expect(result.timestamp).toBeInstanceOf(Date); - expect(result.ids[0]).toBeInstanceOf(UUID); - }); + test('UUID with new keyword', () => { + const result = toFilterQueryObj('{ userId: new UUID("550e8400-e29b-41d4-a716-446655440000") }'); + expect(result).toHaveProperty('userId'); + expect(result.userId).toBeInstanceOf(Binary); + expect((result.userId as Binary).sub_type).toBe(Binary.SUBTYPE_UUID); + }); + + test('MinKey constructor', () => { + const result = toFilterQueryObj('{ start: MinKey() }'); + expect(result).toHaveProperty('start'); + expect(result.start).toBeInstanceOf(MinKey); + }); - it('does not process BSON function calls within string values', () => { - const result = toFilterQueryObj(textWithFunctionSyntax); - expect(result).toEqual({ - userName: 'A user with UUID()name and Date() format', - status: 'active', + test('MaxKey constructor', () => { + const result = toFilterQueryObj('{ end: MaxKey() }'); + expect(result).toHaveProperty('end'); + expect(result.end).toBeInstanceOf(MaxKey); + }); + + test('Date constructor', () => { + const result = toFilterQueryObj('{ created: new Date("2023-01-01") }'); + expect(result).toHaveProperty('created'); + expect(result.created).toBeInstanceOf(Date); + expect((result.created as Date).toISOString()).toBe('2023-01-01T00:00:00.000Z'); + }); + + test('ObjectId constructor', () => { + const result = toFilterQueryObj('{ _id: ObjectId("507f1f77bcf86cd799439011") }'); + expect(result).toHaveProperty('_id'); + expect(result._id).toBeInstanceOf(ObjectId); + }); + + test('ISODate constructor', () => { + const result = toFilterQueryObj('{ ts: ISODate("2024-01-01") }'); + expect(result).toHaveProperty('ts'); + expect(result.ts).toBeInstanceOf(Date); + }); + + test('Decimal128 constructor', () => { + const result = toFilterQueryObj('{ val: Decimal128("1.23") }'); + expect(result).toHaveProperty('val'); + expect(result.val).toBeInstanceOf(Decimal128); + }); + + test('NumberInt constructor', () => { + const result = toFilterQueryObj('{ n: NumberInt(42) }'); + expect(result).toHaveProperty('n'); + expect(result.n).toBeInstanceOf(Int32); + }); + + test('NumberLong constructor', () => { + const result = toFilterQueryObj('{ n: NumberLong(42) }'); + expect(result).toHaveProperty('n'); + expect(result.n).toBeInstanceOf(Long); + }); + + test('Timestamp constructor', () => { + const result = toFilterQueryObj('{ ts: Timestamp(1, 1) }'); + expect(result).toHaveProperty('ts'); + expect(result.ts).toBeInstanceOf(Timestamp); }); }); - describe('error handling', () => { - test.each(errorTestCases)('throws QueryError for $description', ({ input }) => { - expect(() => toFilterQueryObj(input)).toThrow(QueryError); + describe('mixed BSON types', () => { + test('multiple BSON constructors in one query', () => { + const result = toFilterQueryObj( + '{ id: UUID("123e4567-e89b-12d3-a456-426614174000"), start: MinKey(), end: MaxKey(), created: new Date("2023-01-01") }', + ); + + expect(result.id).toBeInstanceOf(Binary); + expect((result.id as Binary).sub_type).toBe(Binary.SUBTYPE_UUID); + expect(result.start).toBeInstanceOf(MinKey); + expect(result.end).toBeInstanceOf(MaxKey); + expect(result.created).toBeInstanceOf(Date); }); - it('throws QueryError with INVALID_FILTER code for invalid JSON', () => { - let thrownError: QueryError | undefined; - try { - toFilterQueryObj('{ invalid json }'); - } catch (error) { - thrownError = error as QueryError; - } - expect(thrownError).toBeDefined(); - expect(thrownError?.name).toBe('QueryError'); - expect(thrownError?.code).toBe('INVALID_FILTER'); + test('nested BSON constructors', () => { + const result = toFilterQueryObj( + '{ range: { start: MinKey(), end: MaxKey() }, timestamp: new Date("2023-01-01") }', + ); + + expect(result.range.start).toBeInstanceOf(MinKey); + expect(result.range.end).toBeInstanceOf(MaxKey); + expect(result.timestamp).toBeInstanceOf(Date); + }); + }); + + describe('error handling', () => { + test('throws QueryError for invalid syntax', () => { + expect(() => toFilterQueryObj('{ invalid json }')).toThrow(QueryError); }); - it('throws QueryError with INVALID_FILTER code for invalid UUID', () => { + test('throws QueryError with INVALID_FILTER code', () => { let thrownError: QueryError | undefined; try { - toFilterQueryObj('{ "id": UUID("invalid-uuid") }'); + toFilterQueryObj('not valid at all'); } catch (error) { thrownError = error as QueryError; } @@ -206,10 +196,10 @@ describe('toFilterQuery', () => { expect(thrownError?.code).toBe('INVALID_FILTER'); }); - it('includes original error message in QueryError message', () => { + test('error message contains "Invalid filter syntax"', () => { let thrownError: QueryError | undefined; try { - toFilterQueryObj('{ invalid json }'); + toFilterQueryObj('not valid'); } catch (error) { thrownError = error as QueryError; } @@ -217,16 +207,15 @@ describe('toFilterQuery', () => { expect(thrownError?.message).toContain('Invalid filter syntax'); }); - it('includes helpful JSON example in error message', () => { + test('error message contains helpful example', () => { let thrownError: QueryError | undefined; try { - toFilterQueryObj('{ invalid json }'); + toFilterQueryObj('not valid'); } catch (error) { thrownError = error as QueryError; } expect(thrownError).toBeDefined(); - expect(thrownError?.message).toContain('Please use valid JSON'); - expect(thrownError?.message).toContain('"name": "value"'); + expect(thrownError?.message).toContain('name: "value"'); }); }); }); diff --git a/src/documentdb/utils/toFilterQuery.ts b/src/documentdb/utils/toFilterQuery.ts index 807f18858..1cbb67a15 100644 --- a/src/documentdb/utils/toFilterQuery.ts +++ b/src/documentdb/utils/toFilterQuery.ts @@ -3,227 +3,38 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EJSON } from 'bson'; -import { UUID, type Document, type Filter } from 'mongodb'; +import { ParseMode, parse as parseShellBSON } from '@mongodb-js/shell-bson-parser'; +import { type Document, type Filter } from 'mongodb'; import * as vscode from 'vscode'; import { QueryError } from '../errors/QueryError'; +/** + * Parses a user-provided filter query string into a DocumentDB filter object. + * + * Uses `@mongodb-js/shell-bson-parser` in Loose mode, which supports: + * - Unquoted keys: `{ name: 1 }` + * - Single-quoted strings: `{ name: 'Alice' }` + * - BSON constructors: `ObjectId("...")`, `UUID("...")`, `ISODate("...")`, etc. + * - JS expressions: `Math.min(1.7, 2)`, `Date.now()`, arithmetic + * - MongoDB Extended JSON: `{ "$oid": "..." }` + * + * Replaces the previous hand-rolled regex-based converter + EJSON.parse pipeline. + */ export function toFilterQueryObj(queryString: string): Filter { try { - // Convert pseudo-JavaScript style BSON constructor calls into Extended JSON that EJSON can parse. - // Example: { "id": UUID("...") } -> { "id": {"$uuid":"..."} } - const extendedJsonQuery = convertToExtendedJson(queryString); - // EJSON.parse will turn Extended JSON into native BSON/JS types (UUID, Date, etc.). - return EJSON.parse(extendedJsonQuery) as Filter; - } catch (error) { if (queryString.trim().length === 0) { return {} as Filter; } - + return parseShellBSON(queryString, { mode: ParseMode.Loose }) as Filter; + } catch (error) { const cause = error instanceof Error ? error : new Error(String(error)); throw new QueryError( 'INVALID_FILTER', vscode.l10n.t( - 'Invalid filter syntax: {0}. Please use valid JSON, for example: { "name": "value" }', + 'Invalid filter syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { name: "value" }', cause.message, ), cause, ); } } - -/** - * Walks the raw query text and rewrites BSON-like constructor calls (UUID, MinKey, MaxKey, Date) - * into MongoDB Extended JSON fragments while deliberately skipping anything that appears inside - * string literals (so user text containing e.g. "UUID(" is not transformed). - * - * This is intentionally lightweight and avoids a full JS / JSON parser to keep latency low inside - * the query input UX. Future improvements may replace this with a tokenizer / parser for richer - * validation and diagnostics. - */ -function convertToExtendedJson(query: string): string { - // Phase 1: Precompute which character positions are inside a (single or double quoted) string. - // This lets the replacement pass stay simple and branchless for nonโ€‘string regions. - const isInString = markStringLiterals(query); - - // Phase 2: Scan + rewrite BSON-like calls only when not inside a string literal. - let result = ''; - let i = 0; - while (i < query.length) { - if (isInString[i]) { - // Inside a user string literal โ€“ copy verbatim. - result += query[i]; - i += 1; - continue; - } - - const remaining = query.slice(i); - - // UUID(...) - const uuidMatch = matchUUID(remaining); - if (uuidMatch) { - const { raw, uuidString } = uuidMatch; - try { - // Validate early so we fail fast instead of producing malformed Extended JSON. - // (Instantiation is enough to validate format.) - new UUID(uuidString); - } catch { - throw new Error(`Invalid UUID: ${uuidString}`); - } - result += `{"$uuid":"${uuidString}"}`; - i += raw.length; - continue; - } - - // MinKey() - const minKeyMatch = matchMinKey(remaining); - if (minKeyMatch) { - result += '{"$minKey":1}'; - i += minKeyMatch.raw.length; - continue; - } - - // MaxKey() - const maxKeyMatch = matchMaxKey(remaining); - if (maxKeyMatch) { - result += '{"$maxKey":1}'; - i += maxKeyMatch.raw.length; - continue; - } - - // Date("...") - const dateMatch = matchDate(remaining); - if (dateMatch) { - const { raw, dateString } = dateMatch; - const date = new Date(dateString); - if (Number.isNaN(date.getTime())) { - throw new Error(`Invalid date: ${dateString}`); - } - result += `{"$date":"${dateString}"}`; - i += raw.length; - continue; - } - - // Fallback: copy one character. - result += query[i]; - i += 1; - } - - return result; -} - -/** - * markStringLiterals - * - * Lightweight pass to flag which character indices are inside a quoted string. - * - * Supported: - * - Single quotes '...' - * - Double quotes "..." - * - Escapes inside those strings via backslash (\" or \') - * - * Not a full JSON validator: - * - Does not detect malformed / unclosed strings (those will just mark to end) - * - Does not handle template literals (not valid JSON anyway) - * - * Rationale: - * This is intentionally simple and fast. It exists to prevent accidental rewriting of text - * inside user-provided string values (e.g. "note: call UUID('x') later") while we still accept - * a relaxed JSON-ish syntax for convenience. If the query authoring experience is expanded - * (linting, richer autocomplete, tolerant recovery) we can replace this with a proper tokenizer. - */ -function markStringLiterals(input: string): boolean[] { - const isInString: boolean[] = new Array(input.length).fill(false) as boolean[]; - let inString = false; - let currentQuote: '"' | "'" | null = null; - let escapeNext = false; - - for (let i = 0; i < input.length; i++) { - const ch = input[i]; - - if (escapeNext) { - // Current char is escaped; treat it as plain content inside the string. - isInString[i] = inString; - escapeNext = false; - continue; - } - - if (inString) { - // Inside a string: mark and handle escapes / termination. - isInString[i] = true; - if (ch === '\\') { - escapeNext = true; - } else if (ch === currentQuote) { - inString = false; - currentQuote = null; - } - continue; - } - - // Not currently in a string โ€“ only a quote can start one. - if (ch === '"' || ch === "'") { - inString = true; - currentQuote = ch as '"' | "'"; - isInString[i] = true; - continue; - } - - // Outside of strings. - isInString[i] = false; - } - - return isInString; -} - -// --- Regex constants for BSON-like constructor calls --- - -/** - * Matches UUID constructor calls, e.g. UUID("...") or new UUID('...'), case-insensitive. - * Captures the quoted UUID string. - * Pattern details: - * - Optional "new" prefix with whitespace: (?:new\s+)? - * - "uuid" keyword, case-insensitive - * - Optional whitespace before and inside parentheses - * - Quoted string (single or double quotes) as argument, captured in group 1 - */ -const UUID_REGEX = /^(?:new\s+)?uuid\s*\(\s*["']([^"']+)["']\s*\)/i; - -/** - * Matches MinKey constructor calls, e.g. MinKey() or new MinKey(), case-insensitive. - * No arguments. - */ -const MIN_KEY_REGEX = /^(?:new\s+)?minkey\s*\(\s*\)/i; - -/** - * Matches MaxKey constructor calls, e.g. MaxKey() or new MaxKey(), case-insensitive. - * No arguments. - */ -const MAX_KEY_REGEX = /^(?:new\s+)?maxkey\s*\(\s*\)/i; - -/** - * Matches Date constructor calls, e.g. Date("...") or new Date('...'), case-insensitive. - * Captures the quoted date string. - * Pattern details: - * - Optional "new" prefix with whitespace: (?:new\s+)? - * - "date" keyword, case-insensitive - * - Optional whitespace before and inside parentheses - * - Quoted string (single or double quotes) as argument, captured in group 1 - */ -const DATE_REGEX = /^(?:new\s+)?date\s*\(\s*["']([^"']+)["']\s*\)/i; - -function matchUUID(src: string): { raw: string; uuidString: string } | undefined { - const m = UUID_REGEX.exec(src); - return m ? { raw: m[0], uuidString: m[1] } : undefined; -} -function matchMinKey(src: string): { raw: string } | undefined { - const m = MIN_KEY_REGEX.exec(src); - return m ? { raw: m[0] } : undefined; -} -function matchMaxKey(src: string): { raw: string } | undefined { - const m = MAX_KEY_REGEX.exec(src); - return m ? { raw: m[0] } : undefined; -} -function matchDate(src: string): { raw: string; dateString: string } | undefined { - const m = DATE_REGEX.exec(src); - return m ? { raw: m[0], dateString: m[1] } : undefined; -} diff --git a/src/extension.ts b/src/extension.ts index dc0209dfe..25b58ce35 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,6 +19,7 @@ import { import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ClustersExtension } from './documentdb/ClustersExtension'; +import { SchemaStore } from './documentdb/SchemaStore'; import { ext } from './extensionVariables'; import { globalUriHandler } from './vscodeUriHandler'; // Import the DocumentDB Extension API interfaces @@ -58,6 +59,7 @@ export async function activateInternal( const clustersSupport: ClustersExtension = new ClustersExtension(); context.subscriptions.push(clustersSupport); // to be disposed when extension is deactivated. + context.subscriptions.push(SchemaStore.getInstance()); await clustersSupport.activateClustersSupport(); context.subscriptions.push( diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index b4becfa62..0fdda593b 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -7,7 +7,6 @@ import { type IAzExtLogOutputChannel, type TreeElementStateManager } from '@micr import { type AzureResourcesExtensionApiWithActivity } from '@microsoft/vscode-azext-utils/activity'; import type * as vscode from 'vscode'; import { type DatabasesFileSystem } from './DatabasesFileSystem'; -import { type MongoDBLanguageClient } from './documentdb/scrapbook/languageClient'; import { type VCoreBranchDataProvider } from './tree/azure-resources-view/documentdb/VCoreBranchDataProvider'; import { type RUBranchDataProvider } from './tree/azure-resources-view/mongo-ru/RUBranchDataProvider'; import { type ClustersWorkspaceBranchDataProvider } from './tree/azure-workspace-view/ClustersWorkbenchBranchDataProvider'; @@ -28,7 +27,6 @@ export namespace ext { export let secretStorage: vscode.SecretStorage; export const prefix: string = 'documentDB'; export let fileSystem: DatabasesFileSystem; - export let mongoLanguageClient: MongoDBLanguageClient; // TODO: TN improve this: This is a temporary solution to get going. export let copiedCollectionNode: CollectionItem | undefined; diff --git a/src/utils/json/data-api/autocomplete/future-work.md b/src/utils/json/data-api/autocomplete/future-work.md new file mode 100644 index 000000000..660113c7d --- /dev/null +++ b/src/utils/json/data-api/autocomplete/future-work.md @@ -0,0 +1,161 @@ +# Autocomplete โ€” Future Work + +Outstanding TODOs flagged in code during the schema transformer implementation (PR #506). +These must be resolved before the completion providers ship to users. + +--- + +## ~~1. `SPECIAL_CHARS_PATTERN` is incomplete + `insertText` quoting doesn't escape~~ โœ… RESOLVED + +**Resolved in:** PR #506 (commit addressing copilot review comment) + +Replaced `SPECIAL_CHARS_PATTERN` with `JS_IDENTIFIER_PATTERN` โ€” a proper identifier validity check. +Added `\` โ†’ `\\` and `"` โ†’ `\"` escaping when quoting `insertText`. +Tests cover dashes, brackets, digits, embedded quotes, and backslashes. + +--- + +## 2. `referenceText` is invalid MQL for special field names + +**Severity:** Medium โ€” will generate broken aggregation expressions +**File:** `toFieldCompletionItems.ts` โ€” `referenceText` construction +**When to fix:** Before the aggregation completion provider is wired up + +### Problem + +`referenceText` is always `$${entry.path}` (e.g., `$address.city`). In MQL, the `$field.path` syntax only works when every segment is a valid identifier without dots, spaces, or `$`. For field names like `order-items`, `a.b`, or `my field`, the `$` prefix syntax produces invalid references. + +### Examples + +| Field name | Current `referenceText` | Valid? | Correct MQL | +| ------------------- | ----------------------- | -------------- | ------------------------------------ | +| `age` | `$age` | โœ… | `$age` | +| `address.city` | `$address.city` | โœ… (nested) | `$address.city` | +| `order-items` | `$order-items` | โŒ | `{ $getField: "order-items" }` | +| `a.b` (literal dot) | `$a.b` | โŒ (ambiguous) | `{ $getField: { $literal: "a.b" } }` | +| `my field` | `$my field` | โŒ | `{ $getField: "my field" }` | + +### Proposed approaches + +**Option A โ€” Make `referenceText` optional:** Return `undefined` for fields that can't use `$`-prefix syntax. The completion provider would omit the reference suggestion for those fields. + +**Option B โ€” Use `$getField` for special names:** + +```typescript +referenceText: needsQuoting + ? `{ $getField: "${escaped}" }` + : `$${entry.path}`, +``` + +**Option C โ€” Provide both forms:** Add a `referenceTextRaw` (always `$path`) and `referenceTextSafe` (uses `$getField` when needed). Let the completion provider choose based on context. + +**Recommendation:** Option B is pragmatic. Option C is more flexible if we later need to support both forms in different contexts (e.g., `$match` vs `$project`). + +--- + +## 3. `FieldEntry.path` dot-concatenation is ambiguous for literal dots + +**Severity:** Low (rare in practice) โ€” fields with literal dots were prohibited before MongoDB API 3.6 +**File:** `getKnownFields.ts` โ€” path concatenation at `path: \`${path}.${childName}\``**When to fix:** When we encounter real-world schemas with literal dots, or during the next`FieldEntry` interface revision + +### Problem + +Paths are built by concatenating segments with `.` as separator. A root-level field named `"a.b"` produces `path: "a.b"`, which is indistinguishable from a nested field `{ a: { b: ... } }`. + +This ambiguity flows downstream to all consumers: `toTypeScriptDefinition`, `toFieldCompletionItems`, `generateDescriptions`, and any future completion provider. + +### Examples + +| Document shape | Resulting `path` | Ambiguous? | +| --------------------- | ---------------- | ----------------------------- | +| `{ a: { b: 1 } }` | `"a.b"` | โ€” | +| `{ "a.b": 1 }` | `"a.b"` | โœ… Same as above | +| `{ x: { "y.z": 1 } }` | `"x.y.z"` | โœ… Looks like 3-level nesting | + +### Proposed fix + +Change `FieldEntry.path` from `string` to `string[]` (segment array): + +```typescript +// Before +interface FieldEntry { + path: string; // "address.city" + ... +} + +// After +interface FieldEntry { + path: string[]; // ["address", "city"] + ... +} +``` + +Each consumer then formats the path for its own context: + +- **TypeScript definitions:** Already use schema `properties` keys directly (no change needed there) +- **Completion items:** `entry.path.join('.')` for display, bracket notation for special segments +- **Aggregation references:** `$` + segments joined with `.`, or `$getField` chains for special segments + +### Impact + +This is a **breaking change** to the `FieldEntry` interface. Affected consumers: + +- `toFieldCompletionItems.ts` +- `toTypeScriptDefinition.ts` (indirect โ€” uses schema, not FieldEntry paths) +- `generateDescriptions.ts` (uses schema, not FieldEntry paths) +- `collectionViewRouter.ts` (imports `FieldEntry` type) +- `ClusterSession.ts` (imports `FieldEntry` type) +- `generateMongoFindJsonSchema.ts` (imports `FieldEntry` type) +- `SchemaAnalyzer.ts` (returns `FieldEntry[]` via `getKnownFields`) + +**Recommendation:** Defer until the completion provider is built. The ambiguity only matters for fields with literal dots, which are uncommon. When fixing, do it as a single atomic change across all consumers. + +--- + +## 4. TypeScript definition output references undeclared BSON type names + +**Severity:** Low โ€” the TS definition is for display/hover only, not compiled or type-checked +**File:** `toTypeScriptDefinition.ts` โ€” `bsonToTypeScriptMap` +**When to fix:** Before the TS definition is used in a context where type correctness matters (e.g., Monaco intellisense with an actual TS language service) + +### Problem + +The BSON-to-TypeScript type mapping emits non-built-in type names such as `ObjectId`, `Binary`, `Timestamp`, `MinKey`, `MaxKey`, `Code`, `DBRef`, and `UUID`. These are MongoDB API BSON driver types, but the generated definition string doesn't include `import` statements or `declare` stubs for them. + +If the output is ever fed to a TypeScript compiler or language service (e.g., Monaco with full TS checking), it will report "Cannot find name 'ObjectId'" etc. + +### Current state + +The generated output is used for documentation/hover display only โ€” it's rendered as syntax-highlighted text, not compiled. So this is purely cosmetic today. + +### Proposed fix (when needed) + +**Option A โ€” Emit `import type`:** + +```typescript +import type { ObjectId, Binary, Timestamp, MinKey, MaxKey, Code, DBRef, UUID } from 'mongodb'; +``` + +Only include types that actually appear in the schema. + +**Option B โ€” Emit `declare type` stubs:** + +```typescript +declare type ObjectId = { toString(): string }; +declare type Binary = { length(): number }; +// ... etc. +``` + +Lightweight, no dependency on the `mongodb` package. + +**Option C โ€” Map everything to primitive types:** + +```typescript +ObjectId โ†’ string // (its string representation) +Binary โ†’ Uint8Array +Timestamp โ†’ { t: number; i: number } +``` + +Loses semantic precision but avoids the undeclared-type problem entirely. + +**Recommendation:** Option A is the most correct approach. Collect the set of non-built-in types actually used in the schema, then prepend a single `import type` line. Defer until the output is consumed by a real TS language service. diff --git a/src/utils/json/data-api/autocomplete/generateDescriptions.test.ts b/src/utils/json/data-api/autocomplete/generateDescriptions.test.ts new file mode 100644 index 000000000..32a103431 --- /dev/null +++ b/src/utils/json/data-api/autocomplete/generateDescriptions.test.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type JSONSchema } from '@vscode-documentdb/schema-analyzer'; +import { generateDescriptions } from './generateDescriptions'; + +describe('generateDescriptions', () => { + it('adds descriptions with type and percentage for simple document', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + name: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + }, + ], + }, + }, + }; + + generateDescriptions(schema); + + const nameSchema = schema.properties?.name as JSONSchema; + expect(nameSchema.description).toBe('String ยท 100%'); + }); + + it('includes min/max stats for numeric fields', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + age: { + 'x-occurrence': 95, + anyOf: [ + { + type: 'number', + 'x-bsonType': 'int32', + 'x-typeOccurrence': 95, + 'x-minValue': 18, + 'x-maxValue': 95, + }, + ], + }, + }, + }; + + generateDescriptions(schema); + + const ageSchema = schema.properties?.age as JSONSchema; + expect(ageSchema.description).toBe('Int32 ยท 95% ยท range: 18โ€“95'); + }); + + it('includes length stats for string fields', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + name: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + 'x-minLength': 3, + 'x-maxLength': 50, + }, + ], + }, + }, + }; + + generateDescriptions(schema); + + const nameSchema = schema.properties?.name as JSONSchema; + expect(nameSchema.description).toBe('String ยท 100% ยท length: 3โ€“50'); + }); + + it('includes date range stats for date fields', () => { + const minDate = new Date('2020-01-01T00:00:00.000Z').getTime(); + const maxDate = new Date('2024-12-31T00:00:00.000Z').getTime(); + + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + createdAt: { + 'x-occurrence': 80, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'date', + 'x-typeOccurrence': 80, + 'x-minDate': minDate, + 'x-maxDate': maxDate, + }, + ], + }, + }, + }; + + generateDescriptions(schema); + + const createdAtSchema = schema.properties?.createdAt as JSONSchema; + expect(createdAtSchema.description).toBe('Date ยท 80% ยท range: 2020-01-01 โ€“ 2024-12-31'); + }); + + it('includes true/false counts for boolean fields', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + active: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'boolean', + 'x-bsonType': 'boolean', + 'x-typeOccurrence': 100, + 'x-trueCount': 80, + 'x-falseCount': 20, + }, + ], + }, + }, + }; + + generateDescriptions(schema); + + const activeSchema = schema.properties?.active as JSONSchema; + expect(activeSchema.description).toBe('Boolean ยท 100% ยท true: 80, false: 20'); + }); + + it('handles nested object fields (descriptions at nested level)', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + address: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'object', + 'x-bsonType': 'object', + 'x-typeOccurrence': 100, + 'x-documentsInspected': 100, + properties: { + city: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + 'x-minLength': 2, + 'x-maxLength': 30, + }, + ], + }, + }, + }, + ], + }, + }, + }; + + generateDescriptions(schema); + + // The parent (address) should also get a description + const addressSchema = schema.properties?.address as JSONSchema; + expect(addressSchema.description).toBe('Object ยท 100%'); + + // The nested city should get its own description + const addressTypeEntry = (addressSchema.anyOf as JSONSchema[])[0]; + const citySchema = addressTypeEntry.properties?.city as JSONSchema; + expect(citySchema.description).toBe('String ยท 100% ยท length: 2โ€“30'); + }); + + it('handles polymorphic fields (shows multiple types)', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + value: { + 'x-occurrence': 95, + anyOf: [ + { + type: 'number', + 'x-bsonType': 'int32', + 'x-typeOccurrence': 60, + 'x-minValue': 1, + 'x-maxValue': 100, + }, + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 35, + }, + ], + }, + }, + }; + + generateDescriptions(schema); + + const valueSchema = schema.properties?.value as JSONSchema; + // Dominant type first, then secondary + expect(valueSchema.description).toBe('Int32 | String ยท 95% ยท range: 1โ€“100'); + }); +}); diff --git a/src/utils/json/data-api/autocomplete/generateDescriptions.ts b/src/utils/json/data-api/autocomplete/generateDescriptions.ts new file mode 100644 index 000000000..2f4f28867 --- /dev/null +++ b/src/utils/json/data-api/autocomplete/generateDescriptions.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BSONTypes, type JSONSchema } from '@vscode-documentdb/schema-analyzer'; +import Denque from 'denque'; + +/** + * Work item for BFS traversal of the schema tree. + */ +interface WorkItem { + schemaNode: JSONSchema; + parentDocumentsInspected: number; +} + +/** + * Post-processor that mutates the schema in-place, adding human-readable + * `description` strings to each property node. Descriptions include: + * - Dominant type name(s) + * - Occurrence percentage (based on `x-occurrence / parentDocumentsInspected`) + * - Type-specific stats (length, range, true/false counts, etc.) + * + * Uses BFS to traverse all property levels. + */ +export function generateDescriptions(schema: JSONSchema): void { + const rootDocumentsInspected = (schema['x-documentsInspected'] as number) ?? 0; + + const queue = new Denque(); + + // Seed the queue with root-level properties + if (schema.properties) { + for (const propName of Object.keys(schema.properties)) { + const propSchema = schema.properties[propName] as JSONSchema; + if (typeof propSchema === 'boolean') continue; + + queue.push({ + schemaNode: propSchema, + parentDocumentsInspected: rootDocumentsInspected, + }); + } + } + + while (queue.length > 0) { + const item = queue.shift(); + if (!item) continue; + + const { schemaNode, parentDocumentsInspected } = item; + + // Collect type display names from anyOf entries + const typeNames = collectTypeDisplayNames(schemaNode); + + // Build description parts + const parts: string[] = []; + + // Part 1: Type info + if (typeNames.length > 0) { + parts.push(typeNames.join(' | ')); + } + + // Part 2: Occurrence percentage + if (parentDocumentsInspected > 0) { + const occurrence = (schemaNode['x-occurrence'] as number) ?? 0; + const percentage = ((occurrence / parentDocumentsInspected) * 100).toFixed(0); + parts.push(`${percentage}%`); + } + + // Part 3: Stats from the dominant type entry + const dominantEntry = getDominantTypeEntry(schemaNode); + if (dominantEntry) { + const statString = getStatString(dominantEntry); + if (statString) { + parts.push(statString); + } + + // If the dominant entry is an object with properties, enqueue children + if (dominantEntry.type === 'object' && dominantEntry.properties) { + const objectDocumentsInspected = (dominantEntry['x-documentsInspected'] as number) ?? 0; + for (const childName of Object.keys(dominantEntry.properties)) { + const childSchema = dominantEntry.properties[childName] as JSONSchema; + if (typeof childSchema === 'boolean') continue; + + queue.push({ + schemaNode: childSchema, + parentDocumentsInspected: objectDocumentsInspected, + }); + } + } + } + + // Set the description + if (parts.length > 0) { + schemaNode.description = parts.join(' ยท '); + } + } +} + +/** + * Collects display names for all types in a schema node's `anyOf` entries. + * Returns them ordered by descending `x-typeOccurrence`. + */ +function collectTypeDisplayNames(schemaNode: JSONSchema): string[] { + if (!schemaNode.anyOf || schemaNode.anyOf.length === 0) { + return []; + } + + const entries: Array<{ name: string; occurrence: number }> = []; + for (const entry of schemaNode.anyOf) { + if (typeof entry === 'boolean') continue; + const bsonType = (entry['x-bsonType'] as string) ?? ''; + const occurrence = (entry['x-typeOccurrence'] as number) ?? 0; + const name = bsonType + ? BSONTypes.toDisplayString(bsonType as BSONTypes) + : ((entry.type as string) ?? 'Unknown'); + entries.push({ name, occurrence }); + } + + // Sort by occurrence descending so dominant type comes first + entries.sort((a, b) => b.occurrence - a.occurrence); + return entries.map((e) => e.name); +} + +/** + * Returns the anyOf entry with the highest `x-typeOccurrence`. + */ +function getDominantTypeEntry(schemaNode: JSONSchema): JSONSchema | null { + if (!schemaNode.anyOf || schemaNode.anyOf.length === 0) { + return null; + } + + let maxOccurrence = -1; + let dominant: JSONSchema | null = null; + + for (const entry of schemaNode.anyOf) { + if (typeof entry === 'boolean') continue; + const occurrence = (entry['x-typeOccurrence'] as number) ?? 0; + if (occurrence > maxOccurrence) { + maxOccurrence = occurrence; + dominant = entry; + } + } + + return dominant; +} + +/** + * Returns a type-specific stats string for the given type entry, or undefined if + * no relevant stats are available. + */ +function getStatString(typeEntry: JSONSchema): string | undefined { + const bsonType = (typeEntry['x-bsonType'] as string) ?? ''; + + switch (bsonType) { + case 'string': + case 'binary': { + const minLen = typeEntry['x-minLength'] as number | undefined; + const maxLen = typeEntry['x-maxLength'] as number | undefined; + if (minLen !== undefined && maxLen !== undefined) { + return `length: ${String(minLen)}โ€“${String(maxLen)}`; + } + return undefined; + } + + case 'int32': + case 'double': + case 'long': + case 'decimal128': + case 'number': { + const minVal = typeEntry['x-minValue'] as number | undefined; + const maxVal = typeEntry['x-maxValue'] as number | undefined; + if (minVal !== undefined && maxVal !== undefined) { + return `range: ${String(minVal)}โ€“${String(maxVal)}`; + } + return undefined; + } + + case 'date': { + const minDate = typeEntry['x-minDate'] as number | undefined; + const maxDate = typeEntry['x-maxDate'] as number | undefined; + if (minDate !== undefined && maxDate !== undefined) { + const minISO = new Date(minDate).toISOString().split('T')[0]; + const maxISO = new Date(maxDate).toISOString().split('T')[0]; + return `range: ${minISO} โ€“ ${maxISO}`; + } + return undefined; + } + + case 'boolean': { + const trueCount = typeEntry['x-trueCount'] as number | undefined; + const falseCount = typeEntry['x-falseCount'] as number | undefined; + if (trueCount !== undefined && falseCount !== undefined) { + return `true: ${String(trueCount)}, false: ${String(falseCount)}`; + } + return undefined; + } + + case 'array': { + const minItems = typeEntry['x-minItems'] as number | undefined; + const maxItems = typeEntry['x-maxItems'] as number | undefined; + if (minItems !== undefined && maxItems !== undefined) { + return `items: ${String(minItems)}โ€“${String(maxItems)}`; + } + return undefined; + } + + case 'object': { + const minProps = typeEntry['x-minProperties'] as number | undefined; + const maxProps = typeEntry['x-maxProperties'] as number | undefined; + if (minProps !== undefined && maxProps !== undefined) { + return `properties: ${String(minProps)}โ€“${String(maxProps)}`; + } + return undefined; + } + + default: + return undefined; + } +} diff --git a/src/utils/json/data-api/autocomplete/getKnownFields.test.ts b/src/utils/json/data-api/autocomplete/getKnownFields.test.ts new file mode 100644 index 000000000..d0680e2f3 --- /dev/null +++ b/src/utils/json/data-api/autocomplete/getKnownFields.test.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type FieldEntry, getKnownFields, SchemaAnalyzer } from '@vscode-documentdb/schema-analyzer'; +import { ObjectId } from 'bson'; + +describe('getKnownFields', () => { + it('returns bsonType for primitive fields', () => { + const analyzer = SchemaAnalyzer.fromDocument({ + _id: new ObjectId(), + name: 'Alice', + age: 42, + score: 3.14, + active: true, + }); + const fields = getKnownFields(analyzer.getSchema()); + + const nameField = fields.find((f: FieldEntry) => f.path === 'name'); + expect(nameField?.type).toBe('string'); + expect(nameField?.bsonType).toBe('string'); + + const ageField = fields.find((f: FieldEntry) => f.path === 'age'); + expect(ageField?.type).toBe('number'); + // bsonType could be 'double' or 'int32' depending on JS runtime + expect(['double', 'int32']).toContain(ageField?.bsonType); + + const activeField = fields.find((f: FieldEntry) => f.path === 'active'); + expect(activeField?.type).toBe('boolean'); + expect(activeField?.bsonType).toBe('boolean'); + }); + + it('returns _id first and sorts alphabetically', () => { + const analyzer = SchemaAnalyzer.fromDocument({ + _id: new ObjectId(), + zebra: 1, + apple: 2, + mango: 3, + }); + const fields = getKnownFields(analyzer.getSchema()); + const paths = fields.map((f: FieldEntry) => f.path); + + expect(paths[0]).toBe('_id'); + // Remaining should be alphabetical + expect(paths.slice(1)).toEqual(['apple', 'mango', 'zebra']); + }); + + it('detects optional fields', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument({ _id: new ObjectId(), name: 'Alice', age: 30 }); + analyzer.addDocument({ _id: new ObjectId(), name: 'Bob' }); // no 'age' + + const fields = getKnownFields(analyzer.getSchema()); + + const nameField = fields.find((f: FieldEntry) => f.path === 'name'); + expect(nameField?.isSparse).toBeUndefined(); // present in all docs + + const ageField = fields.find((f: FieldEntry) => f.path === 'age'); + expect(ageField?.isSparse).toBe(true); // missing in doc2 + }); + + it('returns bsonTypes for polymorphic fields', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument({ _id: new ObjectId(), value: 'hello' }); + analyzer.addDocument({ _id: new ObjectId(), value: 42 }); + + const fields = getKnownFields(analyzer.getSchema()); + const valueField = fields.find((f: FieldEntry) => f.path === 'value'); + + expect(valueField?.bsonTypes).toBeDefined(); + expect(valueField?.bsonTypes).toHaveLength(2); + expect(valueField?.bsonTypes).toContain('string'); + // Could be 'double' or 'int32' + expect(valueField?.bsonTypes?.some((t: string) => ['double', 'int32'].includes(t))).toBe(true); + }); + + it('returns arrayItemBsonType for array fields', () => { + const analyzer = SchemaAnalyzer.fromDocument({ + _id: new ObjectId(), + tags: ['a', 'b', 'c'], + scores: [10, 20, 30], + }); + const fields = getKnownFields(analyzer.getSchema()); + + const tagsField = fields.find((f: FieldEntry) => f.path === 'tags'); + expect(tagsField?.type).toBe('array'); + expect(tagsField?.bsonType).toBe('array'); + expect(tagsField?.arrayItemBsonType).toBe('string'); + + const scoresField = fields.find((f: FieldEntry) => f.path === 'scores'); + expect(scoresField?.type).toBe('array'); + expect(scoresField?.arrayItemBsonType).toBeDefined(); + }); + + it('handles nested object fields', () => { + const analyzer = SchemaAnalyzer.fromDocument({ + _id: new ObjectId(), + user: { + name: 'Alice', + profile: { + bio: 'hello', + }, + }, + }); + const fields = getKnownFields(analyzer.getSchema()); + const paths = fields.map((f: FieldEntry) => f.path); + + // Objects are expanded, not leaf nodes + expect(paths).not.toContain('user'); + expect(paths).toContain('user.name'); + expect(paths).toContain('user.profile.bio'); + }); + + it('detects optional nested fields', () => { + const analyzer = new SchemaAnalyzer(); + analyzer.addDocument({ _id: new ObjectId(), user: { name: 'Alice', age: 30 } }); + analyzer.addDocument({ _id: new ObjectId(), user: { name: 'Bob' } }); // no age in nested obj + + const fields = getKnownFields(analyzer.getSchema()); + + const nameField = fields.find((f: FieldEntry) => f.path === 'user.name'); + expect(nameField?.isSparse).toBeUndefined(); // present in both objects + + const ageField = fields.find((f: FieldEntry) => f.path === 'user.age'); + expect(ageField?.isSparse).toBe(true); // missing in doc2's user object + }); +}); diff --git a/src/utils/json/data-api/autocomplete/toFieldCompletionItems.test.ts b/src/utils/json/data-api/autocomplete/toFieldCompletionItems.test.ts new file mode 100644 index 000000000..37a7ecc4e --- /dev/null +++ b/src/utils/json/data-api/autocomplete/toFieldCompletionItems.test.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type FieldEntry } from '@vscode-documentdb/schema-analyzer'; +import { toFieldCompletionItems } from './toFieldCompletionItems'; + +describe('toFieldCompletionItems', () => { + it('converts simple fields', () => { + const fields: FieldEntry[] = [ + { path: 'name', type: 'string', bsonType: 'string' }, + { path: 'age', type: 'number', bsonType: 'int32' }, + ]; + + const result = toFieldCompletionItems(fields); + + expect(result).toHaveLength(2); + expect(result[0].fieldName).toBe('name'); + expect(result[0].displayType).toBe('String'); + expect(result[0].bsonType).toBe('string'); + expect(result[0].insertText).toBe('name'); + + expect(result[1].fieldName).toBe('age'); + expect(result[1].displayType).toBe('Int32'); + expect(result[1].bsonType).toBe('int32'); + expect(result[1].insertText).toBe('age'); + }); + + it('escapes dotted paths in insertText', () => { + const fields: FieldEntry[] = [ + { path: 'address.city', type: 'string', bsonType: 'string' }, + { path: 'user.profile.bio', type: 'string', bsonType: 'string' }, + ]; + + const result = toFieldCompletionItems(fields); + + expect(result[0].insertText).toBe('"address.city"'); + expect(result[1].insertText).toBe('"user.profile.bio"'); + }); + + it('quotes field names with dashes', () => { + const fields: FieldEntry[] = [{ path: 'order-items', type: 'string', bsonType: 'string' }]; + const result = toFieldCompletionItems(fields); + expect(result[0].insertText).toBe('"order-items"'); + expect(result[0].fieldName).toBe('order-items'); // display stays unescaped + }); + + it('quotes field names with brackets', () => { + const fields: FieldEntry[] = [{ path: 'items[0]', type: 'string', bsonType: 'string' }]; + const result = toFieldCompletionItems(fields); + expect(result[0].insertText).toBe('"items[0]"'); + }); + + it('quotes field names starting with a digit', () => { + const fields: FieldEntry[] = [{ path: '123abc', type: 'string', bsonType: 'string' }]; + const result = toFieldCompletionItems(fields); + expect(result[0].insertText).toBe('"123abc"'); + }); + + it('escapes embedded double quotes in insertText', () => { + const fields: FieldEntry[] = [{ path: 'say"hi"', type: 'string', bsonType: 'string' }]; + const result = toFieldCompletionItems(fields); + expect(result[0].insertText).toBe('"say\\"hi\\""'); + expect(result[0].fieldName).toBe('say"hi"'); // display stays unescaped + }); + + it('escapes backslashes in insertText', () => { + const fields: FieldEntry[] = [{ path: 'back\\slash', type: 'string', bsonType: 'string' }]; + const result = toFieldCompletionItems(fields); + expect(result[0].insertText).toBe('"back\\\\slash"'); + }); + + it('does not quote valid identifiers', () => { + const fields: FieldEntry[] = [ + { path: 'name', type: 'string', bsonType: 'string' }, + { path: '_id', type: 'string', bsonType: 'objectid' }, + { path: '$type', type: 'string', bsonType: 'string' }, + ]; + const result = toFieldCompletionItems(fields); + expect(result[0].insertText).toBe('name'); + expect(result[1].insertText).toBe('_id'); + expect(result[2].insertText).toBe('$type'); + }); + + it('adds $ prefix to referenceText', () => { + const fields: FieldEntry[] = [ + { path: 'age', type: 'number', bsonType: 'int32' }, + { path: 'address.city', type: 'string', bsonType: 'string' }, + ]; + + const result = toFieldCompletionItems(fields); + + expect(result[0].referenceText).toBe('$age'); + expect(result[1].referenceText).toBe('$address.city'); + }); + + it('preserves isSparse', () => { + const fields: FieldEntry[] = [ + { path: 'name', type: 'string', bsonType: 'string', isSparse: false }, + { path: 'nickname', type: 'string', bsonType: 'string', isSparse: true }, + { path: 'email', type: 'string', bsonType: 'string' }, // undefined โ†’ false + ]; + + const result = toFieldCompletionItems(fields); + + expect(result[0].isSparse).toBe(false); + expect(result[1].isSparse).toBe(true); + expect(result[2].isSparse).toBe(false); + }); + + it('uses correct displayType', () => { + const fields: FieldEntry[] = [ + { path: '_id', type: 'string', bsonType: 'objectid' }, + { path: 'createdAt', type: 'string', bsonType: 'date' }, + { path: 'active', type: 'boolean', bsonType: 'boolean' }, + { path: 'score', type: 'number', bsonType: 'double' }, + { path: 'tags', type: 'array', bsonType: 'array' }, + ]; + + const result = toFieldCompletionItems(fields); + + expect(result[0].displayType).toBe('ObjectId'); + expect(result[1].displayType).toBe('Date'); + expect(result[2].displayType).toBe('Boolean'); + expect(result[3].displayType).toBe('Double'); + expect(result[4].displayType).toBe('Array'); + }); +}); diff --git a/src/utils/json/data-api/autocomplete/toFieldCompletionItems.ts b/src/utils/json/data-api/autocomplete/toFieldCompletionItems.ts new file mode 100644 index 000000000..60e299590 --- /dev/null +++ b/src/utils/json/data-api/autocomplete/toFieldCompletionItems.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BSONTypes, type FieldEntry } from '@vscode-documentdb/schema-analyzer'; + +/** + * Completion-ready data for a single field entry. + * + * Design intent: + * - `fieldName` is the human-readable, unescaped field path shown in the completion list. + * Users see clean names like "address.city" or "order-items" without quotes or escaping. + * - `insertText` is the escaped/quoted form that gets inserted when the user selects a + * completion item. For simple identifiers it matches `fieldName`; for names containing + * special characters (dots, spaces, `$`, etc.) it is wrapped in double quotes. + * - `referenceText` is the `$`-prefixed aggregation field reference (e.g., "$age"). + */ +export interface FieldCompletionData { + /** The full dot-notated field name, e.g., "address.city" โ€” kept unescaped for display */ + fieldName: string; + /** Human-readable type display, e.g., "String", "Date", "ObjectId" */ + displayType: string; + /** Raw BSON type from FieldEntry */ + bsonType: string; + /** All observed BSON types for polymorphic fields (e.g., ["string", "int32"]) */ + bsonTypes?: string[]; + /** Human-readable display strings for all observed types (e.g., ["String", "Int32"]) */ + displayTypes?: string[]; + /** Whether the field was not present in every inspected document (statistical observation, not a constraint) */ + isSparse: boolean; + /** Text to insert when the user selects this completion โ€” quoted/escaped if the field name contains special chars */ + insertText: string; + /** + * Field reference for aggregation expressions, e.g., "$age", "$address.city". + * + * TODO: The simple `$field.path` syntax is invalid MQL for field names containing dots, + * spaces, or `$` characters. For such fields, the correct MQL syntax is + * `{ $getField: "fieldName" }`. This should be addressed when the aggregation + * completion provider is wired up โ€” either by using `$getField` for special names + * or by making `referenceText` optional for fields that cannot use the `$` prefix syntax. + */ + referenceText: string; +} + +/** + * Matches valid JavaScript/TypeScript identifiers. + * A valid identifier starts with a letter, underscore, or dollar sign, + * followed by zero or more letters, digits, underscores, or dollar signs. + * + * Field names that do NOT match this pattern must be quoted and escaped + * in `insertText` to produce valid query expressions. + */ +const JS_IDENTIFIER_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; + +/** + * Converts an array of FieldEntry objects into completion-ready FieldCompletionData items. + * + * @param fields - Array of FieldEntry objects from getKnownFields + * @returns Array of FieldCompletionData ready for use in editor completions + */ +export function toFieldCompletionItems(fields: FieldEntry[]): FieldCompletionData[] { + return fields.map((entry) => { + const displayType = BSONTypes.toDisplayString(entry.bsonType as BSONTypes); + const needsQuoting = !JS_IDENTIFIER_PATTERN.test(entry.path); + + let insertText: string; + if (needsQuoting) { + const escaped = entry.path.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + insertText = `"${escaped}"`; + } else { + insertText = entry.path; + } + + return { + fieldName: entry.path, + displayType, + bsonType: entry.bsonType, + bsonTypes: entry.bsonTypes, + displayTypes: entry.bsonTypes?.map((t) => BSONTypes.toDisplayString(t as BSONTypes)), + isSparse: entry.isSparse ?? false, + insertText, + referenceText: `$${entry.path}`, + }; + }); +} diff --git a/src/utils/json/data-api/autocomplete/toTypeScriptDefinition.test.ts b/src/utils/json/data-api/autocomplete/toTypeScriptDefinition.test.ts new file mode 100644 index 000000000..d003b9ded --- /dev/null +++ b/src/utils/json/data-api/autocomplete/toTypeScriptDefinition.test.ts @@ -0,0 +1,318 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type JSONSchema } from '@vscode-documentdb/schema-analyzer'; +import { toTypeScriptDefinition } from './toTypeScriptDefinition'; + +describe('toTypeScriptDefinition', () => { + it('generates basic interface with primitive types', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + _id: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'objectid', + 'x-typeOccurrence': 100, + }, + ], + }, + name: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + }, + ], + }, + age: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'number', + 'x-bsonType': 'int32', + 'x-typeOccurrence': 100, + }, + ], + }, + }, + }; + + const result = toTypeScriptDefinition(schema, 'users'); + + expect(result).toContain('interface UsersDocument {'); + expect(result).toContain(' _id: ObjectId;'); + expect(result).toContain(' name: string;'); + expect(result).toContain(' age: number;'); + expect(result).toContain('}'); + }); + + it('marks optional fields with ?', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + name: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + }, + ], + }, + nickname: { + 'x-occurrence': 50, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 50, + }, + ], + }, + }, + }; + + const result = toTypeScriptDefinition(schema, 'users'); + + expect(result).toContain(' name: string;'); + expect(result).toContain(' nickname?: string;'); + }); + + it('handles nested objects as inline blocks', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + address: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'object', + 'x-bsonType': 'object', + 'x-typeOccurrence': 100, + 'x-documentsInspected': 100, + properties: { + city: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + }, + ], + }, + zip: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + }, + ], + }, + }, + }, + ], + }, + }, + }; + + const result = toTypeScriptDefinition(schema, 'users'); + + expect(result).toContain(' address: {'); + expect(result).toContain(' city: string;'); + expect(result).toContain(' zip: string;'); + expect(result).toContain(' };'); + }); + + it('handles arrays with element types', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + tags: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'array', + 'x-bsonType': 'array', + 'x-typeOccurrence': 100, + items: { + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + }, + ], + }, + }, + ], + }, + }, + }; + + const result = toTypeScriptDefinition(schema, 'posts'); + + expect(result).toContain(' tags: string[];'); + }); + + it('handles polymorphic fields as unions', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + metadata: { + 'x-occurrence': 80, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 50, + }, + { + type: 'number', + 'x-bsonType': 'int32', + 'x-typeOccurrence': 20, + }, + { + type: 'null', + 'x-bsonType': 'null', + 'x-typeOccurrence': 10, + }, + ], + }, + }, + }; + + const result = toTypeScriptDefinition(schema, 'items'); + + expect(result).toContain(' metadata?: string | number | null;'); + }); + + it('PascalCase conversion for collection name', () => { + expect(toTypeScriptDefinition({ 'x-documentsInspected': 0 }, 'users')).toContain('interface UsersDocument'); + expect(toTypeScriptDefinition({ 'x-documentsInspected': 0 }, 'order_items')).toContain( + 'interface OrderItemsDocument', + ); + expect(toTypeScriptDefinition({ 'x-documentsInspected': 0 }, 'my-awesome-collection')).toContain( + 'interface MyAwesomeCollectionDocument', + ); + }); + + it('prefixes with _ when collection name starts with a digit', () => { + expect(toTypeScriptDefinition({ 'x-documentsInspected': 0 }, '123abc')).toContain('interface _123abcDocument'); + expect(toTypeScriptDefinition({ 'x-documentsInspected': 0 }, '99_bottles')).toContain( + 'interface _99BottlesDocument', + ); + }); + + it('falls back to CollectionDocument when name is only separators', () => { + expect(toTypeScriptDefinition({ 'x-documentsInspected': 0 }, '---')).toContain('interface CollectionDocument'); + expect(toTypeScriptDefinition({ 'x-documentsInspected': 0 }, '_ _ _')).toContain( + 'interface CollectionDocument', + ); + }); + + it('falls back to CollectionDocument for empty string', () => { + expect(toTypeScriptDefinition({ 'x-documentsInspected': 0 }, '')).toContain('interface CollectionDocument'); + }); + + describe('special character field names', () => { + function makeSchemaWithField(fieldName: string): JSONSchema { + return { + 'x-documentsInspected': 100, + properties: { + [fieldName]: { + 'x-occurrence': 100, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 100, + }, + ], + }, + }, + }; + } + + it('leaves valid identifiers unquoted', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('age'), 'test'); + expect(result).toContain(' age: string;'); + }); + + it('leaves underscore-prefixed identifiers unquoted', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('_id'), 'test'); + expect(result).toContain(' _id: string;'); + }); + + it('leaves dollar-prefixed identifiers unquoted', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('$type'), 'test'); + expect(result).toContain(' $type: string;'); + }); + + it('quotes field names with dashes', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('order-items'), 'test'); + expect(result).toContain(' "order-items": string;'); + }); + + it('quotes field names with dots', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('a.b'), 'test'); + expect(result).toContain(' "a.b": string;'); + }); + + it('quotes field names with spaces', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('my field'), 'test'); + expect(result).toContain(' "my field": string;'); + }); + + it('quotes field names with brackets', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('items[0]'), 'test'); + expect(result).toContain(' "items[0]": string;'); + }); + + it('escapes embedded double quotes in field names', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('say"hi"'), 'test'); + expect(result).toContain(' "say\\"hi\\"": string;'); + }); + + it('escapes backslashes in field names', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('back\\slash'), 'test'); + expect(result).toContain(' "back\\\\slash": string;'); + }); + + it('quotes field names that start with a digit', () => { + const result = toTypeScriptDefinition(makeSchemaWithField('123abc'), 'test'); + expect(result).toContain(' "123abc": string;'); + }); + + it('preserves optionality with quoted field names', () => { + const schema: JSONSchema = { + 'x-documentsInspected': 100, + properties: { + 'order-items': { + 'x-occurrence': 50, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-typeOccurrence': 50, + }, + ], + }, + }, + }; + + const result = toTypeScriptDefinition(schema, 'test'); + expect(result).toContain(' "order-items"?: string;'); + }); + }); +}); diff --git a/src/utils/json/data-api/autocomplete/toTypeScriptDefinition.ts b/src/utils/json/data-api/autocomplete/toTypeScriptDefinition.ts new file mode 100644 index 000000000..17328dfeb --- /dev/null +++ b/src/utils/json/data-api/autocomplete/toTypeScriptDefinition.ts @@ -0,0 +1,272 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BSONTypes, type JSONSchema } from '@vscode-documentdb/schema-analyzer'; + +/** + * Maps a BSON type string to the corresponding TypeScript type representation. + */ +const bsonToTypeScriptMap: Record = { + [BSONTypes.String]: 'string', + [BSONTypes.Int32]: 'number', + [BSONTypes.Double]: 'number', + [BSONTypes.Long]: 'number', + [BSONTypes.Decimal128]: 'number', + [BSONTypes.Number]: 'number', + [BSONTypes.Boolean]: 'boolean', + [BSONTypes.Date]: 'Date', + [BSONTypes.ObjectId]: 'ObjectId', + [BSONTypes.Null]: 'null', + [BSONTypes.Undefined]: 'undefined', + [BSONTypes.Binary]: 'Binary', + [BSONTypes.RegExp]: 'RegExp', + [BSONTypes.UUID]: 'UUID', + [BSONTypes.UUID_LEGACY]: 'UUID', + [BSONTypes.Timestamp]: 'Timestamp', + [BSONTypes.MinKey]: 'MinKey', + [BSONTypes.MaxKey]: 'MaxKey', + [BSONTypes.Code]: 'Code', + [BSONTypes.CodeWithScope]: 'Code', + [BSONTypes.DBRef]: 'DBRef', + [BSONTypes.Map]: 'Map', + [BSONTypes.Symbol]: 'symbol', +}; + +/** + * Converts a BSON type string to a TypeScript type string. + */ +function bsonTypeToTS(bsonType: string): string { + return bsonToTypeScriptMap[bsonType] ?? 'unknown'; +} + +/** + * Matches valid JavaScript/TypeScript identifiers. + * A valid identifier starts with a letter, underscore, or dollar sign, + * followed by zero or more letters, digits, underscores, or dollar signs. + */ +const JS_IDENTIFIER_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; + +/** + * Returns a safe TypeScript property name for use in interface definitions. + * If the name is a valid JS identifier, it is returned as-is. + * Otherwise, it is wrapped in double quotes with internal quotes and backslashes escaped. + * + * Examples: + * - "age" โ†’ "age" (valid identifier, unchanged) + * - "order-items" โ†’ '"order-items"' (dash) + * - "a.b" โ†’ '"a.b"' (dot) + * - "my field" โ†’ '"my field"' (space) + * - 'say"hi"' โ†’ '"say\\"hi\\""' (embedded quotes escaped) + */ +function safePropertyName(name: string): string { + if (JS_IDENTIFIER_PATTERN.test(name)) { + return name; + } + const escaped = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + return `"${escaped}"`; +} + +/** + * Converts a collection name to PascalCase and appends "Document". + * If the result would start with a digit, a leading `_` is prepended. + * If the collection name contains only separators or is empty, falls back to "CollectionDocument". + * + * Examples: + * - "users" โ†’ "UsersDocument" + * - "order_items" โ†’ "OrderItemsDocument" + * - "123abc" โ†’ "_123abcDocument" + * - "---" โ†’ "CollectionDocument" + */ +function toInterfaceName(collectionName: string): string { + const pascal = collectionName + .split(/[_\-\s]+/) + .filter((s) => s.length > 0) + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(''); + + if (pascal.length === 0) { + return 'CollectionDocument'; + } + + // Prefix with _ if the first character is a digit (invalid TS identifier start) + const prefix = /^[0-9]/.test(pascal) ? '_' : ''; + return `${prefix}${pascal}Document`; +} + +/** + * Generates a TypeScript interface definition string from a JSONSchema + * produced by the SchemaAnalyzer. + * + * @param schema - The JSON Schema with x- extensions from SchemaAnalyzer + * @param collectionName - The MongoDB API collection name, used to derive the interface name + * @returns A formatted TypeScript interface definition string + */ +export function toTypeScriptDefinition(schema: JSONSchema, collectionName: string): string { + const interfaceName = toInterfaceName(collectionName); + const rootDocumentsInspected = (schema['x-documentsInspected'] as number) ?? 0; + + const lines: string[] = []; + lines.push(`interface ${interfaceName} {`); + + if (schema.properties) { + renderProperties(schema.properties, rootDocumentsInspected, 1, lines); + } + + lines.push('}'); + return lines.join('\n'); +} + +/** + * Renders property lines for a set of JSON Schema properties at a given indent level. + */ +function renderProperties( + properties: Record, + parentDocumentsInspected: number, + indentLevel: number, + lines: string[], +): void { + const indent = ' '.repeat(indentLevel); + + for (const [propName, propSchema] of Object.entries(properties)) { + if (typeof propSchema === 'boolean') continue; + + const isOptional = isFieldOptional(propSchema, parentDocumentsInspected); + const optionalMarker = isOptional ? '?' : ''; + const tsType = resolveTypeString(propSchema, indentLevel); + const safeName = safePropertyName(propName); + + lines.push(`${indent}${safeName}${optionalMarker}: ${tsType};`); + } +} + +/** + * Returns true if the field's occurrence is less than the parent's document count. + */ +function isFieldOptional(schemaNode: JSONSchema, parentDocumentsInspected: number): boolean { + const occurrence = (schemaNode['x-occurrence'] as number) ?? 0; + return parentDocumentsInspected > 0 && occurrence < parentDocumentsInspected; +} + +/** + * Resolves a full TypeScript type string for a schema node by examining its + * `anyOf` entries. Handles primitives, objects (inline blocks), and arrays. + */ +function resolveTypeString(schemaNode: JSONSchema, indentLevel: number): string { + if (!schemaNode.anyOf || schemaNode.anyOf.length === 0) { + return 'unknown'; + } + + const typeStrings: string[] = []; + + for (const entry of schemaNode.anyOf) { + if (typeof entry === 'boolean') continue; + const ts = singleEntryToTS(entry, indentLevel); + if (ts && !typeStrings.includes(ts)) { + typeStrings.push(ts); + } + } + + if (typeStrings.length === 0) { + return 'unknown'; + } + + return typeStrings.join(' | '); +} + +/** + * Converts a single `anyOf` type entry to a TypeScript type string. + */ +function singleEntryToTS(entry: JSONSchema, indentLevel: number): string { + const bsonType = (entry['x-bsonType'] as string) ?? ''; + + // Object with nested properties โ†’ inline block + if (entry.type === 'object' && entry.properties) { + return renderInlineObject(entry, indentLevel); + } + + // Array โ†’ determine element types + if (entry.type === 'array' || bsonType === (BSONTypes.Array as string)) { + return renderArrayType(entry, indentLevel); + } + + // Primitive or mapped type + if (bsonType) { + return bsonTypeToTS(bsonType); + } + + // Fallback to JSON type + const jsonType = entry.type as string | undefined; + if (jsonType) { + return jsonType; + } + + return 'unknown'; +} + +/** + * Renders an inline object type `{ field: type; ... }`. + */ +function renderInlineObject(entry: JSONSchema, indentLevel: number): string { + const lines: string[] = []; + const objectDocumentsInspected = (entry['x-documentsInspected'] as number) ?? 0; + + lines.push('{'); + + if (entry.properties) { + renderProperties(entry.properties, objectDocumentsInspected, indentLevel + 1, lines); + } + + const closingIndent = ' '.repeat(indentLevel); + lines.push(`${closingIndent}}`); + + return lines.join('\n'); +} + +/** + * Renders an array type, e.g., `string[]` or `(string | number)[]`. + */ +function renderArrayType(entry: JSONSchema, indentLevel: number): string { + const itemsSchema = entry.items; + + if (!itemsSchema || typeof itemsSchema === 'boolean') { + return 'unknown[]'; + } + + // Items specified as a single schema (not an array of schemas) + if (!Array.isArray(itemsSchema)) { + const itemSchema = itemsSchema as JSONSchema; + + if (itemSchema.anyOf && itemSchema.anyOf.length > 0) { + const elementTypes: string[] = []; + for (const itemEntry of itemSchema.anyOf) { + if (typeof itemEntry === 'boolean') continue; + const ts = singleEntryToTS(itemEntry, indentLevel); + if (ts && !elementTypes.includes(ts)) { + elementTypes.push(ts); + } + } + + if (elementTypes.length === 0) { + return 'unknown[]'; + } + + if (elementTypes.length === 1) { + return `${elementTypes[0]}[]`; + } + + return `(${elementTypes.join(' | ')})[]`; + } + + // Single item type without anyOf + const bsonType = (itemSchema['x-bsonType'] as string) ?? ''; + if (bsonType) { + return `${bsonTypeToTS(bsonType)}[]`; + } + + return 'unknown[]'; + } + + return 'unknown[]'; +} diff --git a/src/utils/json/mongo/MongoBSONTypes.ts b/src/utils/json/mongo/MongoBSONTypes.ts deleted file mode 100644 index fa97add9c..000000000 --- a/src/utils/json/mongo/MongoBSONTypes.ts +++ /dev/null @@ -1,200 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { - Binary, - BSONSymbol, - Code, - DBRef, - Decimal128, - Double, - Int32, - Long, - MaxKey, - MinKey, - ObjectId, - Timestamp, - UUID, -} from 'mongodb'; - -/** - * Represents the different data types that can be stored in a MongoDB document. - * The string representation is casesensitive and should match the MongoDB documentation. - * https://www.mongodb.com/docs/manual/reference/bson-types/ - */ -export enum MongoBSONTypes { - String = 'string', - Number = 'number', - Int32 = 'int32', - Double = 'double', - Decimal128 = 'decimal128', - Long = 'long', - Boolean = 'boolean', - Object = 'object', - Array = 'array', - Null = 'null', - Undefined = 'undefined', - Date = 'date', - RegExp = 'regexp', - Binary = 'binary', - ObjectId = 'objectid', - Symbol = 'symbol', - Timestamp = 'timestamp', - UUID = 'uuid', - UUID_LEGACY = 'uuid-legacy', // old UUID subtype, used in some legacy data - MinKey = 'minkey', - MaxKey = 'maxkey', - DBRef = 'dbref', - Code = 'code', - CodeWithScope = 'codewithscope', - Map = 'map', - // Add any deprecated types if necessary - _UNKNOWN_ = '_unknown_', // Catch-all for unknown types -} - -export namespace MongoBSONTypes { - const displayStringMap: Record = { - [MongoBSONTypes.String]: 'String', - [MongoBSONTypes.Number]: 'Number', - [MongoBSONTypes.Int32]: 'Int32', - [MongoBSONTypes.Double]: 'Double', - [MongoBSONTypes.Decimal128]: 'Decimal128', - [MongoBSONTypes.Long]: 'Long', - [MongoBSONTypes.Boolean]: 'Boolean', - [MongoBSONTypes.Object]: 'Object', - [MongoBSONTypes.Array]: 'Array', - [MongoBSONTypes.Null]: 'Null', - [MongoBSONTypes.Undefined]: 'Undefined', - [MongoBSONTypes.Date]: 'Date', - [MongoBSONTypes.RegExp]: 'RegExp', - [MongoBSONTypes.Binary]: 'Binary', - [MongoBSONTypes.ObjectId]: 'ObjectId', - [MongoBSONTypes.Symbol]: 'Symbol', - [MongoBSONTypes.Timestamp]: 'Timestamp', - [MongoBSONTypes.MinKey]: 'MinKey', - [MongoBSONTypes.MaxKey]: 'MaxKey', - [MongoBSONTypes.DBRef]: 'DBRef', - [MongoBSONTypes.Code]: 'Code', - [MongoBSONTypes.CodeWithScope]: 'CodeWithScope', - [MongoBSONTypes.Map]: 'Map', - [MongoBSONTypes._UNKNOWN_]: 'Unknown', - [MongoBSONTypes.UUID]: 'UUID', - [MongoBSONTypes.UUID_LEGACY]: 'UUID (Legacy)', - }; - - export function toDisplayString(type: MongoBSONTypes): string { - return displayStringMap[type] || 'Unknown'; - } - - export function toString(type: MongoBSONTypes): string { - return type; - } - - /** - * Converts a MongoDB data type to a case sensitive JSON data type - * @param type The MongoDB data type - * @returns A corresponding JSON data type (please note: it's case sensitive) - */ - export function toJSONType(type: MongoBSONTypes): string { - switch (type) { - case MongoBSONTypes.String: - case MongoBSONTypes.Symbol: - case MongoBSONTypes.Date: - case MongoBSONTypes.Timestamp: - case MongoBSONTypes.ObjectId: - case MongoBSONTypes.RegExp: - case MongoBSONTypes.Binary: - case MongoBSONTypes.Code: - case MongoBSONTypes.UUID: - case MongoBSONTypes.UUID_LEGACY: - return 'string'; - - case MongoBSONTypes.Boolean: - return 'boolean'; - - case MongoBSONTypes.Int32: - case MongoBSONTypes.Long: - case MongoBSONTypes.Double: - case MongoBSONTypes.Decimal128: - return 'number'; - - case MongoBSONTypes.Object: - case MongoBSONTypes.Map: - case MongoBSONTypes.DBRef: - case MongoBSONTypes.CodeWithScope: - return 'object'; - - case MongoBSONTypes.Array: - return 'array'; - - case MongoBSONTypes.Null: - case MongoBSONTypes.Undefined: - case MongoBSONTypes.MinKey: - case MongoBSONTypes.MaxKey: - return 'null'; - - default: - return 'string'; // Default to string for unknown types - } - } - - /** - * Accepts a value from a MongoDB 'Document' object and returns the inferred type. - * @param value The value of a field in a MongoDB 'Document' object - * @returns - */ - export function inferType(value: unknown): MongoBSONTypes { - if (value === null) return MongoBSONTypes.Null; - if (value === undefined) return MongoBSONTypes.Undefined; - - switch (typeof value) { - case 'string': - return MongoBSONTypes.String; - case 'number': - return MongoBSONTypes.Double; // JavaScript numbers are doubles - case 'boolean': - return MongoBSONTypes.Boolean; - case 'object': - if (Array.isArray(value)) { - return MongoBSONTypes.Array; - } - - // Check for common BSON types first - if (value instanceof ObjectId) return MongoBSONTypes.ObjectId; - if (value instanceof Int32) return MongoBSONTypes.Int32; - if (value instanceof Double) return MongoBSONTypes.Double; - if (value instanceof Date) return MongoBSONTypes.Date; - if (value instanceof Timestamp) return MongoBSONTypes.Timestamp; - - // Less common types - if (value instanceof Decimal128) return MongoBSONTypes.Decimal128; - if (value instanceof Long) return MongoBSONTypes.Long; - if (value instanceof MinKey) return MongoBSONTypes.MinKey; - if (value instanceof MaxKey) return MongoBSONTypes.MaxKey; - if (value instanceof BSONSymbol) return MongoBSONTypes.Symbol; - if (value instanceof DBRef) return MongoBSONTypes.DBRef; - if (value instanceof Map) return MongoBSONTypes.Map; - if (value instanceof UUID && value.sub_type === Binary.SUBTYPE_UUID) return MongoBSONTypes.UUID; - if (value instanceof UUID && value.sub_type === Binary.SUBTYPE_UUID_OLD) - return MongoBSONTypes.UUID_LEGACY; - if (value instanceof Buffer || value instanceof Binary) return MongoBSONTypes.Binary; - if (value instanceof RegExp) return MongoBSONTypes.RegExp; - if (value instanceof Code) { - if (value.scope) { - return MongoBSONTypes.CodeWithScope; - } else { - return MongoBSONTypes.Code; - } - } - - // Default to Object if none of the above match - return MongoBSONTypes.Object; - default: - // This should never happen, but if it does, we'll catch it here - // TODO: add telemetry somewhere to know when it happens (not here, this could get hit too often) - return MongoBSONTypes._UNKNOWN_; - } - } -} diff --git a/src/utils/json/mongo/SchemaAnalyzer.test.ts b/src/utils/json/mongo/SchemaAnalyzer.test.ts deleted file mode 100644 index 731791611..000000000 --- a/src/utils/json/mongo/SchemaAnalyzer.test.ts +++ /dev/null @@ -1,255 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type JSONSchema, type JSONSchemaRef } from '../JSONSchema'; -import { getPropertyNamesAtLevel, updateSchemaWithDocument } from './SchemaAnalyzer'; -import { - arraysWithDifferentDataTypes, - complexDocument, - complexDocumentsArray, - complexDocumentWithOddTypes, - embeddedDocumentOnly, - flatDocument, - sparseDocumentsArray, -} from './mongoTestDocuments'; - -describe('DocumentDB Schema Analyzer', () => { - it('prints out schema for testing', () => { - const schema: JSONSchema = {}; - updateSchemaWithDocument(schema, embeddedDocumentOnly); - console.log(JSON.stringify(schema, null, 2)); - expect(schema).toBeDefined(); - }); - - it('supports many documents', () => { - const schema: JSONSchema = {}; - sparseDocumentsArray.forEach((doc) => updateSchemaWithDocument(schema, doc)); - expect(schema).toBeDefined(); - - // Check that 'x-documentsInspected' is correct - expect(schema['x-documentsInspected']).toBe(sparseDocumentsArray.length); - - // Check that the schema has the correct root properties - const expectedRootProperties = new Set(['_id', 'name', 'age', 'email', 'isActive', 'score', 'description']); - - expect(Object.keys(schema.properties || {})).toEqual( - expect.arrayContaining(Array.from(expectedRootProperties)), - ); - - // Check that the 'name' field is detected correctly - const nameField: JSONSchema = schema.properties?.['name']; - expect(nameField).toBeDefined(); - expect(nameField?.['x-occurrence']).toBeGreaterThan(0); - - // Access 'anyOf' to get the type entries - const nameFieldTypes = nameField.anyOf?.map((typeEntry) => typeEntry['type']); - expect(nameFieldTypes).toContain('string'); - - // Check that the 'age' field has the correct type - const ageField: JSONSchema = schema.properties?.['age']; - expect(ageField).toBeDefined(); - const ageFieldTypes = ageField.anyOf?.map((typeEntry) => typeEntry['type']); - expect(ageFieldTypes).toContain('number'); - - // Check that the 'isActive' field is a boolean - const isActiveField: JSONSchema = schema.properties?.['isActive']; - expect(isActiveField).toBeDefined(); - const isActiveTypes = isActiveField.anyOf?.map((typeEntry) => typeEntry['type']); - expect(isActiveTypes).toContain('boolean'); - - // Check that the 'description' field is optional (occurs in some documents) - const descriptionField = schema.properties?.['description']; - expect(descriptionField).toBeDefined(); - expect(descriptionField?.['x-occurrence']).toBeLessThan(sparseDocumentsArray.length); - }); - - it('detects all BSON types from flatDocument', () => { - const schema: JSONSchema = {}; - updateSchemaWithDocument(schema, flatDocument); - - // Check that all fields are detected - const expectedFields = Object.keys(flatDocument); - expect(Object.keys(schema.properties || {})).toEqual(expect.arrayContaining(expectedFields)); - - // Helper function to get the 'x-bsonType' from a field - function getBsonType(fieldName: string): string | undefined { - const field = schema.properties?.[fieldName]; - const anyOf = field?.anyOf; - return anyOf && anyOf[0]?.['x-bsonType']; - } - - // Check that specific BSON types are correctly identified - expect(getBsonType('int32Field')).toBe('int32'); - expect(getBsonType('doubleField')).toBe('double'); - expect(getBsonType('decimalField')).toBe('decimal128'); - expect(getBsonType('dateField')).toBe('date'); - expect(getBsonType('objectIdField')).toBe('objectid'); - expect(getBsonType('codeField')).toBe('code'); - expect(getBsonType('uuidField')).toBe('uuid'); - expect(getBsonType('uuidLegacyField')).toBe('uuid-legacy'); - }); - - it('detects embedded objects correctly', () => { - const schema: JSONSchema = {}; - updateSchemaWithDocument(schema, embeddedDocumentOnly); - - // Check that the root properties are detected - expect(schema.properties).toHaveProperty('personalInfo'); - expect(schema.properties).toHaveProperty('jobInfo'); - - // Access 'personalInfo' properties - const personalInfoAnyOf = schema.properties && schema.properties['personalInfo']?.anyOf; - const personalInfoProperties = personalInfoAnyOf?.[0]?.properties; - expect(personalInfoProperties).toBeDefined(); - expect(personalInfoProperties).toHaveProperty('name'); - expect(personalInfoProperties).toHaveProperty('age'); - expect(personalInfoProperties).toHaveProperty('married'); - expect(personalInfoProperties).toHaveProperty('address'); - - // Access 'address' properties within 'personalInfo' - const addressAnyOf = personalInfoProperties['address'].anyOf; - const addressProperties = addressAnyOf?.[0]?.properties; - expect(addressProperties).toBeDefined(); - expect(addressProperties).toHaveProperty('street'); - expect(addressProperties).toHaveProperty('city'); - expect(addressProperties).toHaveProperty('zip'); - }); - - it('detects arrays and their element types correctly', () => { - const schema: JSONSchema = {}; - updateSchemaWithDocument(schema, arraysWithDifferentDataTypes); - - // Check that arrays are detected - expect(schema.properties).toHaveProperty('integersArray'); - expect(schema.properties).toHaveProperty('stringsArray'); - expect(schema.properties).toHaveProperty('booleansArray'); - expect(schema.properties).toHaveProperty('mixedArray'); - expect(schema.properties).toHaveProperty('datesArray'); - - // Helper function to get item types from an array field - function getArrayItemTypes(fieldName: string): string[] | undefined { - const field = schema.properties?.[fieldName]; - const anyOf = field?.anyOf; - const itemsAnyOf: JSONSchemaRef[] = anyOf?.[0]?.items?.anyOf; - return itemsAnyOf?.map((typeEntry) => typeEntry['type']); - } - - // Check that 'integersArray' has elements of type 'number' - const integerItemTypes = getArrayItemTypes('integersArray'); - expect(integerItemTypes).toContain('number'); - - // Check that 'stringsArray' has elements of type 'string' - const stringItemTypes = getArrayItemTypes('stringsArray'); - expect(stringItemTypes).toContain('string'); - - // Check that 'mixedArray' contains multiple types - const mixedItemTypes = getArrayItemTypes('mixedArray'); - expect(mixedItemTypes).toEqual(expect.arrayContaining(['number', 'string', 'boolean', 'object', 'null'])); - }); - - it('handles arrays within objects and objects within arrays', () => { - const schema: JSONSchema = {}; - updateSchemaWithDocument(schema, complexDocument); - - // Access 'user.profile.hobbies' - const userProfile = schema.properties && schema.properties['user'].anyOf?.[0]?.properties?.['profile']; - const hobbies = userProfile?.anyOf?.[0]?.properties?.['hobbies']; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const hobbiesItemTypes = hobbies?.anyOf?.[0]?.items?.anyOf?.map((typeEntry) => typeEntry['type']); - expect(hobbiesItemTypes).toContain('string'); - - // Access 'user.profile.addresses' - const addresses = userProfile?.anyOf?.[0]?.properties?.['addresses']; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const addressItemTypes = addresses?.anyOf?.[0]?.items?.anyOf?.map((typeEntry) => typeEntry['type']); - expect(addressItemTypes).toContain('object'); - - // Check that 'orders' is an array - const orders = schema.properties && schema.properties['orders']; - expect(orders).toBeDefined(); - const ordersType = orders.anyOf?.[0]?.type; - expect(ordersType).toBe('array'); - - // Access 'items' within 'orders' - const orderItems = orders.anyOf?.[0]?.items?.anyOf?.[0]?.properties?.['items']; - const orderItemsType = orderItems?.anyOf?.[0]?.type; - expect(orderItemsType).toBe('array'); - }); - - it('updates schema correctly when processing multiple documents', () => { - const schema: JSONSchema = {}; - complexDocumentsArray.forEach((doc) => updateSchemaWithDocument(schema, doc)); - - // Check that 'x-documentsInspected' is correct - expect(schema['x-documentsInspected']).toBe(complexDocumentsArray.length); - - // Check that some fields are present from different documents - expect(schema.properties).toHaveProperty('stringField'); - expect(schema.properties).toHaveProperty('personalInfo'); - expect(schema.properties).toHaveProperty('integersArray'); - expect(schema.properties).toHaveProperty('user'); - - // Check that 'integersArray' has correct min and max values - const integersArray = schema.properties && schema.properties['integersArray']; - const integerItemType = integersArray.anyOf?.[0]?.items?.anyOf?.[0]; - expect(integerItemType?.['x-minValue']).toBe(1); - expect(integerItemType?.['x-maxValue']).toBe(5); - - // Check that 'orders.items.price' is detected as Decimal128 - const orders = schema.properties && schema.properties['orders']; - const orderItems = orders.anyOf?.[0]?.items?.anyOf?.[0]?.properties?.['items']; - const priceField = orderItems?.anyOf?.[0]?.items?.anyOf?.[0]?.properties?.['price']; - const priceFieldType = priceField?.anyOf?.[0]; - expect(priceFieldType?.['x-bsonType']).toBe('decimal128'); - }); - - describe('traverses schema', () => { - it('with valid paths', () => { - const schema: JSONSchema = {}; - updateSchemaWithDocument(schema, complexDocument); - - let propertiesAtRoot = getPropertyNamesAtLevel(schema, []); - expect(propertiesAtRoot).toHaveLength(4); - - propertiesAtRoot = getPropertyNamesAtLevel(schema, ['user']); - expect(propertiesAtRoot).toHaveLength(3); - - propertiesAtRoot = getPropertyNamesAtLevel(schema, ['user', 'profile']); - expect(propertiesAtRoot).toHaveLength(4); - }); - - it('with broken paths', () => { - const schema: JSONSchema = {}; - updateSchemaWithDocument(schema, complexDocument); - - const propertiesAtRoot = getPropertyNamesAtLevel(schema, []); - expect(propertiesAtRoot).toHaveLength(4); - - expect(() => getPropertyNamesAtLevel(schema, ['no-entry'])).toThrow(); - - expect(() => getPropertyNamesAtLevel(schema, ['user', 'no-entry'])).toThrow(); - }); - - it('with sparse docs and mixed types', () => { - const schema: JSONSchema = {}; - updateSchemaWithDocument(schema, complexDocument); - updateSchemaWithDocument(schema, complexDocumentWithOddTypes); - - let propertiesAtRoot = getPropertyNamesAtLevel(schema, []); - expect(propertiesAtRoot).toHaveLength(4); - - propertiesAtRoot = getPropertyNamesAtLevel(schema, ['user']); - expect(propertiesAtRoot).toHaveLength(3); - expect(propertiesAtRoot).toEqual(['email', 'profile', 'username']); - - propertiesAtRoot = getPropertyNamesAtLevel(schema, ['user', 'profile']); - expect(propertiesAtRoot).toHaveLength(4); - expect(propertiesAtRoot).toEqual(['addresses', 'firstName', 'hobbies', 'lastName']); - - propertiesAtRoot = getPropertyNamesAtLevel(schema, ['history']); - expect(propertiesAtRoot).toHaveLength(6); - }); - }); -}); diff --git a/src/utils/json/mongo/autocomplete/basicMongoFindFilterSchema.json b/src/utils/json/mongo/autocomplete/basicMongoFindFilterSchema.json deleted file mode 100644 index a886411a4..000000000 --- a/src/utils/json/mongo/autocomplete/basicMongoFindFilterSchema.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "mongodb-generic-filter-schema", - "title": "MongoDB Generic Find Filter Schema", - "type": "object", - "additionalProperties": { - "oneOf": [ - { - "title": "Direct Value", - "description": "A direct value for equality matching on any field.", - "examples": ["example", 42, true, null] - }, - { - "title": "Operator-Based Query", - "$ref": "#/definitions/operatorObject", - "examples": [ - { "$gt": 10 }, - { "$lt": 100 }, - { "$gte": 5 }, - { "$lte": 50 }, - { "$in": ["red", "blue", "green"] }, - { "$nin": ["yellow", "purple"] }, - { "$exists": false }, - { "$regex": "^start.*end$" }, - { "$gt": 10, "$lt": 20 }, - { "$in": [1, 2, 3], "$nin": [4, 5] } - ] - } - ] - }, - "properties": { - "$or": { - "type": "array", - "items": { "$ref": "#" }, - "description": "Joins query clauses with a logical OR.", - "examples": [ - [{ "status": "A" }, { "qty": { "$lt": 30 } }], - [{ "age": { "$gte": 18 } }, { "membership": "gold" }], - [{ "category": { "$in": ["electronics", "books"] } }, { "onSale": true }] - ] - }, - "$and": { - "type": "array", - "items": { "$ref": "#" }, - "description": "Joins query clauses with a logical AND.", - "examples": [ - [{ "status": "A" }, { "qty": { "$gt": 20, "$lt": 50 } }], - [{ "verified": true }, { "email": { "$exists": true } }], - [{ "price": { "$gte": 100 } }, { "stock": { "$lte": 500 } }] - ] - }, - "$not": { - "oneOf": [{ "$ref": "#" }], - "description": "Inverts the effect of a query expression.", - "examples": [ - { "price": { "$gt": 100 } }, - { "status": { "$eq": "inactive" } }, - { "category": { "$in": ["outdated", "discontinued"] } } - ] - }, - "$nor": { - "type": "array", - "items": { "$ref": "#" }, - "description": "Joins query clauses with a logical NOR.", - "examples": [ - [{ "price": 1.99 }, { "qty": { "$lt": 20 } }], - [{ "status": "A" }, { "onSale": true }], - [{ "rating": { "$gte": 4.5 } }, { "reviews": { "$gt": 100 } }] - ] - } - }, - "definitions": { - "operatorObject": { - "type": "object", - "properties": { - "$eq": { - "description": "Matches values that are equal to a specified value.", - "examples": ["active", 100, true] - }, - "$ne": { - "description": "Matches all values that are not equal to a specified value.", - "examples": ["inactive", 0, false] - }, - "$gt": { - "description": "Matches values that are greater than a specified value.", - "examples": [10, 100] - }, - "$gte": { - "description": "Matches values that are greater than or equal to a specified value.", - "examples": [5, 50] - }, - "$lt": { - "description": "Matches values that are less than a specified value.", - "examples": [20, 80] - }, - "$lte": { - "description": "Matches values that are less than or equal to a specified value.", - "examples": [15, 75] - }, - "$in": { - "type": "array", - "description": "Matches any of the values specified in an array.", - "examples": [ - ["red", "green", "blue"], - [1, 2, 3], - ["small", "medium", "large"] - ] - }, - "$nin": { - "type": "array", - "description": "Matches none of the values specified in an array.", - "examples": [ - ["yellow", "purple"], - [4, 5, 6], - ["extra-large", "xxl"] - ] - }, - "$exists": { - "type": "boolean", - "description": "Matches documents that have the specified field.", - "examples": [true, false] - }, - "$regex": { - "description": "Provides regular expression capabilities for pattern matching strings.", - "examples": ["^start", "end$", ".*pattern.*", "^[A-Z]{3}[0-9]{2}$"] - }, - "$size": { - "type": "integer", - "description": "Matches any array with the specified number of elements.", - "examples": [0, 5, 10] - }, - "$type": { - "description": "Matches values based on their BSON type.", - "examples": [1, "string", "object"] - }, - "$all": { - "type": "array", - "description": "Matches arrays that contain all elements specified in the query.", - "examples": [ - ["red", "blue"], - [10, 20], - ["feature1", "feature2"] - ] - }, - "$elemMatch": { - "type": "object", - "description": "Matches documents that contain an array field with at least one element that matches the specified query criteria.", - "examples": [ - { "score": { "$gt": 80 } }, - { "dimensions": { "$lt": 50, "$gt": 20 } }, - { "attributes": { "color": "red", "size": "M" } } - ] - } - }, - "additionalProperties": false, - "description": "An object containing MongoDB query operators and their corresponding values.", - "minProperties": 1, - "examples": [ - { "$gt": 10 }, - { "$lt": 100 }, - { "$gte": 5 }, - { "$lte": 50 }, - { "$in": ["value1", "value2"] }, - { "$gt": 10, "$lt": 20 }, - { "$exists": true }, - { "$regex": "^[a-z]+$" }, - { "$in": [1, 2, 3], "$nin": [4, 5, 6] }, - { "$elemMatch": { "score": { "$gte": 80 } } } - ] - } - }, - "description": "Generic schema for MongoDB find query filters without knowledge of specific fields." -} diff --git a/src/utils/json/mongo/autocomplete/generateMongoFindJsonSchema.ts b/src/utils/json/mongo/autocomplete/generateMongoFindJsonSchema.ts deleted file mode 100644 index 0f0fa7bbe..000000000 --- a/src/utils/json/mongo/autocomplete/generateMongoFindJsonSchema.ts +++ /dev/null @@ -1,270 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type FieldEntry } from './getKnownFields'; - -/** - * Generates a JSON schema for MongoDB find filter queries. - * - * This function is a short-term solution for providing autocompletion for MongoDB find filter queries. - * A MongoDB find filter query is a JSON document that can range from simple to complex structures. - * Basic autocompletion can be provided using a modified JSON schema, which is what we've done here. - * - * The long-term plan is to provide a more sophisticated auto-completion using, for example, - * the suggestion API that Monaco provides. This will be looked at in the future. - * - * @param fieldEntries - An array of field entries where each entry contains: - * - path: A string representing the full path of the field in the dataset (e.g., "age", "address.city"). - * - type: The most common or expected data type for that field (e.g., "number", "string"). - * - * The data provided is supposed to contain all known data paths from the expected dataset, - * focusing only on leaf nodes. - * - * The returned JSON schema can be directly added to the Monaco editor to activate autocompletion. - * - * @returns A JSON schema object that can be used for autocompletion in the Monaco editor. - */ -export function generateMongoFindJsonSchema(fieldEntries: FieldEntry[]) { - // Initialize the base schema object - const schema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'mongodb-filter-schema', - title: 'MongoDB Find Filter Schema', - type: 'object', - properties: {}, - additionalProperties: { - oneOf: [ - { - title: 'Direct Value', - description: 'A direct value for equality matching on an unknown field.', - examples: ['value', 123, true, null], - }, - { - title: 'Operator-Based Query', - $ref: '#/definitions/operatorObjectUnknown', - examples: [{ $ne: 'inactive' }, { $exists: true }], - }, - ], - }, - definitions: { - operatorObject: { - type: 'object', - properties: { - $eq: { - description: 'Matches values that are equal to a specified value.', - examples: [21, 'active', true], - }, - $ne: { - description: 'Matches all values that are not equal to a specified value.', - examples: [30, 'inactive', false], - }, - $gt: { - description: 'Matches values that are greater than a specified value.', - examples: [25, 100], - }, - $gte: { - description: 'Matches values that are greater than or equal to a specified value.', - examples: [18, 50], - }, - $lt: { - description: 'Matches values that are less than a specified value.', - examples: [65, 100], - }, - $lte: { - description: 'Matches values that are less than or equal to a specified value.', - examples: [30, 75], - }, - $in: { - type: 'array', - description: 'Matches any of the values specified in an array.', - examples: [ - ['red', 'blue'], - [21, 30, 40], - ], - }, - $nin: { - type: 'array', - description: 'Matches none of the values specified in an array.', - examples: [['green'], [50, 60]], - }, - $exists: { - type: 'boolean', - description: 'Matches documents that have the specified field.', - examples: [true, false], - }, - $regex: { - description: 'Provides regular expression capabilities for pattern matching strings.', - examples: ['^re', '.*blue$', '^[A-Z]+'], - }, - }, - additionalProperties: false, - description: 'An object containing a MongoDB query operator and its corresponding value.', - minProperties: 1, - }, - operatorObjectUnknown: { - $ref: '#/definitions/operatorObject', - }, - }, - description: - 'Schema for MongoDB find query filters, supporting known fields with various operators for querying documents.', - }; - - // Set to collect all full paths - const fullPathsSet = new Set(); - - // Function to generate examples based on type - function generateExamples(type: string): unknown[] { - let examples; - if (type === 'number') { - examples = [42, 100]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - examples.push(false); // odd type - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - examples.push(null); - } else if (type === 'string') { - examples = ['red', 'blue']; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - examples.push(null); - } else if (type === 'boolean') { - examples = [true, false]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - examples.push(null); - } else { - examples = ['value', 123, true, null]; - } - return examples as []; - } - - // Function to generate examples for operator-based queries - function generateOperatorExamples(type: string): unknown[] { - let examples; - if (type === 'number') { - examples = [{ $gt: 25 }, { $in: [20, 30, 40] }]; - } else if (type === 'string') { - examples = [{ $regex: '^re' }, { $ne: 'blue' }]; - } else if (type === 'boolean') { - examples = [{ $eq: true }, { $ne: false }]; - } else { - examples = [{ $exists: true }]; - } - return examples as []; - } - - // Function to create nested properties based on path components - function createNestedProperty( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - obj: any, - pathComponents: string[], - type: string, - currentPath: string = '', - ) { - const fieldName = pathComponents[0]; - const newPath = currentPath ? `${currentPath}.${fieldName}` : fieldName; - - fullPathsSet.add(newPath); - - if (pathComponents.length === 1) { - // Leaf node - const examples = generateExamples(type); - const operatorExamples = generateOperatorExamples(type); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - obj[fieldName] = { - oneOf: [ - { - title: 'Direct Value', - description: `A direct value for equality matching on the '${fieldName}' field.`, - examples: examples, - }, - { - title: 'Operator-Based Query', - $ref: '#/definitions/operatorObject', - examples: operatorExamples, - }, - ], - }; - } else { - // Nested object - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!obj[fieldName]) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - obj[fieldName] = { - type: 'object', - properties: {}, - additionalProperties: false, - description: `Embedded '${fieldName}' object containing fields.`, - }; - } - createNestedProperty( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - obj[fieldName]['properties'], - pathComponents.slice(1), - type, - newPath, - ); - } - } - - // Process each fieldEntry - for (const fieldEntry of fieldEntries) { - const pathComponents = fieldEntry.path.split('.'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - createNestedProperty(schema['properties'], pathComponents, fieldEntry.type); - } - - // Function to get type for a full path - function getTypeForFullPath(fullPath: string): string | undefined { - for (const fieldEntry of fieldEntries) { - if (fieldEntry.path === fullPath) { - return fieldEntry.type; - } - } - return undefined; - } - - // Create properties with full paths at the root level - for (const fullPath of fullPathsSet) { - const type = getTypeForFullPath(fullPath) || 'string'; - const examples = generateExamples(type); - const operatorExamples = generateOperatorExamples(type); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - schema['properties'][fullPath] = { - oneOf: [ - { - title: 'Direct Value', - description: `A direct value for equality matching on the '${fullPath}' field.`, - examples: examples, - }, - { - title: 'Operator-Based Query', - $ref: '#/definitions/operatorObject', - examples: operatorExamples, - }, - ], - }; - } - - // Add logical operators - const logicalOperators = ['$or', '$and', '$not', '$nor']; - for (const operator of logicalOperators) { - if (operator === '$not') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - schema['properties'][operator] = { - oneOf: [{ $ref: '#' }], - description: `Inverts the effect of a query expression.`, - }; - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - schema['properties'][operator] = { - type: 'array', - items: { $ref: '#' }, - description: `Joins query clauses with a logical ${operator.toUpperCase().substring(1)}.`, - }; - } - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return schema; -} diff --git a/src/utils/json/mongo/autocomplete/getKnownFields.ts b/src/utils/json/mongo/autocomplete/getKnownFields.ts deleted file mode 100644 index a82277a73..000000000 --- a/src/utils/json/mongo/autocomplete/getKnownFields.ts +++ /dev/null @@ -1,95 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import Denque from 'denque'; -import { type JSONSchema } from '../../JSONSchema'; - -export interface FieldEntry { - path: string; - type: string; -} - -/** - * This function traverses our JSON Schema object and collects all leaf property paths - * along with their most common data types. - * - * This information is needed for auto-completion support - * - * The approach is as follows: - * - Initialize a queue with the root properties of the schema to perform a breadth-first traversal. - * - While the queue is not empty: - * - Dequeue the next item, which includes the current schema node and its path. - * - Determine the most common type for the current node by looking at the 'x-typeOccurrence' field. - * - If the most common type is an object with properties: - * - Enqueue its child properties with their updated paths into the queue for further traversal. - * - Else if the most common type is a leaf type (e.g., string, number, boolean): - * - Add the current path and type to the result array as it represents a leaf property. - * - Continue this process until all nodes have been processed. - * - Return the result array containing objects with 'path' and 'type' for each leaf property. - */ -export function getKnownFields(schema: JSONSchema): FieldEntry[] { - const result: Array<{ path: string; type: string }> = []; - type QueueItem = { - path: string; - schemaNode: JSONSchema; - }; - - const queue: Denque = new Denque(); - - // Initialize the queue with root properties - if (schema.properties) { - for (const propName of Object.keys(schema.properties)) { - const propSchema = schema.properties[propName] as JSONSchema; - queue.push({ path: propName, schemaNode: propSchema }); - } - } - - while (queue.length > 0) { - const item = queue.shift(); - if (!item) continue; - - const { path, schemaNode } = item; - const mostCommonTypeEntry = getMostCommonTypeEntry(schemaNode); - - if (mostCommonTypeEntry) { - if (mostCommonTypeEntry.type === 'object' && mostCommonTypeEntry.properties) { - // Not a leaf node, enqueue its properties - for (const childName of Object.keys(mostCommonTypeEntry.properties)) { - const childSchema = mostCommonTypeEntry.properties[childName] as JSONSchema; - queue.push({ path: `${path}.${childName}`, schemaNode: childSchema }); - } - } else { - // Leaf node, add to result - result.push({ path: path, type: mostCommonTypeEntry.type as string }); - } - } - } - - return result; -} - -/** - * Helper function to get the most common type entry from a schema node. - * It looks for the 'anyOf' array and selects the type with the highest 'x-typeOccurrence'. - */ -function getMostCommonTypeEntry(schemaNode: JSONSchema): JSONSchema | null { - if (schemaNode.anyOf && schemaNode.anyOf.length > 0) { - let maxOccurrence = -1; - let mostCommonTypeEntry: JSONSchema | null = null; - - for (const typeEntry of schemaNode.anyOf as JSONSchema[]) { - const occurrence = typeEntry['x-typeOccurrence'] || 0; - if (occurrence > maxOccurrence) { - maxOccurrence = occurrence; - mostCommonTypeEntry = typeEntry; - } - } - return mostCommonTypeEntry; - } else if (schemaNode.type) { - // If 'anyOf' is not present, use the 'type' field directly - return schemaNode; - } - return null; -} diff --git a/src/utils/slickgrid/mongo/toSlickGridTable.test.ts b/src/utils/slickgrid/mongo/toSlickGridTable.test.ts index 69156bf1b..4b1d0af3f 100644 --- a/src/utils/slickgrid/mongo/toSlickGridTable.test.ts +++ b/src/utils/slickgrid/mongo/toSlickGridTable.test.ts @@ -76,7 +76,6 @@ describe('toSlickGridTable', () => { it('at a nested level', () => { const tableData = getDataAtPath(mongoDocuments, ['nestedDocument']); - console.log(tableData); expect(tableData).toHaveLength(5); expect(tableData[0]['key']).toBeDefined(); diff --git a/src/utils/slickgrid/mongo/toSlickGridTable.ts b/src/utils/slickgrid/mongo/toSlickGridTable.ts index 737fcb7c0..5deb51fe0 100644 --- a/src/utils/slickgrid/mongo/toSlickGridTable.ts +++ b/src/utils/slickgrid/mongo/toSlickGridTable.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BSONTypes, valueToDisplayString } from '@vscode-documentdb/schema-analyzer'; import { EJSON } from 'bson'; import { type Document, type WithId } from 'mongodb'; import { type TableDataEntry } from '../../../documentdb/ClusterSession'; -import { MongoBSONTypes } from '../../json/mongo/MongoBSONTypes'; -import { valueToDisplayString } from '../../json/mongo/MongoValueFormatters'; /** * Extracts data from a list of MongoDB documents at a specified path. @@ -45,8 +44,8 @@ export function getDataAtPath(documents: WithId[], path: string[]): Ta // we also make sure that the '_id' field is always included in the data! if (doc._id) { row['_id'] = { - value: valueToDisplayString(doc._id, MongoBSONTypes.inferType(doc._id)), - type: MongoBSONTypes.inferType(doc._id), + value: valueToDisplayString(doc._id, BSONTypes.inferType(doc._id)), + type: BSONTypes.inferType(doc._id), }; // TODO: problem here -> what if the user has a field with this name... row['x-objectid'] = EJSON.stringify(doc._id, { relaxed: false }); // this is crucial, we need to retain the _id field for future queries from the table view @@ -72,13 +71,13 @@ export function getDataAtPath(documents: WithId[], path: string[]): Ta continue; } else { const value: unknown = subdocument[key]; - const type: MongoBSONTypes = MongoBSONTypes.inferType(value); + const type: BSONTypes = BSONTypes.inferType(value); // eslint-disable-next-line if (value instanceof Array) { row[key] = { value: `array[${value.length}]`, - type: MongoBSONTypes.Array, + type: BSONTypes.Array, }; } else { row[key] = { value: valueToDisplayString(value, type), type: type }; diff --git a/src/utils/slickgrid/mongo/toSlickGridTree.ts b/src/utils/slickgrid/mongo/toSlickGridTree.ts index 9d3742cfe..849ad42b6 100644 --- a/src/utils/slickgrid/mongo/toSlickGridTree.ts +++ b/src/utils/slickgrid/mongo/toSlickGridTree.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BSONTypes, valueToDisplayString } from '@vscode-documentdb/schema-analyzer'; import { type Document, type ObjectId, type WithId } from 'mongodb'; -import { MongoBSONTypes } from '../../json/mongo/MongoBSONTypes'; -import { valueToDisplayString } from '../../json/mongo/MongoValueFormatters'; /** * The data structure for a single node entry in the tree data structure for SlickGrid. @@ -113,10 +112,10 @@ export function documentToSlickGridTree(document: WithId, idPrefix?: s continue; } - const dataType: MongoBSONTypes = MongoBSONTypes.inferType(stackEntry.value); + const dataType: BSONTypes = BSONTypes.inferType(stackEntry.value); switch (dataType) { - case MongoBSONTypes.Object: { + case BSONTypes.Object: { tree.push({ id: globalEntryId, field: `${stackEntry.key}`, @@ -131,7 +130,7 @@ export function documentToSlickGridTree(document: WithId, idPrefix?: s }); break; } - case MongoBSONTypes.Array: { + case BSONTypes.Array: { const value = stackEntry.value as unknown[]; tree.push({ @@ -157,7 +156,7 @@ export function documentToSlickGridTree(document: WithId, idPrefix?: s id: globalEntryId, field: `${stackEntry.key}`, value: valueToDisplayString(stackEntry.value, dataType), - type: MongoBSONTypes.toDisplayString(MongoBSONTypes.inferType(stackEntry.value)), + type: BSONTypes.toDisplayString(BSONTypes.inferType(stackEntry.value)), parentId: stackEntry.parentId, }); break; diff --git a/src/webviews/components/MonacoAutoHeight.tsx b/src/webviews/components/MonacoAutoHeight.tsx index 59980d36d..824659a22 100644 --- a/src/webviews/components/MonacoAutoHeight.tsx +++ b/src/webviews/components/MonacoAutoHeight.tsx @@ -179,14 +179,21 @@ export const MonacoAutoHeight = (props: MonacoAutoHeightProps) => { /** * Configures the Tab key behavior for the Monaco editor. * - * When called, this function sets up or removes a keydown handler for the Tab key. - * If `shouldTrap` is true, Tab/Shift+Tab are trapped within the editor (focus remains in editor). - * If `shouldTrap` is false, Tab/Shift+Tab move focus to the next/previous focusable element outside the editor. + * When `shouldTrap` is true, Tab/Shift+Tab are trapped within the editor + * (default Monaco behavior for code indentation). + * + * When `shouldTrap` is false, Tab/Shift+Tab move focus to the next/previous + * focusable element outside the editor โ€” UNLESS the editor is in snippet + * tab-stop mode (`inSnippetMode`), in which case Tab navigates between + * snippet placeholders. After the snippet session ends (final tab stop or + * ESC), Tab reverts to moving focus out of the editor. + * + * Uses `editor.addAction` with a precondition context key expression + * (`!inSnippetMode`) rather than `onKeyDown` interception, so Monaco's + * built-in snippet navigation takes priority when a snippet is active. * * @param {monacoEditor.editor.IStandaloneCodeEditor} editor - The Monaco editor instance. * @param {boolean} shouldTrap - Whether to trap Tab key in the editor. - * - true: Tab/Shift+Tab are trapped in the editor. - * - false: Tab/Shift+Tab move focus to next/previous element. */ const configureTabKeyMode = (editor: monacoEditor.editor.IStandaloneCodeEditor, shouldTrap: boolean) => { if (tabKeyDisposerRef.current) { @@ -198,17 +205,30 @@ export const MonacoAutoHeight = (props: MonacoAutoHeightProps) => { return; } - tabKeyDisposerRef.current = editor.onKeyDown((event) => { - if (event.keyCode !== monacoEditor.KeyCode.Tab) { - return; - } - - event.preventDefault(); - event.stopPropagation(); + // Register Tab and Shift+Tab actions that only fire when NOT in snippet mode. + // When inSnippetMode is true, Monaco's built-in snippet Tab handler takes over. + const tabAction = editor.addAction({ + id: 'documentdb.tab.moveFocusNext', + label: 'Move Focus to Next Element', + keybindings: [monacoEditor.KeyCode.Tab], + precondition: '!inSnippetMode', + run: () => moveFocus(editor, 'next'), + }); - const direction = event.browserEvent.shiftKey ? 'previous' : 'next'; - moveFocus(editor, direction); + const shiftTabAction = editor.addAction({ + id: 'documentdb.tab.moveFocusPrevious', + label: 'Move Focus to Previous Element', + keybindings: [monacoEditor.KeyMod.Shift | monacoEditor.KeyCode.Tab], + precondition: '!inSnippetMode', + run: () => moveFocus(editor, 'previous'), }); + + tabKeyDisposerRef.current = { + dispose: () => { + tabAction.dispose(); + shiftTabAction.dispose(); + }, + }; }; // Default escape handler: move focus to next element (like Tab) diff --git a/src/webviews/components/MonacoEditor.tsx b/src/webviews/components/MonacoEditor.tsx index c08e2087d..7e6f84530 100644 --- a/src/webviews/components/MonacoEditor.tsx +++ b/src/webviews/components/MonacoEditor.tsx @@ -75,11 +75,20 @@ export const MonacoEditor = ({ onEscapeEditor, onMount, ...props }: MonacoEditor disposablesRef.current.forEach((d) => d.dispose()); disposablesRef.current = []; - // Register Escape key handler to exit the editor + // Register Escape key handler to exit the editor. + // The context expression ensures ESC is only handled when: + // - The suggest (autocomplete) widget is NOT visible + // - The editor is NOT in snippet tab-stop mode + // This allows Monaco's built-in handlers to dismiss the suggest + // widget or exit snippet mode first, before our handler fires. if (onEscapeEditor) { - editor.addCommand(monacoInstance.KeyCode.Escape, () => { - onEscapeEditor(); - }); + editor.addCommand( + monacoInstance.KeyCode.Escape, + () => { + onEscapeEditor(); + }, + '!suggestWidgetVisible && !inSnippetMode', + ); } // Announce escape hint once when editor gains focus diff --git a/src/webviews/documentdb/collectionView/CollectionView.tsx b/src/webviews/documentdb/collectionView/CollectionView.tsx index 2ff56aa2c..ea1f3330d 100644 --- a/src/webviews/documentdb/collectionView/CollectionView.tsx +++ b/src/webviews/documentdb/collectionView/CollectionView.tsx @@ -12,6 +12,7 @@ import { Announcer } from '../../api/webview-client/accessibility'; import { useConfiguration } from '../../api/webview-client/useConfiguration'; import { useTrpcClient } from '../../api/webview-client/useTrpcClient'; import { useSelectiveContextMenuPrevention } from '../../api/webview-client/utils/useSelectiveContextMenuPrevention'; +import { setCompletionContext } from '../../documentdbQuery'; import './collectionView.scss'; import { CollectionViewContext, @@ -265,17 +266,24 @@ export const CollectionView = (): JSX.Element => { } function updateAutoCompletionData(): void { - trpcClient.mongoClusters.collectionView.getAutocompletionSchema + trpcClient.mongoClusters.collectionView.getFieldCompletionData .query() - .then(async (schema) => { - void (await currentContextRef.current.queryEditor?.setJsonSchema(schema)); + .then((fields) => { + setCompletionContext(configuration.sessionId, { fields }); }) .catch((error) => { - void trpcClient.common.displayErrorMessage.mutate({ - message: l10n.t('Error while loading the autocompletion data'), - modal: false, - cause: error instanceof Error ? error.message : String(error), - }); + console.debug('Failed to update field completion data:', error); + // Non-blocking โ€” completion will work without fields + trpcClient.common.reportEvent + .mutate({ + eventName: 'fieldCompletionDataFetchFailed', + properties: { + error: error instanceof Error ? error.message : String(error), + }, + }) + .catch(() => { + // best-effort telemetry, swallow errors + }); }); } diff --git a/src/webviews/documentdb/collectionView/collectionViewContext.ts b/src/webviews/documentdb/collectionView/collectionViewContext.ts index 435396ce6..e3a64fa63 100644 --- a/src/webviews/documentdb/collectionView/collectionViewContext.ts +++ b/src/webviews/documentdb/collectionView/collectionViewContext.ts @@ -97,7 +97,6 @@ export type CollectionViewContextType = { skip: number; limit: number; }; - setJsonSchema(schema: object): Promise; //monacoEditor.languages.json.DiagnosticsOptions, but we don't want to import monacoEditor here }; isAiRowVisible: boolean; // Controls visibility of the AI prompt row in QueryEditor queryInsights: QueryInsightsState; // Query insights state for progressive loading diff --git a/src/webviews/documentdb/collectionView/collectionViewRouter.ts b/src/webviews/documentdb/collectionView/collectionViewRouter.ts index fec8eb05d..8e00cfa70 100644 --- a/src/webviews/documentdb/collectionView/collectionViewRouter.ts +++ b/src/webviews/documentdb/collectionView/collectionViewRouter.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type FieldEntry } from '@vscode-documentdb/schema-analyzer'; import * as fs from 'fs'; import { type Document } from 'mongodb'; import * as path from 'path'; import * as vscode from 'vscode'; -import { type JSONSchema } from 'vscode-json-languageservice'; import { z } from 'zod'; import { ClusterSession } from '../../../documentdb/ClusterSession'; import { getConfirmationAsInSettings } from '../../../utils/dialogs/getConfirmation'; -import { getKnownFields, type FieldEntry } from '../../../utils/json/mongo/autocomplete/getKnownFields'; import { publicProcedureWithTelemetry, router, type WithTelemetry } from '../../api/extension-server/trpc'; import * as l10n from '@vscode/l10n'; @@ -39,9 +38,7 @@ import { Views } from '../../../documentdb/Views'; import { ext } from '../../../extensionVariables'; import { QueryInsightsAIService } from '../../../services/ai/QueryInsightsAIService'; import { type CollectionItem } from '../../../tree/documentdb/CollectionItem'; -// eslint-disable-next-line import/no-internal-modules -import basicFindQuerySchema from '../../../utils/json/mongo/autocomplete/basicMongoFindFilterSchema.json'; -import { generateMongoFindJsonSchema } from '../../../utils/json/mongo/autocomplete/generateMongoFindJsonSchema'; +import { toFieldCompletionItems } from '../../../utils/json/data-api/autocomplete/toFieldCompletionItems'; import { promptAfterActionEventually } from '../../../utils/survey'; import { UsageImpact } from '../../../utils/surveyTypes'; import { type BaseRouterContext } from '../../api/configuration/appRouter'; @@ -234,25 +231,16 @@ export const collectionsViewRouter = router({ return { documentCount: size }; }), - getAutocompletionSchema: publicProcedureWithTelemetry + getFieldCompletionData: publicProcedureWithTelemetry // procedure type .query(({ ctx }) => { const myCtx = ctx as WithTelemetry; const session: ClusterSession = ClusterSession.getSession(myCtx.sessionId); - const _currentJsonSchema = session.getCurrentSchema(); - const autoCompletionData: FieldEntry[] = getKnownFields(_currentJsonSchema); + const fieldEntries: FieldEntry[] = session.getKnownFields(); - let querySchema: JSONSchema; - - if (autoCompletionData.length > 0) { - querySchema = generateMongoFindJsonSchema(autoCompletionData); - } else { - querySchema = basicFindQuerySchema; - } - - return querySchema; + return toFieldCompletionItems(fieldEntries); }), getCurrentPageAsTable: publicProcedureWithTelemetry // parameters diff --git a/src/webviews/documentdb/collectionView/components/queryEditor/QueryEditor.tsx b/src/webviews/documentdb/collectionView/components/queryEditor/QueryEditor.tsx index 7b6d7e8ec..8bfaabc77 100644 --- a/src/webviews/documentdb/collectionView/components/queryEditor/QueryEditor.tsx +++ b/src/webviews/documentdb/collectionView/components/queryEditor/QueryEditor.tsx @@ -10,9 +10,16 @@ import { useContext, useEffect, useRef, useState, type JSX } from 'react'; import { InputWithProgress } from '../../../../components/InputWithProgress'; // eslint-disable-next-line import/no-internal-modules import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -// eslint-disable-next-line import/no-internal-modules -import basicFindQuerySchema from '../../../../../utils/json/mongo/autocomplete/basicMongoFindFilterSchema.json'; import { useConfiguration } from '../../../../api/webview-client/useConfiguration'; +import { + buildEditorUri, + clearCompletionContext, + EditorType, + LANGUAGE_ID, + registerDocumentDBQueryLanguage, + validateExpression, + type Diagnostic, +} from '../../../../documentdbQuery'; import { type CollectionViewWebviewConfigurationType } from '../../collectionViewController'; import { ArrowResetRegular, SendRegular, SettingsFilled, SettingsRegular } from '@fluentui/react-icons'; @@ -24,6 +31,31 @@ import { CollectionViewContext } from '../../collectionViewContext'; import { useHideScrollbarsDuringResize } from '../../hooks/useHideScrollbarsDuringResize'; import './queryEditor.scss'; +/** + * Convert a Diagnostic from the documentdb-query validator to a Monaco marker. + */ +function toMonacoMarker( + diagnostic: Diagnostic, + model: monacoEditor.editor.ITextModel, + monaco: typeof monacoEditor, +): monacoEditor.editor.IMarkerData { + const startPos = model.getPositionAt(diagnostic.startOffset); + const endPos = model.getPositionAt(diagnostic.endOffset); + return { + severity: + diagnostic.severity === 'error' + ? monaco.MarkerSeverity.Error + : diagnostic.severity === 'warning' + ? monaco.MarkerSeverity.Warning + : monaco.MarkerSeverity.Info, + message: diagnostic.message, + startLineNumber: startPos.lineNumber, + startColumn: startPos.column, + endLineNumber: endPos.lineNumber, + endColumn: endPos.column, + }; +} + interface QueryEditorProps { onExecuteRequest: () => void; } @@ -46,7 +78,6 @@ export const QueryEditor = ({ onExecuteRequest }: QueryEditorProps): JSX.Element // AI prompt history (survives hide/show of AI input) const [aiPromptHistory, setAiPromptHistory] = useState([]); - const schemaAbortControllerRef = useRef(null); const aiGenerationAbortControllerRef = useRef(null); const aiInputRef = useRef(null); @@ -57,12 +88,162 @@ export const QueryEditor = ({ onExecuteRequest }: QueryEditorProps): JSX.Element const hideScrollbarsTemporarily = useHideScrollbarsDuringResize(); - const handleEditorDidMount = (editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor) => { - editor.setValue('{ }'); + /** + * Creates a Monaco model with a URI scheme for the given editor type. + * This enables the completion provider to identify which editor the request is for. + */ + const createEditorModel = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + monaco: typeof monacoEditor, + editorType: EditorType, + initialValue: string, + ): monacoEditor.editor.ITextModel => { + const uri = monaco.Uri.parse(buildEditorUri(editorType, configuration.sessionId)); + let model = monaco.editor.getModel(uri); + if (!model) { + model = monaco.editor.createModel(initialValue, LANGUAGE_ID, uri); + } + editor.setModel(model); + return model; + }; + + /** + * Sets up debounced validation on editor content changes. + * Returns a cleanup function to clear any pending timeout. + */ + const setupValidation = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + monaco: typeof monacoEditor, + model: monacoEditor.editor.ITextModel, + ): (() => void) => { + let validationTimeout: ReturnType; + const disposable = editor.onDidChangeModelContent(() => { + clearTimeout(validationTimeout); + validationTimeout = setTimeout(() => { + const diagnostics = validateExpression(editor.getValue()); + const markers = diagnostics.map((d) => toMonacoMarker(d, model, monaco)); + monaco.editor.setModelMarkers(model, 'documentdb-query', markers); + }, 300); + }); + return () => { + clearTimeout(validationTimeout); + disposable.dispose(); + }; + }; + + /** + * Cancels any active snippet session on the given editor. + * + * After a snippet completion (e.g., `fieldName: $1`), Monaco keeps the + * snippet session alive and highlights the tab-stop placeholder. If the + * user continues typing, the highlight grows โ€” the "ghost selection" + * bug. Calling this function ends the snippet session cleanly. + */ + const cancelSnippetSession = (editor: monacoEditor.editor.IStandaloneCodeEditor): void => { + const controller = editor.getContribution('snippetController2') as { cancel: () => void } | null | undefined; + controller?.cancel(); + }; + + /** Characters that signal the end of a field-value pair and should exit snippet mode. */ + const SNIPPET_EXIT_CHARS = new Set([',', '}', ']']); + + /** + * Sets up pattern-based auto-trigger of completions. + * When a content change results in a trigger character followed by a + * space (`: `, `, `, `{ `, `[ `) at the end of the inserted text, + * completions are triggered automatically after a short delay. This + * handles both manual typing and completion acceptance. + * + * Also cancels any active snippet session when a delimiter character + * (`,`, `}`, `]`) is typed, preventing the "ghost selection" bug + * where the tab-stop highlight expands as the user continues typing. + * + * Returns a cleanup function. + */ + const setupSmartTrigger = (editor: monacoEditor.editor.IStandaloneCodeEditor): (() => void) => { + let triggerTimeout: ReturnType; + const contentDisposable = editor.onDidChangeModelContent((e) => { + clearTimeout(triggerTimeout); + + const change = e.changes[0]; + if (!change || change.text.length === 0) return; + + // Cancel snippet session when the user *types* a delimiter character. + // Only applies to single-character edits (user keystrokes), not to + // multi-character completion insertions which may legitimately + // contain commas or braces as part of the snippet text. + if (change.text.length === 1 && SNIPPET_EXIT_CHARS.has(change.text)) { + cancelSnippetSession(editor); + } + + const model = editor.getModel(); + if (!model) return; + + // Calculate the offset at the end of the inserted text in the new model + const endOffset = change.rangeOffset + change.text.length; + + // We need at least 2 chars to check for ": " or ", " + if (endOffset < 2) return; + + const fullText = model.getValue(); + const lastTwo = fullText.substring(endOffset - 2, endOffset); + if (lastTwo === ': ' || lastTwo === ', ' || lastTwo === '{ ' || lastTwo === '[ ') { + triggerTimeout = setTimeout(() => { + editor.trigger('smart-trigger', 'editor.action.triggerSuggest', {}); + }, 50); + } + }); + + // Cancel snippet session when the editor loses focus (Option D). + // If the user clicks away while a tab-stop is highlighted, the + // highlight should not persist when they return. + const blurDisposable = editor.onDidBlurEditorText(() => { + cancelSnippetSession(editor); + }); + + // Cancel snippet session on Enter or Ctrl+Enter / Cmd+Enter. + // Enter commits the current line and should exit snippet mode. + // Ctrl+Enter triggers query execution and should also exit snippet mode + // so the tab-stop highlight doesn't persist after running a query. + const keyDownDisposable = editor.onKeyDown((e) => { + if (e.browserEvent.key === 'Enter') { + cancelSnippetSession(editor); + } + }); + + return () => { + clearTimeout(triggerTimeout); + contentDisposable.dispose(); + blurDisposable.dispose(); + keyDownDisposable.dispose(); + }; + }; + // Track validation cleanup functions + const filterValidationCleanupRef = useRef<(() => void) | null>(null); + const projectValidationCleanupRef = useRef<(() => void) | null>(null); + const sortValidationCleanupRef = useRef<(() => void) | null>(null); + const filterSmartTriggerCleanupRef = useRef<(() => void) | null>(null); + const projectSmartTriggerCleanupRef = useRef<(() => void) | null>(null); + const sortSmartTriggerCleanupRef = useRef<(() => void) | null>(null); + const handleEditorDidMount = (editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor) => { // Store the filter editor reference filterEditorRef.current = editor; + // Register the documentdb-query language (idempotent โ€” safe to call on every mount). + // Pass the tRPC openUrl handler so hover links can be opened via the extension host, + // bypassing the webview sandbox's popup restrictions. + void registerDocumentDBQueryLanguage(monaco, (url) => void trpcClient.common.openUrl.mutate({ url })); + + // Create model with URI scheme for contextual completions + const model = createEditorModel(editor, monaco, EditorType.Filter, '{ }'); + + // Set up debounced validation + filterValidationCleanupRef.current = setupValidation(editor, monaco, model); + + // Set up smart-trigger for completions after ": " and ", " + filterSmartTriggerCleanupRef.current = setupSmartTrigger(editor); + const getCurrentQueryFunction = () => ({ filter: filterValue, project: projectValue, @@ -76,78 +257,8 @@ export const QueryEditor = ({ onExecuteRequest }: QueryEditorProps): JSX.Element ...prev, queryEditor: { getCurrentQuery: getCurrentQueryFunction, - /** - * Dynamically sets the JSON schema for the Monaco editor's validation and autocompletion. - * - * NOTE: This function can encounter network errors if called immediately after the - * editor mounts, as the underlying JSON web worker may not have finished loading. - * To mitigate this, a delay is introduced before attempting to set the schema. - * - * A more robust long-term solution should be implemented to programmatically - * verify that the JSON worker is initialized before this function proceeds. - * - * An AbortController is used to prevent race conditions when this function is - * called in quick succession (e.g., rapid "refresh" clicks). It ensures that - * any pending schema update is cancelled before a new one begins, guaranteeing - * a clean, predictable state and allowing the Monaco worker to initialize correctly. - */ - setJsonSchema: async (schema) => { - // Use the ref to cancel the previous operation - if (schemaAbortControllerRef.current) { - schemaAbortControllerRef.current.abort(); - } - - // Create and store the new AbortController in the ref - const abortController = new AbortController(); - schemaAbortControllerRef.current = abortController; - const signal = abortController.signal; - - try { - // Wait for 2 seconds to give the worker time to initialize - await new Promise((resolve) => setTimeout(resolve, 2000)); - - // If the operation was cancelled during the delay, abort early - if (signal.aborted) { - return; - } - - // Check if JSON language features are available and set the schema - if (monaco.languages.json?.jsonDefaults) { - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: false, - schemas: [ - { - uri: 'mongodb-filter-query-schema.json', - fileMatch: ['*'], - schema: schema, - }, - ], - }); - } - } catch (error) { - // The error is likely an uncaught exception in the worker, - // but we catch here just in case. - console.warn('Error setting JSON schema:', error); - } - }, }, })); - - // initialize the monaco editor with the schema that's basic - // as we don't know the schema of the collection available - // this is a fallback for the case when the autocompletion feature fails. - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: true, - schemas: [ - { - uri: 'mongodb-filter-query-schema.json', // Unique identifier - fileMatch: ['*'], // Apply to all JSON files or specify as needed - - schema: basicFindQuerySchema, - // schema: generateMongoFindJsonSchema(fieldEntries) - }, - ], - }); }; const monacoOptions: editor.IStandaloneEditorConstructionOptions = { @@ -173,19 +284,58 @@ export const QueryEditor = ({ onExecuteRequest }: QueryEditorProps): JSX.Element automaticLayout: false, }; + // Intercept link clicks in Monaco hover tooltips. + // Monaco renders hover markdown links as tags, but the webview CSP + // blocks direct navigation. Capture clicks and route through tRPC. + const editorContainerRef = useRef(null); + useEffect(() => { + const container = editorContainerRef.current; + if (!container) return; + + const handleLinkClick = (e: MouseEvent): void => { + const target = e.target as HTMLElement; + const anchor = target.closest('a'); + if (!anchor) return; + + const href = anchor.getAttribute('href'); + if (href && (href.startsWith('https://') || href.startsWith('http://'))) { + e.preventDefault(); + e.stopPropagation(); + void trpcClient.common.openUrl.mutate({ url: href }); + } + }; + + container.addEventListener('click', handleLinkClick, true); + return () => container.removeEventListener('click', handleLinkClick, true); + }, [trpcClient]); + // Cleanup any pending operations when component unmounts useEffect(() => { return () => { - if (schemaAbortControllerRef.current) { - schemaAbortControllerRef.current.abort(); - schemaAbortControllerRef.current = null; - } if (aiGenerationAbortControllerRef.current) { aiGenerationAbortControllerRef.current.abort(); aiGenerationAbortControllerRef.current = null; } + + // Clean up validation timeouts + filterValidationCleanupRef.current?.(); + projectValidationCleanupRef.current?.(); + sortValidationCleanupRef.current?.(); + + // Clean up smart-trigger listeners + filterSmartTriggerCleanupRef.current?.(); + projectSmartTriggerCleanupRef.current?.(); + sortSmartTriggerCleanupRef.current?.(); + + // Dispose Monaco models + filterEditorRef.current?.getModel()?.dispose(); + projectEditorRef.current?.getModel()?.dispose(); + sortEditorRef.current?.getModel()?.dispose(); + + // Clear completion store for this session + clearCompletionContext(configuration.sessionId); }; - }, []); + }, [configuration.sessionId]); // Update getCurrentQuery function whenever state changes useEffect(() => { @@ -342,7 +492,7 @@ export const QueryEditor = ({ onExecuteRequest }: QueryEditorProps): JSX.Element }; return ( -
+
{/* Optional AI prompt row */}
@@ -397,7 +547,7 @@ export const QueryEditor = ({ onExecuteRequest }: QueryEditorProps): JSX.Element { handleEditorDidMount(editor, monaco); - // Sync initial value + // Sync editor content to state editor.onDidChangeModelContent(() => { setFilterValue(editor.getValue()); }); }} options={{ ...monacoOptions, - ariaLabel: l10n.t('Filter: Enter the DocumentDB query filter in JSON format'), + ariaLabel: l10n.t('Filter: Enter the DocumentDB query filter'), }} />
@@ -508,16 +658,31 @@ export const QueryEditor = ({ onExecuteRequest }: QueryEditorProps): JSX.Element { + onMount={(editor, monaco) => { + // Register language (idempotent) + void registerDocumentDBQueryLanguage( + monaco, + (url) => void trpcClient.common.openUrl.mutate({ url }), + ); + projectEditorRef.current = editor; - editor.setValue(projectValue); + + // Create model with URI scheme for project completions + const model = createEditorModel(editor, monaco, EditorType.Project, projectValue); + + // Set up validation + projectValidationCleanupRef.current = setupValidation(editor, monaco, model); + + // Set up smart-trigger + projectSmartTriggerCleanupRef.current = setupSmartTrigger(editor); + editor.onDidChangeModelContent(() => { setProjectValue(editor.getValue()); }); @@ -539,16 +704,31 @@ export const QueryEditor = ({ onExecuteRequest }: QueryEditorProps): JSX.Element { + onMount={(editor, monaco) => { + // Register language (idempotent) + void registerDocumentDBQueryLanguage( + monaco, + (url) => void trpcClient.common.openUrl.mutate({ url }), + ); + sortEditorRef.current = editor; - editor.setValue(sortValue); + + // Create model with URI scheme for sort completions + const model = createEditorModel(editor, monaco, EditorType.Sort, sortValue); + + // Set up validation + sortValidationCleanupRef.current = setupValidation(editor, monaco, model); + + // Set up smart-trigger + sortSmartTriggerCleanupRef.current = setupSmartTrigger(editor); + editor.onDidChangeModelContent(() => { setSortValue(editor.getValue()); }); diff --git a/src/webviews/documentdbQuery/completionStore.test.ts b/src/webviews/documentdbQuery/completionStore.test.ts new file mode 100644 index 000000000..fde71ed1b --- /dev/null +++ b/src/webviews/documentdbQuery/completionStore.test.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + clearAllCompletionContexts, + clearCompletionContext, + getCompletionContext, + setCompletionContext, +} from './completionStore'; + +describe('completionStore', () => { + beforeEach(() => { + clearAllCompletionContexts(); + }); + + test('setCompletionContext then getCompletionContext round-trips correctly', () => { + const context = { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + ], + }; + + setCompletionContext('session-1', context); + expect(getCompletionContext('session-1')).toEqual(context); + }); + + test('getCompletionContext returns undefined for unknown session', () => { + expect(getCompletionContext('unknown')).toBeUndefined(); + }); + + test('clearCompletionContext removes the entry', () => { + setCompletionContext('session-1', { fields: [] }); + expect(getCompletionContext('session-1')).toBeDefined(); + + clearCompletionContext('session-1'); + expect(getCompletionContext('session-1')).toBeUndefined(); + }); + + test('clearCompletionContext is a no-op for unknown session', () => { + expect(() => clearCompletionContext('unknown')).not.toThrow(); + }); + + test('clearAllCompletionContexts removes all entries', () => { + setCompletionContext('session-1', { fields: [] }); + setCompletionContext('session-2', { fields: [] }); + + clearAllCompletionContexts(); + + expect(getCompletionContext('session-1')).toBeUndefined(); + expect(getCompletionContext('session-2')).toBeUndefined(); + }); + + test('setCompletionContext overwrites existing data', () => { + const original = { + fields: [ + { + fieldName: 'old', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'old', + referenceText: '$old', + }, + ], + }; + const updated = { + fields: [ + { + fieldName: 'new', + displayType: 'Number', + bsonType: 'double', + isSparse: true, + insertText: 'new', + referenceText: '$new', + }, + ], + }; + + setCompletionContext('session-1', original); + setCompletionContext('session-1', updated); + + expect(getCompletionContext('session-1')).toEqual(updated); + }); + + test('multiple sessions are independent', () => { + const ctx1 = { + fields: [ + { + fieldName: 'a', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'a', + referenceText: '$a', + }, + ], + }; + const ctx2 = { + fields: [ + { + fieldName: 'b', + displayType: 'Number', + bsonType: 'int32', + isSparse: true, + insertText: 'b', + referenceText: '$b', + }, + ], + }; + + setCompletionContext('session-1', ctx1); + setCompletionContext('session-2', ctx2); + + expect(getCompletionContext('session-1')).toEqual(ctx1); + expect(getCompletionContext('session-2')).toEqual(ctx2); + }); +}); diff --git a/src/webviews/documentdbQuery/completionStore.ts b/src/webviews/documentdbQuery/completionStore.ts new file mode 100644 index 000000000..b97ed859d --- /dev/null +++ b/src/webviews/documentdbQuery/completionStore.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type FieldCompletionData } from '../../utils/json/data-api/autocomplete/toFieldCompletionItems'; + +/** + * Completion context for a single editor session. + * Holds dynamic field data fetched from the extension host after query execution. + */ +export interface CompletionContext { + fields: FieldCompletionData[]; +} + +const store = new Map(); + +/** Update field data for a session (called after query execution). */ +export function setCompletionContext(sessionId: string, context: CompletionContext): void { + store.set(sessionId, context); +} + +/** Get field data for a session. */ +export function getCompletionContext(sessionId: string): CompletionContext | undefined { + return store.get(sessionId); +} + +/** Remove a session's data (called on tab close / dispose). */ +export function clearCompletionContext(sessionId: string): void { + store.delete(sessionId); +} + +/** Clear all sessions (for testing). */ +export function clearAllCompletionContexts(): void { + store.clear(); +} diff --git a/src/webviews/documentdbQuery/completions/README.md b/src/webviews/documentdbQuery/completions/README.md new file mode 100644 index 000000000..bb060d369 --- /dev/null +++ b/src/webviews/documentdbQuery/completions/README.md @@ -0,0 +1,110 @@ +# Completions Module + +Context-sensitive completion items for the `documentdb-query` Monaco language. + +## Architecture + +``` +registerLanguage.ts + โ””โ”€ provideCompletionItems() + โ”‚ + โ”œโ”€ cursorContext.ts โ† detect semantic cursor position + โ”‚ + โ””โ”€ completions/ + โ”œโ”€ createCompletionItems.ts โ† main entry, context routing + โ”œโ”€ mapCompletionItems.ts โ† operator/field โ†’ CompletionItem + โ”œโ”€ typeSuggestions.ts โ† type-aware value suggestions + โ”œโ”€ snippetUtils.ts โ† snippet text manipulation + โ””โ”€ completionKnowledge.ts โ† curated domain rules & constants +``` + +### Flow + +1. Monaco calls `provideCompletionItems()` (registered in `registerLanguage.ts`) +2. `detectCursorContext()` scans backward from the cursor to determine the semantic position +3. `createCompletionItems()` routes to the appropriate builder: + - **key / array-element** โ†’ field names + key-position operators + - **value** โ†’ type suggestions + operators (with braces) + BSON constructors + JS globals + - **operator** โ†’ operators only (braces stripped, type-aware sorting) + - **empty** (unknown + needsWrapping) โ†’ key-position completions with `{ }` wrapping + - **unknown** (ambiguous, no wrapping) โ†’ all completions (fields, all operators, BSON constructors, JS globals) + +## Sorting + +Completion items use `sortText` prefixes so Monaco displays them in the intended order. Lower prefixes appear higher in the list. + +### Empty position (no braces) + +Same as key position. All insertions wrapped with `{ }`. + +| Prefix | Content | Example | +|--------|---------|---------| +| `0_fieldName` | Schema field names (wrapped) | `{ age: $1 }`, `{ name: $1 }` | +| `1_$and` | Key-position operators (with braces) | `{ $and: [...] }` | + +### Value position + +| Prefix | Content | Example | +|--------|---------|---------| +| `00_00` โ€“ `00_99` | Type suggestions | `true` / `false` for boolean fields | +| `0_$eq` โ€“ `2_$op` | Query operators (type-aware) | `{ $eq: โ€ฆ }`, `{ $gt: โ€ฆ }` | +| `3_ObjectId` | BSON constructors | `ObjectId(โ€ฆ)`, `ISODate(โ€ฆ)` | +| `4_Date` | JS globals | `Date`, `Math`, `RegExp`, `Infinity` | + +### Key position + +| Prefix | Content | Example | +|--------|---------|---------| +| `0_fieldName` | Schema field names | `age`, `name`, `_id` | +| `1_$and` | Key-position operators | `$and`, `$or`, `$nor` | + +### Operator position (type-aware) + +When the field's BSON type is known, operators are tiered by relevance: + +| Prefix | Tier | Meaning | +|--------|------|---------| +| `0_` | Type-relevant | Operator's `applicableBsonTypes` matches the field | +| `1a_` | Comparison (universal) | `$eq`, `$ne`, `$gt`, `$in`, etc. โ€” no type restriction, most commonly used | +| `1b_` | Other universal | Element/evaluation/geospatial operators with no type restriction | +| `2_` | Non-matching | Operator has type restrictions that don't match the field | + +Within each tier, operators sort alphabetically by name (`$eq` < `$gt` < `$in`). + +**Example โ€” boolean field `isActive`:** +- Tier `1a_`: `$eq`, `$gt`, `$gte`, `$in`, `$lt`, `$lte`, `$ne`, `$nin` (comparison) +- Tier `1b_`: `$exists`, `$type`, `$mod`, `$expr`, `$jsonSchema` (other universal) +- Tier `2_`: `$regex` (string-only), `$elemMatch` (array-only), `$bitsAllSet` (int/long-only) + +### Decision matrix + +``` +Has field type info? +โ”œโ”€ NO โ†’ no sortText override (Monaco default alphabetical) +โ”œโ”€ YES +โ”‚ โ”œโ”€ Operator has applicableBsonTypes matching field? โ†’ "0_" +โ”‚ โ”œโ”€ Operator has no applicableBsonTypes? +โ”‚ โ”‚ โ”œโ”€ Is comparison operator (meta = query:comparison)? โ†’ "1a_" +โ”‚ โ”‚ โ””โ”€ Other category? โ†’ "1b_" +โ”‚ โ””โ”€ Operator has applicableBsonTypes NOT matching field? โ†’ "2_" +``` + +## Key concepts + +### `completionKnowledge.ts` + +Curated domain rules that go beyond the auto-generated operator registry in `documentdb-constants`. Contains: + +- **`KEY_POSITION_OPERATORS`** โ€” operators valid only at query root level (`$and`, `$or`, etc.) +- **`LABEL_PLACEHOLDER`** โ€” the `โ€ฆ` character used in display labels +- **`INFO_INDICATOR`** โ€” the `โ„น` character prepended to example descriptions + +### Snippet handling + +Operator snippets in `documentdb-constants` include outer braces: `{ $gt: ${1:value} }`. + +- **Empty position**: operators keep full braces (user has no braces); fields wrapped with `{ ... }` +- **Value position**: inserted as-is (user is replacing the entire value) +- **Operator position**: outer `{ }` stripped via `stripOuterBraces()` (user is already inside braces) +- **Key position**: outer `{ }` stripped (user is already inside the query object) +- **`$` escaping**: `escapeSnippetDollars()` prevents Monaco from treating `$gt` as a variable reference diff --git a/src/webviews/documentdbQuery/completions/completionKnowledge.ts b/src/webviews/documentdbQuery/completions/completionKnowledge.ts new file mode 100644 index 000000000..148539e60 --- /dev/null +++ b/src/webviews/documentdbQuery/completions/completionKnowledge.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Completion knowledge โ€” curated domain rules for the completion provider. + * + * This file centralises "knowledge" that is **not** part of the generic + * DocumentDB operator registry (`documentdb-constants`) but is essential for + * producing high-quality, context-sensitive completions in the query editor. + * + * ### Why this file exists + * + * The `documentdb-constants` package is auto-generated from the official + * operator reference and is intentionally kept generic โ€” it describes *what* + * operators exist, not *where* they are syntactically valid. + * + * However the completion provider needs to know additional rules: + * + * 1. **Which operators are only valid at key (root) position?** + * `$and`, `$or`, `$nor`, etc. accept sub-queries, not field values. + * Showing them inside a field's operator list (`{ age: { $and โ€ฆ } }`) is + * misleading, so we need an explicit list to filter them out of + * operator-position completions and include them in key-position completions. + * + * 2. **Placeholder character for labels** + * A single Unicode character used in completion-list labels to represent + * "user fills this in". Must render well in all editors and at any font size. + * + * Adding new knowledge here keeps the completion provider self-documented and + * avoids magic values scattered across multiple files. + */ + +/** + * Operators that are syntactically valid only at the **key position** (the + * root level of a query document, or inside a `$and`/`$or`/`$nor` array + * element). + * + * These operators accept sub-expressions or arrays of sub-queries as their + * values โ€” they do **not** operate on a specific field's BSON value. For + * example: + * + * ```js + * // โœ… Valid โ€” key position + * { $and: [{ age: { $gt: 18 } }, { name: "Alice" }] } + * + * // โŒ Invalid โ€” operator position on field 'age' + * { age: { $and: โ€ฆ } } + * ``` + * + * **`$not` is intentionally excluded** โ€” despite being a logical operator, + * `$not` is a field-level operator that wraps a single field's expression: + * `{ price: { $not: { $gt: 1.99 } } }`. It does NOT work at query root. + * + * The completion provider uses this set to: + * - **Include** these operators at key position and array-element position + * - **Exclude** them from operator position (inside `{ field: { โ€ฆ } }`) + * - **Exclude** them from value position + * + * Source: DocumentDB query language specification โ€” logical and meta operators. + */ +export const KEY_POSITION_OPERATORS = new Set([ + '$and', + '$or', + '$nor', + '$comment', + '$expr', + '$jsonSchema', + '$text', + '$where', +]); + +/** + * Placeholder character used in completion-list **labels** to indicate where + * the user should type a value. + * + * This is purely cosmetic โ€” the actual insertText uses Monaco snippet tab stops + * (`${1:placeholder}`). The label placeholder is what users see in the + * completion picker before selecting an item. + * + * We use the horizontal ellipsis `โ€ฆ` (U+2026) because: + * - It is universally understood as "something goes here" + * - It renders reliably across all monospace and proportional fonts + * - It is visually lightweight and does not distract from the operator syntax + * + * Previously we used `โ–ช` (U+25AA, Black Small Square) but it was too subtle + * at small font sizes and less semantically clear. + */ +export const LABEL_PLACEHOLDER = '\u2026'; // โ€ฆ (horizontal ellipsis) + +/** + * Info indicator for completion descriptions that contain usage examples. + * + * Prepended to description strings that show example values to differentiate + * them from plain type labels (e.g., `"โ„น e.g. ends with '.com'"` vs `"string literal"`). + */ +export const INFO_INDICATOR = '\u2139'; // โ„น (information source) diff --git a/src/webviews/documentdbQuery/completions/createCompletionItems.ts b/src/webviews/documentdbQuery/completions/createCompletionItems.ts new file mode 100644 index 000000000..960c096b1 --- /dev/null +++ b/src/webviews/documentdbQuery/completions/createCompletionItems.ts @@ -0,0 +1,377 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Context-sensitive completion item creation for the `documentdb-query` language. + * + * This module is the main entry point for the completion provider. It uses + * cursor context detection to determine which completions to show and delegates + * to specialized functions for each context (key, value, operator, etc.). + */ + +import { + FILTER_COMPLETION_META, + getFilteredCompletions, + PROJECTION_COMPLETION_META, +} from '@vscode-documentdb/documentdb-constants'; +// eslint-disable-next-line import/no-internal-modules +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { getCompletionContext } from '../completionStore'; +import { type CursorContext } from '../cursorContext'; +import { EditorType } from '../languageConfig'; +import { KEY_POSITION_OPERATORS } from './completionKnowledge'; +import { createJsGlobalCompletionItems } from './jsGlobals'; +import { mapFieldToCompletionItem, mapOperatorToCompletionItem } from './mapCompletionItems'; +import { createTypeSuggestions } from './typeSuggestions'; + +/** + * Parameters for creating completion items. + */ +export interface CreateCompletionItemsParams { + /** The editor type parsed from the model URI (undefined if URI doesn't match). */ + editorType: EditorType | undefined; + /** The session ID for looking up dynamic field completions. */ + sessionId: string | undefined; + /** The range to insert completions at. */ + range: monacoEditor.IRange; + /** Whether the cursor is immediately after a '$' character. */ + isDollarPrefix: boolean; + /** The Monaco editor API. */ + monaco: typeof monacoEditor; + /** + * Optional BSON types of the field the cursor is operating on. + * When provided, operators are sorted by type relevance. + */ + fieldBsonTypes?: readonly string[]; + /** + * When true, completion snippets should include outer `{ }` wrapping. + * Set when the editor content has no braces (user cleared the editor), + * so that inserted completions produce valid query syntax. + */ + needsWrapping?: boolean; + /** + * Optional cursor context from the heuristic cursor position detector. + * When provided, completions are filtered based on the semantic position + * of the cursor. When undefined, falls back to showing all completions + * (fields, operators, BSON constructors, and JS globals). + */ + cursorContext?: CursorContext; +} + +// KEY_POSITION_OPERATORS is imported from ./completionKnowledge +// Re-export for backwards compatibility and testing +export { KEY_POSITION_OPERATORS } from './completionKnowledge'; + +/** + * Returns the completion meta tags appropriate for the given editor type. + * + * Exported for testing. + */ +export function getMetaTagsForEditorType(editorType: EditorType | undefined): readonly string[] { + switch (editorType) { + case EditorType.Filter: + return FILTER_COMPLETION_META; + case EditorType.Project: + case EditorType.Sort: + return PROJECTION_COMPLETION_META; + default: + return FILTER_COMPLETION_META; + } +} + +/** + * Creates Monaco completion items based on the editor context. + * + * Main entry point called by the CompletionItemProvider. + * + * Context routing: + * - **key**: field names + key-position operators ($and, $or, etc.) + * - **value**: type suggestions + operators (with braces) + BSON constructors + * - **operator**: operators (without braces) with type-aware sorting + * - **array-element**: same as key position + * - **empty** (unknown + needsWrapping): key-position completions with `{ }` wrapping + * - **unknown** (ambiguous): all completions โ€” full discovery fallback + */ +export function createCompletionItems(params: CreateCompletionItemsParams): monacoEditor.languages.CompletionItem[] { + const { editorType, sessionId, range, monaco, fieldBsonTypes, cursorContext, needsWrapping } = params; + + if (!cursorContext || cursorContext.position === 'unknown') { + if (needsWrapping) { + // EMPTY editor โ€” no braces present. Show key-position completions + // (fields + root operators) with { } wrapping so inserted items + // produce valid syntax. + return createEmptyEditorCompletions(editorType, sessionId, range, monaco); + } + // Genuinely UNKNOWN โ€” show all completions as a discovery fallback. + return createAllCompletions(editorType, sessionId, range, monaco); + } + + switch (cursorContext.position) { + case 'key': + case 'array-element': + return createKeyPositionCompletions(editorType, sessionId, range, monaco); + + case 'value': { + const fieldBsonType = cursorContext.fieldBsonType; + return createValuePositionCompletions(editorType, range, monaco, fieldBsonType); + } + + case 'operator': { + const bsonTypes = cursorContext.fieldBsonType ? [cursorContext.fieldBsonType] : fieldBsonTypes; + return createOperatorPositionCompletions(editorType, range, monaco, bsonTypes); + } + + default: + return createAllCompletions(editorType, sessionId, range, monaco); + } +} + +// ---------- Context-specific completion builders ---------- + +/** + * Empty editor completions โ€” shows key-position items with `{ }` wrapping. + * + * Used when the editor has no braces (user cleared content). Behaves like + * key position but wraps all inserted completions with outer `{ }` so they + * produce valid query syntax. + */ +function createEmptyEditorCompletions( + editorType: EditorType | undefined, + sessionId: string | undefined, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem[] { + const metaTags = getMetaTagsForEditorType(editorType); + const allEntries = getFilteredCompletions({ meta: [...metaTags] }); + + // Key-position operators โ€” keep outer braces (don't strip) + const keyEntries = allEntries.filter((e) => KEY_POSITION_OPERATORS.has(e.value)); + const operatorItems = keyEntries.map((entry) => { + const item = mapOperatorToCompletionItem(entry, range, monaco); + item.sortText = `1_${entry.value}`; + return item; + }); + + // Fields โ€” wrap insertText with `{ ... }` for valid syntax + const fieldItems = getFieldCompletionItems(sessionId, range, monaco).map((item) => ({ + ...item, + insertText: `{ ${item.insertText as string} }`, + })); + + return [...fieldItems, ...operatorItems]; +} + +/** + * All completions โ€” used when cursor context is genuinely ambiguous (UNKNOWN). + * Shows fields, all operators, BSON constructors, and JS globals. + * Full discovery fallback for positions the parser can't classify. + */ +function createAllCompletions( + editorType: EditorType | undefined, + sessionId: string | undefined, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem[] { + const metaTags = getMetaTagsForEditorType(editorType); + const allEntries = getFilteredCompletions({ meta: [...metaTags] }); + + const fieldItems = getFieldCompletionItems(sessionId, range, monaco); + + const operatorItems = allEntries + .filter((e) => e.meta !== 'bson' && e.meta !== 'variable' && e.standalone !== false) + .map((entry) => mapOperatorToCompletionItem(entry, range, monaco)); + + const bsonItems = allEntries + .filter((e) => e.meta === 'bson') + .map((entry) => { + const item = mapOperatorToCompletionItem(entry, range, monaco); + item.sortText = `3_${entry.value}`; + return item; + }); + + const jsGlobals = createJsGlobalCompletionItems(range, monaco); + + return [...fieldItems, ...operatorItems, ...bsonItems, ...jsGlobals]; +} + +function createKeyPositionCompletions( + editorType: EditorType | undefined, + sessionId: string | undefined, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem[] { + const metaTags = getMetaTagsForEditorType(editorType); + const allEntries = getFilteredCompletions({ meta: [...metaTags] }); + + const keyEntries = allEntries.filter((e) => KEY_POSITION_OPERATORS.has(e.value)); + const operatorItems = keyEntries.map((entry) => { + // Strip outer braces โ€” the user is already inside `{ }` at key position, + // so inserting the full `{ $and: [...] }` would create double braces. + const item = mapOperatorToCompletionItem(entry, range, monaco, undefined, true); + item.sortText = `1_${entry.value}`; + return item; + }); + + const fieldItems = getFieldCompletionItems(sessionId, range, monaco); + return [...fieldItems, ...operatorItems]; +} + +/** + * Value position completions: + * - **Project editor**: `1` (include) and `0` (exclude) โ€” the most common projection values + * - **Sort editor**: `1` (ascending) and `-1` (descending) + * - **Filter editor** (default): + * 1. Type-aware suggestions (sort `00_`) โ€” e.g., `true`/`false` for booleans + * 2. Query operators with brace-wrapping snippets (sort `0_`โ€“`2_`) + * 3. BSON constructors (sort `3_`) + * 4. JS globals: Date, Math, RegExp, etc. (sort `4_`) + */ +function createValuePositionCompletions( + editorType: EditorType | undefined, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, + fieldBsonType: string | undefined, +): monacoEditor.languages.CompletionItem[] { + // Project editor: only show include/exclude values + if (editorType === EditorType.Project) { + return createProjectValueCompletions(range, monaco); + } + + // Sort editor: only show ascending/descending values + if (editorType === EditorType.Sort) { + return createSortValueCompletions(range, monaco); + } + + const metaTags = getMetaTagsForEditorType(editorType); + const allEntries = getFilteredCompletions({ meta: [...metaTags] }); + + // 1. Type-aware suggestions (highest priority) + const typeSuggestions = createTypeSuggestions(fieldBsonType, range, monaco); + + // 2. Operators, excluding key-position-only operators. + // When fieldBsonType is known, apply type-aware sorting so comparison + // operators (e.g., $eq) appear above irrelevant ones (e.g., $bitsAllSet). + const fieldBsonTypes = fieldBsonType ? [fieldBsonType] : undefined; + const operatorEntries = allEntries.filter( + (e) => + e.meta !== 'bson' && + e.meta !== 'variable' && + e.standalone !== false && + !KEY_POSITION_OPERATORS.has(e.value), + ); + const operatorItems = operatorEntries.map((entry) => { + const item = mapOperatorToCompletionItem(entry, range, monaco, fieldBsonTypes); + // If type-aware sorting produced a prefix, keep it; otherwise default to 0_ + if (!item.sortText) { + item.sortText = `0_${entry.value}`; + } + return item; + }); + + // 3. BSON constructors (sort prefix 3_ โ€” after all operator tiers: 0_, 1a_, 1b_, 2_) + const bsonEntries = allEntries.filter((e) => e.meta === 'bson'); + const bsonItems = bsonEntries.map((entry) => { + const item = mapOperatorToCompletionItem(entry, range, monaco); + item.sortText = `3_${entry.value}`; + return item; + }); + + // 4. JS globals: Date, Math, RegExp, Infinity, NaN, undefined (sort prefix 4_) + const jsGlobals = createJsGlobalCompletionItems(range, monaco); + + return [...typeSuggestions, ...operatorItems, ...bsonItems, ...jsGlobals]; +} + +/** + * Value completions for the **project** editor: `1` (include) and `0` (exclude). + * + * Projection operators like `$slice` and `$elemMatch` are already available + * via operator-position completions; these simple numeric values cover the + * most common use case. + */ +function createProjectValueCompletions( + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem[] { + return [ + { + label: { label: '1', description: 'include field' }, + kind: monaco.languages.CompletionItemKind.Value, + insertText: '1', + sortText: '00_1', + preselect: true, + range, + }, + { + label: { label: '0', description: 'exclude field' }, + kind: monaco.languages.CompletionItemKind.Value, + insertText: '0', + sortText: '00_0', + range, + }, + ]; +} + +/** + * Value completions for the **sort** editor: `1` (ascending) and `-1` (descending). + */ +function createSortValueCompletions( + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem[] { + return [ + { + label: { label: '1', description: 'ascending' }, + kind: monaco.languages.CompletionItemKind.Value, + insertText: '1', + sortText: '00_1', + preselect: true, + range, + }, + { + label: { label: '-1', description: 'descending' }, + kind: monaco.languages.CompletionItemKind.Value, + insertText: '-1', + sortText: '00_-1', + range, + }, + ]; +} + +function createOperatorPositionCompletions( + editorType: EditorType | undefined, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, + fieldBsonTypes: readonly string[] | undefined, +): monacoEditor.languages.CompletionItem[] { + const metaTags = getMetaTagsForEditorType(editorType); + const allEntries = getFilteredCompletions({ meta: [...metaTags] }); + + const operatorEntries = allEntries.filter( + (e) => + e.meta !== 'bson' && + e.meta !== 'variable' && + e.standalone !== false && + !KEY_POSITION_OPERATORS.has(e.value), + ); + return operatorEntries.map((entry) => mapOperatorToCompletionItem(entry, range, monaco, fieldBsonTypes, true)); +} + +function getFieldCompletionItems( + sessionId: string | undefined, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem[] { + const fieldItems: monacoEditor.languages.CompletionItem[] = []; + if (sessionId) { + const context = getCompletionContext(sessionId); + if (context) { + for (const field of context.fields) { + fieldItems.push(mapFieldToCompletionItem(field, range, monaco)); + } + } + } + return fieldItems; +} diff --git a/src/webviews/documentdbQuery/completions/index.ts b/src/webviews/documentdbQuery/completions/index.ts new file mode 100644 index 000000000..c9fa6db8c --- /dev/null +++ b/src/webviews/documentdbQuery/completions/index.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Completion items for the `documentdb-query` language. + * + * This folder contains context-sensitive completion logic: + * - `createCompletionItems.ts` โ€” main entry point, context branching + * - `mapCompletionItems.ts` โ€” operator/field โ†’ CompletionItem mapping + * - `typeSuggestions.ts` โ€” type-aware value suggestions (bool โ†’ true/false, etc.) + * - `jsGlobals.ts` โ€” JS globals available in the shell-bson-parser sandbox (Date, Math, etc.) + * - `snippetUtils.ts` โ€” snippet text manipulation (brace stripping, $ escaping) + */ + +export { INFO_INDICATOR, LABEL_PLACEHOLDER } from './completionKnowledge'; +export { + KEY_POSITION_OPERATORS, + createCompletionItems, + getMetaTagsForEditorType, + type CreateCompletionItemsParams, +} from './createCompletionItems'; +export { createJsGlobalCompletionItems } from './jsGlobals'; +export { + getCategoryLabel, + getCompletionKindForMeta, + getOperatorSortPrefix, + mapFieldToCompletionItem, + mapOperatorToCompletionItem, +} from './mapCompletionItems'; +export { escapeSnippetDollars, stripOuterBraces } from './snippetUtils'; +export { createTypeSuggestions } from './typeSuggestions'; diff --git a/src/webviews/documentdbQuery/completions/jsGlobals.ts b/src/webviews/documentdbQuery/completions/jsGlobals.ts new file mode 100644 index 000000000..483485f54 --- /dev/null +++ b/src/webviews/documentdbQuery/completions/jsGlobals.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * JavaScript global completions for the `documentdb-query` language. + * + * The `documentdb-query` language uses `@mongodb-js/shell-bson-parser` to + * execute queries. That parser runs in a sandboxed scope that exposes a + * limited set of JavaScript globals beyond the BSON constructors (which are + * already registered in `documentdb-constants`). + * + * This module provides completion items for those JS globals so they appear + * in the value-position completion list. They are NOT added to + * `documentdb-constants` because they are runtime JS constructs, not + * DocumentDB API operators. + * + * ### Supported JS globals (from shell-bson-parser's sandbox scope) + * + * **Class expressions** (object with whitelisted methods): + * - `Date` โ€” `new Date()`, `Date()`, `Date.now()`, plus instance methods + * - `Math` โ€” `Math.floor()`, `Math.min()`, `Math.max()`, etc. + * + * **Globals** (primitive values): + * - `Infinity`, `NaN`, `undefined` + * + * **Constructor functions** (SCOPE_ANY / SCOPE_NEW / SCOPE_CALL): + * - `RegExp` โ€” already handled by the JS tokenizer, but listed for completeness + * + * Source: `node_modules/@mongodb-js/shell-bson-parser/dist/scope.js` + * (SCOPE_ANY, SCOPE_NEW, SCOPE_CALL, GLOBALS, ALLOWED_CLASS_EXPRESSIONS) + */ + +// eslint-disable-next-line import/no-internal-modules +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { escapeSnippetDollars } from './snippetUtils'; + +/** A JS global completion definition. */ +interface JsGlobalDef { + /** Display label (e.g., "Date") */ + label: string; + /** Optional snippet to insert (otherwise label is used) */ + snippet?: string; + /** Short description shown right-aligned in the completion list */ + description: string; + /** Documentation shown in the details panel */ + documentation: string; +} + +/** + * JS globals available in shell-bson-parser's sandbox. + * + * These are the class expressions and global values that the parser's + * sandboxed eval supports. BSON constructors (ObjectId, ISODate, etc.) + * are already provided by `documentdb-constants` and are NOT duplicated here. + */ +const JS_GLOBALS: readonly JsGlobalDef[] = [ + // -- Class constructors -- + { + label: 'Date', + snippet: 'new Date(${1})', + description: 'JS global', + documentation: + 'JavaScript Date constructor.\n\n' + + 'Usages:\n' + + '- `new Date()` โ€” current time\n' + + '- `new Date("2025-01-01")` โ€” specific date\n' + + '- `new Date(Date.now() - 14 * 24 * 60 * 60 * 1000)` โ€” 14 days ago', + }, + { + label: 'Date.now()', + snippet: 'Date.now()', + description: 'JS global', + documentation: + 'Returns milliseconds since Unix epoch (Jan 1, 1970).\n\nUseful for relative date queries:\n```\n{ $gt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }\n```', + }, + { + label: 'RegExp', + snippet: 'RegExp("${1:pattern}")', + description: 'JS global', + documentation: + 'JavaScript RegExp constructor.\n\nExample: `RegExp("^test")`\n\nPrefer regex literals: `/^test/`', + }, + + // -- Math methods -- + { + label: 'Math.floor()', + snippet: 'Math.floor(${1:value})', + description: 'JS global', + documentation: 'Round down to the nearest integer.\n\nExample: `Math.floor(3.7)` โ†’ `3`', + }, + { + label: 'Math.ceil()', + snippet: 'Math.ceil(${1:value})', + description: 'JS global', + documentation: 'Round up to the nearest integer.\n\nExample: `Math.ceil(3.2)` โ†’ `4`', + }, + { + label: 'Math.round()', + snippet: 'Math.round(${1:value})', + description: 'JS global', + documentation: 'Round to the nearest integer.\n\nExample: `Math.round(3.5)` โ†’ `4`', + }, + { + label: 'Math.min()', + snippet: 'Math.min(${1:a}, ${2:b})', + description: 'JS global', + documentation: 'Return the smaller of two values.\n\nExample: `Math.min(1.7, 2)` โ†’ `1.7`', + }, + { + label: 'Math.max()', + snippet: 'Math.max(${1:a}, ${2:b})', + description: 'JS global', + documentation: 'Return the larger of two values.\n\nExample: `Math.max(1.7, 2)` โ†’ `2`', + }, + + // -- Primitive globals -- + { + label: 'Infinity', + description: 'JS global', + documentation: 'Numeric value representing infinity.\n\nExample: `{ $lt: Infinity }`', + }, + { + label: 'NaN', + description: 'JS global', + documentation: 'Numeric value representing Not-a-Number.\n\nExample: `{ $ne: NaN }`', + }, + { + label: 'undefined', + description: 'JS global', + documentation: 'The undefined value.\n\nExample: `{ field: undefined }` โ€” matches missing fields.', + }, +]; + +/** + * Creates completion items for JavaScript globals supported by the + * shell-bson-parser sandbox. + * + * These are shown at value position with sort prefix `4_` (after BSON + * constructors at `3_`). + * + * @param range - the insertion range + * @param monaco - the Monaco API + */ +export function createJsGlobalCompletionItems( + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem[] { + return JS_GLOBALS.map((def) => { + const hasSnippet = !!def.snippet; + let insertText = hasSnippet ? def.snippet! : def.label; + if (hasSnippet) { + insertText = escapeSnippetDollars(insertText); + } + + return { + label: { + label: def.label, + description: def.description, + }, + kind: hasSnippet + ? monaco.languages.CompletionItemKind.Constructor + : monaco.languages.CompletionItemKind.Constant, + insertText, + insertTextRules: hasSnippet ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, + documentation: { value: def.documentation }, + sortText: `4_${def.label}`, + range, + }; + }); +} diff --git a/src/webviews/documentdbQuery/completions/mapCompletionItems.ts b/src/webviews/documentdbQuery/completions/mapCompletionItems.ts new file mode 100644 index 000000000..3f68537f5 --- /dev/null +++ b/src/webviews/documentdbQuery/completions/mapCompletionItems.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Functions for mapping operator and field data to Monaco CompletionItems. + */ + +import { type OperatorEntry } from '@vscode-documentdb/documentdb-constants'; +// eslint-disable-next-line import/no-internal-modules +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { type FieldCompletionData } from '../../../utils/json/data-api/autocomplete/toFieldCompletionItems'; +import { escapeSnippetDollars, stripOuterBraces } from './snippetUtils'; + +/** + * Maps a meta tag category to a Monaco CompletionItemKind. + */ +export function getCompletionKindForMeta( + meta: string, + kinds: typeof monacoEditor.languages.CompletionItemKind, +): number { + if (meta.startsWith('query')) return kinds.Operator; + if (meta.startsWith('expr')) return kinds.Function; + if (meta === 'bson') return kinds.Constructor; + if (meta === 'stage') return kinds.Module; + if (meta === 'accumulator') return kinds.Method; + if (meta === 'update') return kinds.Property; + if (meta === 'variable') return kinds.Variable; + if (meta === 'window') return kinds.Event; + if (meta === 'field:identifier') return kinds.Field; + return kinds.Text; +} + +/** + * Computes a sortText prefix for an operator based on its type relevance + * to the given field BSON types. + * + * Sorting tiers (ascending = higher priority): + * - `"0_"` โ€” Type-relevant: operator's `applicableBsonTypes` intersects with `fieldBsonTypes` + * - `"1a_"` โ€” Comparison operators (universal): `$eq`, `$ne`, `$gt`, `$in`, etc. + * These are the most commonly used operators for any field type. + * - `"1b_"` โ€” Other universal operators: element, evaluation, geospatial, etc. + * - `"2_"` โ€” Non-matching: operator's `applicableBsonTypes` is set but doesn't match + * + * Returns `undefined` when no field type info is available (no sorting override). + */ +export function getOperatorSortPrefix( + entry: OperatorEntry, + fieldBsonTypes: readonly string[] | undefined, +): string | undefined { + if (!fieldBsonTypes || fieldBsonTypes.length === 0) { + return undefined; + } + + if (!entry.applicableBsonTypes || entry.applicableBsonTypes.length === 0) { + // Promote comparison operators above other universal operators + return entry.meta === 'query:comparison' ? '1a_' : '1b_'; + } + + const hasMatch = entry.applicableBsonTypes.some((t) => fieldBsonTypes.includes(t)); + return hasMatch ? '0_' : '2_'; +} + +/** + * Extracts a human-readable category label from a meta tag. + * `'query:comparison'` โ†’ `'comparison'`, `'bson'` โ†’ `'bson'` + */ +export function getCategoryLabel(meta: string): string { + const colonIndex = meta.indexOf(':'); + return colonIndex >= 0 ? meta.substring(colonIndex + 1) : meta; +} + +/** + * Maps an OperatorEntry from documentdb-constants to a Monaco CompletionItem. + * + * Pure function โ€” safe for unit testing without a Monaco runtime. + * + * @param entry - the operator entry to map + * @param range - the insertion range + * @param monaco - the Monaco API + * @param fieldBsonTypes - optional BSON types of the field for type-aware sorting + * @param shouldStripBraces - when true, strip outer `{ }` from snippets (for operator position) + */ +export function mapOperatorToCompletionItem( + entry: OperatorEntry, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, + fieldBsonTypes?: readonly string[], + shouldStripBraces?: boolean, +): monacoEditor.languages.CompletionItem { + const hasSnippet = !!entry.snippet; + const sortPrefix = getOperatorSortPrefix(entry, fieldBsonTypes); + let insertText = hasSnippet ? entry.snippet! : entry.value; + if (shouldStripBraces && hasSnippet) { + insertText = stripOuterBraces(insertText); + } + if (hasSnippet) { + insertText = escapeSnippetDollars(insertText); + } + + const categoryLabel = getCategoryLabel(entry.meta); + + let documentationValue = entry.description; + if (entry.link) { + documentationValue += `\n\n[โ“˜ Documentation](${entry.link})`; + } + + return { + label: { + label: entry.value, + description: categoryLabel, + }, + kind: getCompletionKindForMeta(entry.meta, monaco.languages.CompletionItemKind), + insertText, + insertTextRules: hasSnippet ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, + documentation: { + value: documentationValue, + isTrusted: true, + }, + sortText: sortPrefix ? `${sortPrefix}${entry.value}` : undefined, + range, + }; +} + +/** + * Maps a FieldCompletionData entry to a Monaco CompletionItem. + * + * Fields are given a sort prefix of `"0_"` so they appear before operators. + * The insert text includes a trailing `: $1` snippet so that selecting a + * field name immediately places the cursor at the value position. + */ +export function mapFieldToCompletionItem( + field: FieldCompletionData, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem { + const sparseIndicator = field.isSparse ? ' (sparse)' : ''; + return { + label: { + label: field.fieldName, + description: `${field.displayType}${sparseIndicator}`, + }, + kind: monaco.languages.CompletionItemKind.Field, + insertText: `${field.insertText}: $1`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + sortText: `0_${field.fieldName}`, + range, + }; +} diff --git a/src/webviews/documentdbQuery/completions/snippetUtils.ts b/src/webviews/documentdbQuery/completions/snippetUtils.ts new file mode 100644 index 000000000..e43b7f70c --- /dev/null +++ b/src/webviews/documentdbQuery/completions/snippetUtils.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Utility functions for manipulating Monaco snippet text. + */ + +/** + * Strips the outermost `{ ` and ` }` from an operator snippet. + * + * Operator snippets in documentdb-constants are designed for value position + * (e.g., `{ $gt: ${1:value} }`). At operator position, the user is already + * inside braces, so the outer wrapping must be removed to avoid double-nesting. + * + * Only strips if the snippet starts with `'{ '` and ends with `' }'`. + * Inner brackets/braces are preserved: + * - `{ $in: [${1:value}] }` โ†’ `$in: [${1:value}]` + * - `{ $gt: ${1:value} }` โ†’ `$gt: ${1:value}` + */ +export function stripOuterBraces(snippet: string): string { + if (snippet.startsWith('{ ') && snippet.endsWith(' }')) { + return snippet.slice(2, -2); + } + return snippet; +} + +/** + * Escapes literal `$` signs in snippet text that would be misinterpreted + * as Monaco snippet variables. + * + * In Monaco snippet syntax, `$name` is a variable reference (resolves to empty + * for unknown variables). Operator names like `$gt` in snippets get consumed + * as variable references, producing empty output instead of the literal `$gt`. + * + * This function escapes `$` when followed by a letter (`$gt` โ†’ `\$gt`) + * while preserving tab stop syntax (`${1:value}` and `$1` are unchanged). + */ +export function escapeSnippetDollars(snippet: string): string { + return snippet.replace(/\$(?=[a-zA-Z])/g, '\\$'); +} diff --git a/src/webviews/documentdbQuery/completions/typeSuggestions.ts b/src/webviews/documentdbQuery/completions/typeSuggestions.ts new file mode 100644 index 000000000..391e3094f --- /dev/null +++ b/src/webviews/documentdbQuery/completions/typeSuggestions.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Type-aware value suggestions for the completion provider. + * + * When the cursor is at a value position and the field's BSON type is known, + * this module provides contextual suggestions that match the field type: + * - Boolean fields โ†’ `true`, `false` + * - Number fields โ†’ range query snippet `{ $gt: โ–ช, $lt: โ–ช }` + * - String fields โ†’ regex snippet, empty string literal + * - Date fields โ†’ ISODate constructor, date range snippet + * - ObjectId fields โ†’ ObjectId constructor + * - Null fields โ†’ `null` + * - Array fields โ†’ `$elemMatch` snippet + * + * These suggestions appear at the top of the completion list (sort prefix `00_`) + * to surface the most common patterns for each type. + */ + +// eslint-disable-next-line import/no-internal-modules +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { LABEL_PLACEHOLDER } from './completionKnowledge'; +import { escapeSnippetDollars } from './snippetUtils'; + +/** A type suggestion definition. */ +interface TypeSuggestionDef { + /** Display label */ + label: string; + /** Text or snippet to insert */ + insertText: string; + /** Whether insertText is a snippet (has tab stops) */ + isSnippet: boolean; + /** Description shown in the label area */ + description: string; + /** Documentation shown in the details panel */ + documentation?: string; +} + +/** + * Maps BSON type strings to curated value suggestions. + * + * Each type maps to an array of suggestions ordered by likelihood. + * The suggestions use Monaco snippet syntax for tab stops. + */ +const TYPE_SUGGESTIONS: Record = { + // BSONTypes.Boolean = 'boolean' + boolean: [ + { + label: 'true', + insertText: 'true', + isSnippet: false, + description: 'boolean literal', + documentation: `Boolean literal \`true\`.\n\nExample: \`{ field: true }\``, + }, + { + label: 'false', + insertText: 'false', + isSnippet: false, + description: 'boolean literal', + documentation: `Boolean literal \`false\`.\n\nExample: \`{ field: false }\``, + }, + ], + // BSONTypes.Int32 = 'int32' + int32: numberSuggestions(), + // BSONTypes.Double = 'double' + double: numberSuggestions(), + // BSONTypes.Long = 'long' + long: numberSuggestions(), + // BSONTypes.Decimal128 = 'decimal128' + decimal128: numberSuggestions(), + // BSONTypes.Number = 'number' (generic number without specific subtype) + number: numberSuggestions(), + string: [ + { + label: `{ $regex: /${LABEL_PLACEHOLDER}/ }`, + insertText: '{ $regex: /${1:pattern}/ }', + isSnippet: true, + description: 'pattern match', + documentation: + 'Match string fields with a regex pattern.\n\n' + + 'Example โ€” ends with `.com`:\n```\n{ $regex: /\\.com$/ }\n```', + }, + { + label: '{ $regex: /\\.com$/ }', + insertText: '{ $regex: /${1:\\.com$}/ }', + isSnippet: true, + description: `ends with .com - pattern match`, + documentation: 'Example pattern match for: ends with `.com`:\n```\n{ $regex: /\\.com$/ }\n```', + }, + { + label: '""', + insertText: '"${1:text}"', + isSnippet: true, + description: 'string literal', + documentation: `Exact string match.\n\nExample: \`"active"\`, \`"pending"\``, + }, + ], + date: [ + { + label: `ISODate("${LABEL_PLACEHOLDER}")`, + insertText: `ISODate("\${1:${twoWeeksAgo()}}")`, + isSnippet: true, + description: 'date value', + documentation: `Match a specific date.\n\nExample: \`ISODate("${twoWeeksAgo()}")\``, + }, + { + label: `{ $gt: ISODate("${LABEL_PLACEHOLDER}"), $lt: ISODate("${LABEL_PLACEHOLDER}") }`, + insertText: `{ $gt: ISODate("\${1:${twoWeeksAgo()}}"), $lt: ISODate("\${2:${todayISO()}}") }`, + isSnippet: true, + description: 'date range', + documentation: `Match dates within a range.\n\nExample: last 2 weeks โ€” \`{ $gt: ISODate("${twoWeeksAgo()}"), $lt: ISODate("${todayISO()}") }\``, + }, + { + label: `{ $gt: new Date(Date.now() - ${LABEL_PLACEHOLDER}) }`, + insertText: '{ $gt: new Date(Date.now() - ${1:14} * 24 * 60 * 60 * 1000) }', + isSnippet: true, + description: 'last N days', + documentation: `Match dates in the last N days relative to now.\n\nExample: last 14 days โ€” \`{ $gt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000) }\``, + }, + ], + objectid: [ + { + label: `ObjectId("${LABEL_PLACEHOLDER}")`, + insertText: 'ObjectId("${1:hex}")', + isSnippet: true, + description: 'ObjectId value', + documentation: `Match by ObjectId.\n\nExample: \`ObjectId("507f1f77bcf86cd799439011")\``, + }, + ], + null: [ + { + label: 'null', + insertText: 'null', + isSnippet: false, + description: 'null literal', + documentation: `Match null or missing fields.\n\nExample: \`{ field: null }\``, + }, + ], + array: [ + { + label: `{ $elemMatch: { ${LABEL_PLACEHOLDER} } }`, + insertText: '{ $elemMatch: { ${1:query} } }', + isSnippet: true, + description: 'match element', + documentation: `Match arrays with at least one element satisfying the query.\n\nExample: \`{ $elemMatch: { status: "urgent" } }\``, + }, + { + label: `{ $size: ${LABEL_PLACEHOLDER} }`, + insertText: '{ $size: ${1:length} }', + isSnippet: true, + description: 'array length', + documentation: `Match arrays with exactly N elements.\n\nExample: \`{ $size: 3 }\``, + }, + ], +}; + +/** Shared number-type suggestions (int, double, long, decimal). */ +function numberSuggestions(): readonly TypeSuggestionDef[] { + return [ + { + label: `{ $gt: ${LABEL_PLACEHOLDER}, $lt: ${LABEL_PLACEHOLDER} }`, + insertText: '{ $gt: ${1:min}, $lt: ${2:max} }', + isSnippet: true, + description: 'range query', + documentation: `Match numbers within a range.\n\nExample: between 18 and 65 โ€” \`{ $gt: 18, $lt: 65 }\``, + }, + { + label: `{ $gte: ${LABEL_PLACEHOLDER} }`, + insertText: '{ $gte: ${1:value} }', + isSnippet: true, + description: 'minimum value', + documentation: `Match numbers greater than or equal to a value.\n\nExample: at least 100 โ€” \`{ $gte: 100 }\``, + }, + ]; +} + +/** + * Returns an ISO 8601 timestamp for two weeks ago (UTC, midnight). + * Used as a sensible default date placeholder โ€” recent enough to be practical. + */ +function twoWeeksAgo(): string { + const d = new Date(); + d.setUTCDate(d.getUTCDate() - 14); + d.setUTCHours(0, 0, 0, 0); + return d.toISOString().replace('.000Z', 'Z'); +} + +/** + * Returns an ISO 8601 timestamp for today (UTC, end of day). + */ +function todayISO(): string { + const d = new Date(); + d.setUTCHours(23, 59, 59, 0); + return d.toISOString().replace('.000Z', 'Z'); +} + +/** + * Creates type-aware value suggestions based on the field's BSON type. + * + * Returns an array of high-priority completion items (sort prefix `00_`) + * that appear at the top of the value-position completion list. + * + * Returns an empty array when the BSON type is unknown or has no specific suggestions. + * + * @param fieldBsonType - BSON type string from the schema (e.g., 'int32', 'string', 'boolean') + * @param range - the insertion range + * @param monaco - the Monaco API + */ +export function createTypeSuggestions( + fieldBsonType: string | undefined, + range: monacoEditor.IRange, + monaco: typeof monacoEditor, +): monacoEditor.languages.CompletionItem[] { + if (!fieldBsonType) { + return []; + } + + const suggestions = TYPE_SUGGESTIONS[fieldBsonType]; + if (!suggestions) { + return []; + } + + return suggestions.map((def, index) => { + let insertText = def.insertText; + if (def.isSnippet) { + insertText = escapeSnippetDollars(insertText); + } + + return { + label: { + label: def.label, + description: def.description, + }, + kind: def.isSnippet + ? monaco.languages.CompletionItemKind.Snippet + : monaco.languages.CompletionItemKind.Value, + insertText, + insertTextRules: def.isSnippet ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, + documentation: def.documentation ? { value: def.documentation } : undefined, + sortText: `00_${String(index).padStart(2, '0')}`, + preselect: index === 0, + range, + }; + }); +} diff --git a/src/webviews/documentdbQuery/cursorContext.test.ts b/src/webviews/documentdbQuery/cursorContext.test.ts new file mode 100644 index 000000000..4ee6f57fe --- /dev/null +++ b/src/webviews/documentdbQuery/cursorContext.test.ts @@ -0,0 +1,271 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { detectCursorContext, type CursorContext, type FieldTypeLookup } from './cursorContext'; + +/** + * Helper: place cursor at the `|` marker in the input string. + * Returns { text, offset } with the `|` removed. + */ +function parseCursor(input: string): { text: string; offset: number } { + const idx = input.indexOf('|'); + if (idx === -1) { + throw new Error(`Test input must contain a '|' cursor marker: "${input}"`); + } + return { + text: input.slice(0, idx) + input.slice(idx + 1), + offset: idx, + }; +} + +/** Shorthand to detect context from a `|`-marked string. */ +function detect(input: string, fieldLookup?: FieldTypeLookup): CursorContext { + const { text, offset } = parseCursor(input); + return detectCursorContext(text, offset, fieldLookup); +} + +describe('detectCursorContext', () => { + // --------------------------------------------------------------- + // Step 1: Core context detection (complete expressions) + // --------------------------------------------------------------- + describe('Step 1: Core context detection', () => { + describe('key position (root)', () => { + it('detects key position in empty object', () => { + const result = detect('{ | }'); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + + it('detects key position after opening brace', () => { + const result = detect('{|}'); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + + it('detects key position after comma in root object', () => { + const result = detect('{ name: "Alice", | }'); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + }); + + describe('value position', () => { + it('detects value position after colon', () => { + const result = detect('{ _id: | }'); + expect(result).toEqual({ position: 'value', fieldName: '_id' }); + }); + + it('detects value position for quoted key', () => { + const result = detect('{ "my.field": | }'); + expect(result).toEqual({ position: 'value', fieldName: 'my.field' }); + }); + + it('detects value position for single-quoted key', () => { + const result = detect("{ 'address.city': | }"); + expect(result).toEqual({ position: 'value', fieldName: 'address.city' }); + }); + + it('includes bsonType when fieldLookup provides it', () => { + const lookup: FieldTypeLookup = (name) => (name === 'age' ? 'int32' : undefined); + const result = detect('{ age: | }', lookup); + expect(result).toEqual({ position: 'value', fieldName: 'age', fieldBsonType: 'int32' }); + }); + + it('omits bsonType when fieldLookup returns undefined', () => { + const lookup: FieldTypeLookup = () => undefined; + const result = detect('{ age: | }', lookup); + expect(result).toEqual({ position: 'value', fieldName: 'age' }); + }); + }); + + describe('operator position (nested object)', () => { + it('detects operator position inside nested object', () => { + const result = detect('{ age: { | } }'); + expect(result).toEqual({ position: 'operator', fieldName: 'age' }); + }); + + it('detects operator position with bsonType', () => { + const lookup: FieldTypeLookup = (name) => (name === 'age' ? 'int32' : undefined); + const result = detect('{ age: { | } }', lookup); + expect(result).toEqual({ position: 'operator', fieldName: 'age', fieldBsonType: 'int32' }); + }); + + it('detects operator position after comma in nested object', () => { + const result = detect('{ age: { $gt: 5, | } }'); + expect(result).toEqual({ position: 'operator', fieldName: 'age' }); + }); + }); + + describe('array-element position', () => { + it('detects array-element inside $and', () => { + const result = detect('{ $and: [ | ] }'); + expect(result).toEqual({ position: 'array-element', parentOperator: '$and' }); + }); + + it('detects array-element inside $or', () => { + const result = detect('{ $or: [ | ] }'); + expect(result).toEqual({ position: 'array-element', parentOperator: '$or' }); + }); + + it('detects array-element inside $nor', () => { + const result = detect('{ $nor: [ | ] }'); + expect(result).toEqual({ position: 'array-element', parentOperator: '$nor' }); + }); + }); + + describe('key inside logical operator array element', () => { + it('detects key inside $and array element object', () => { + const result = detect('{ $and: [ { | } ] }'); + expect(result.position).toBe('key'); + }); + + it('detects key inside $or array element object after comma', () => { + const result = detect('{ $or: [ { x: 1 }, { | } ] }'); + expect(result.position).toBe('key'); + }); + }); + + describe('edge cases', () => { + it('returns unknown for empty string', () => { + expect(detectCursorContext('', 0)).toEqual({ position: 'unknown' }); + }); + + it('returns unknown for cursor at offset 0', () => { + expect(detectCursorContext('{ age: 1 }', 0)).toEqual({ position: 'unknown' }); + }); + + it('returns unknown for null-ish text', () => { + expect(detectCursorContext('', 5)).toEqual({ position: 'unknown' }); + }); + + it('clamps cursor offset to text length', () => { + // Cursor past end of text โ€” should still work + const result = detectCursorContext('{ age: ', 100); + expect(result).toEqual({ position: 'value', fieldName: 'age' }); + }); + }); + }); + + // --------------------------------------------------------------- + // Step 1.5: Incomplete / broken input (mid-typing states) + // --------------------------------------------------------------- + describe('Step 1.5: Incomplete / broken input', () => { + it('{ age: | โ€” colon just typed, no closing brace', () => { + const result = detect('{ age: |'); + expect(result).toEqual({ position: 'value', fieldName: 'age' }); + }); + + it('{ age: $| โ€” started typing BSON constructor', () => { + const result = detect('{ age: $|'); + expect(result).toEqual({ position: 'value', fieldName: 'age' }); + }); + + it('{ age: $ |} โ€” dollar with closing brace', () => { + const result = detect('{ age: $ |}'); + expect(result).toEqual({ position: 'value', fieldName: 'age' }); + }); + + it('{ age: {| โ€” opened nested object, no close', () => { + const result = detect('{ age: {|'); + expect(result).toEqual({ position: 'operator', fieldName: 'age' }); + }); + + it('{ age: { $| โ€” partially typed operator', () => { + const result = detect('{ age: { $|'); + expect(result).toEqual({ position: 'operator', fieldName: 'age' }); + }); + + it('{ age: { $ |} โ€” incomplete operator inside nested object', () => { + const result = detect('{ age: { $ |}'); + expect(result).toEqual({ position: 'operator', fieldName: 'age' }); + }); + + it('{ age: { $g| โ€” partially typed $gt', () => { + const result = detect('{ age: { $g|'); + expect(result).toEqual({ position: 'operator', fieldName: 'age' }); + }); + + it('{ | โ€” opened root object, no field name yet', () => { + const result = detect('{ |'); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + + it('{ a| โ€” partially typed field name', () => { + const result = detect('{ a|'); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + + it('{ name: "Alice", | โ€” comma after first pair, new key expected', () => { + const result = detect('{ name: "Alice", |'); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + + it('{ name: "Alice", a| โ€” partially typed second field name', () => { + const result = detect('{ name: "Alice", a|'); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + + it('{ $and: [| โ€” opened array for logical operator', () => { + const result = detect('{ $and: [|'); + expect(result).toEqual({ position: 'array-element', parentOperator: '$and' }); + }); + + it('{ $and: [ {| โ€” inside $and array element object', () => { + const result = detect('{ $and: [ {|'); + expect(result.position).toBe('key'); + }); + + it('{ age: { $gt: 5, | โ€” after comma inside nested operator object', () => { + const result = detect('{ age: { $gt: 5, |'); + expect(result).toEqual({ position: 'operator', fieldName: 'age' }); + }); + + it('{| โ€” just the opening brace', () => { + const result = detect('{|'); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + + it('empty string โ†’ unknown', () => { + expect(detectCursorContext('', 0)).toEqual({ position: 'unknown' }); + }); + + it('handles fieldLookup with incomplete input', () => { + const lookup: FieldTypeLookup = (name) => (name === 'age' ? 'int32' : undefined); + const result = detect('{ age: { $|', lookup); + expect(result).toEqual({ position: 'operator', fieldName: 'age', fieldBsonType: 'int32' }); + }); + + it('{ $or: [ { name: "x" }, {| โ€” second element in $or array', () => { + const result = detect('{ $or: [ { name: "x" }, {|'); + expect(result.position).toBe('key'); + }); + }); + + // --------------------------------------------------------------- + // Multi-line expressions + // --------------------------------------------------------------- + describe('multi-line expressions', () => { + it('key position in multi-line object', () => { + const result = detect(`{ + name: "Alice", + | +}`); + expect(result).toEqual({ position: 'key', depth: 1 }); + }); + + it('value position in multi-line object', () => { + const result = detect(`{ + age: | +}`); + expect(result).toEqual({ position: 'value', fieldName: 'age' }); + }); + + it('operator position in multi-line nested object', () => { + const result = detect(`{ + age: { + | + } +}`); + expect(result).toEqual({ position: 'operator', fieldName: 'age' }); + }); + }); +}); diff --git a/src/webviews/documentdbQuery/cursorContext.ts b/src/webviews/documentdbQuery/cursorContext.ts new file mode 100644 index 000000000..606db0f39 --- /dev/null +++ b/src/webviews/documentdbQuery/cursorContext.ts @@ -0,0 +1,393 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Cursor context detection for the `documentdb-query` language. + * + * Determines the semantic position of the cursor within a DocumentDB query + * expression (e.g., key position, value position, operator position) using + * a heuristic character-scanning approach. + * + * This module is a pure function with no Monaco or VS Code dependencies, + * making it fully unit-testable. + */ + +/** + * The semantic position of the cursor within a query expression. + * + * Used by the completion provider to determine which completions to show. + */ +export type CursorContext = + | { position: 'key'; depth: number } + | { position: 'value'; fieldName: string; fieldBsonType?: string } + | { position: 'operator'; fieldName: string; fieldBsonType?: string } + | { position: 'array-element'; parentOperator: string } + | { position: 'unknown' }; + +/** + * A callback that resolves a field name to its BSON type string. + * Used to enrich cursor context with type information from the completion store. + */ +export type FieldTypeLookup = (fieldName: string) => string | undefined; + +/** + * Detects the semantic cursor context within a DocumentDB query expression. + * + * Uses a heuristic backward-scanning approach from the cursor position to + * determine whether the cursor is at a key, value, operator, or array-element + * position. Falls back to `{ position: 'unknown' }` when context cannot be + * determined. + * + * @param text - the full text of the editor + * @param cursorOffset - the 0-based character offset of the cursor + * @param fieldLookup - optional callback to resolve field names to BSON types + * @returns the detected cursor context + */ +export function detectCursorContext(text: string, cursorOffset: number, fieldLookup?: FieldTypeLookup): CursorContext { + if (!text || cursorOffset <= 0) { + return { position: 'unknown' }; + } + + // Clamp cursor to text length + const offset = Math.min(cursorOffset, text.length); + + // Find the nearest structural character before the cursor + const scanResult = scanBackward(text, offset); + + if (!scanResult) { + return { position: 'unknown' }; + } + + switch (scanResult.char) { + case ':': + return resolveValueContext(text, scanResult.index, fieldLookup); + + case '{': + return resolveOpenBraceContext(text, scanResult.index, fieldLookup); + + case ',': + return resolveCommaContext(text, scanResult.index, fieldLookup); + + case '[': + return resolveOpenBracketContext(text, scanResult.index); + + default: + return { position: 'unknown' }; + } +} + +// ---------- Internal helpers ---------- + +/** Structural characters that define context boundaries. */ +const STRUCTURAL_CHARS = new Set([':', '{', ',', '[']); + +interface ScanResult { + char: string; + index: number; +} + +// Known edge case: the backward scanner does not track whether characters +// are inside quoted strings. A structural character that appears within a +// string literal is still treated as structural. For example, in +// { msg: "{", | } +// the `{` inside the string `"{"` would be found before the real opening +// brace, causing a misclassification. This is acceptable for a completion +// heuristic where rare edge cases degrade gracefully rather than break. + +/** + * Scans backward from the cursor, skipping whitespace and identifier characters + * (letters, digits, `_`, `$`, `.`, quotes), to find the nearest structural character. + * + * Identifier characters are skipped because the cursor may be mid-word + * (e.g., `{ ag|` โ€” cursor is after 'g', but context is 'key' from the `{`). + */ +function scanBackward(text: string, offset: number): ScanResult | undefined { + let i = offset - 1; + while (i >= 0) { + const ch = text[i]; + if (STRUCTURAL_CHARS.has(ch)) { + return { char: ch, index: i }; + } + // Skip whitespace and identifier-like characters + if (isSkippable(ch)) { + i--; + continue; + } + // Hit something unexpected (e.g., '}', ']', ')') โ€” stop scanning + // '}' and ']' indicate we've exited the current expression + return undefined; + } + return undefined; +} + +/** + * Characters to skip during backward scanning. + * These are characters that can appear between a structural char and the cursor: + * - whitespace + * - identifier chars (a-z, A-Z, 0-9, _, $, .) + * - quote marks (the user may be inside a quoted key) + * - minus sign (for negative numbers) + */ +function isSkippable(ch: string): boolean { + return /[\s\w.$"'`\-/]/.test(ch); +} + +/** + * Resolves context when ':' is found โ€” cursor is in a value position. + * + * Examples: + * - `{ _id: | }` โ†’ value with fieldName '_id' + * - `{ age: | }` โ†’ value with fieldName 'age' + */ +function resolveValueContext(text: string, colonIndex: number, fieldLookup?: FieldTypeLookup): CursorContext { + const fieldName = extractKeyBeforeColon(text, colonIndex); + if (!fieldName) { + return { position: 'unknown' }; + } + const fieldBsonType = fieldLookup?.(fieldName); + return { + position: 'value', + fieldName, + ...(fieldBsonType !== undefined && { fieldBsonType }), + }; +} + +/** + * Resolves context when '{' is found. + * + * Two sub-cases: + * 1. Root or top-level: `{ | }` โ†’ key position + * 2. After a colon: `{ age: { | } }` โ†’ operator position for field 'age' + */ +function resolveOpenBraceContext(text: string, braceIndex: number, fieldLookup?: FieldTypeLookup): CursorContext { + // Look backward from the '{' to find what precedes it + const beforeBrace = scanBackwardFrom(text, braceIndex); + + if (beforeBrace && beforeBrace.char === ':') { + // Pattern: `fieldName: { | }` โ†’ operator position + const fieldName = extractKeyBeforeColon(text, beforeBrace.index); + if (fieldName) { + // If the field name starts with '$', this is a nested query object + // inside a logical operator like $and: [ { | } ], but the immediate + // '{' is after a ':' which makes it an operator context + const fieldBsonType = fieldLookup?.(fieldName); + return { + position: 'operator', + fieldName, + ...(fieldBsonType !== undefined && { fieldBsonType }), + }; + } + } + + if (beforeBrace && beforeBrace.char === '[') { + // Pattern: `$and: [ { | } ]` โ†’ key at depth 1 + return resolveKeyInsideArray(text, beforeBrace.index); + } + + if (beforeBrace && beforeBrace.char === ',') { + // Pattern: `$and: [ {...}, { | } ]` โ€” inside an array after another element + return resolveCommaInsideArrayForBrace(text, beforeBrace.index); + } + + // Root object or can't determine parent + // +1 because the brace at braceIndex is the one we're inside + const depth = computeDepth(text, braceIndex) + 1; + return { position: 'key', depth }; +} + +/** + * Resolves context when ',' is found. + * + * Sub-cases: + * 1. Inside an object: `{ name: "x", | }` โ†’ key position + * 2. Inside an operator object: `{ age: { $gt: 5, | } }` โ†’ operator position + * 3. Inside an array: `{ $and: [ {...}, | ] }` โ†’ array-element position + */ +function resolveCommaContext(text: string, commaIndex: number, fieldLookup?: FieldTypeLookup): CursorContext { + // Determine if comma is inside an array or an object by finding the + // nearest unmatched '[' or '{' + const enclosing = findEnclosingBracket(text, commaIndex); + + if (!enclosing) { + return { position: 'unknown' }; + } + + if (enclosing.char === '[') { + // Inside an array โ€” determine parent operator + return resolveOpenBracketContext(text, enclosing.index); + } + + if (enclosing.char === '{') { + // Inside an object โ€” is this a root-level object or a nested operator object? + return resolveOpenBraceContext(text, enclosing.index, fieldLookup); + } + + return { position: 'unknown' }; +} + +/** + * Resolves context when '[' is found. + * + * Example: `{ $and: [ | ] }` โ†’ array-element with parentOperator '$and' + */ +function resolveOpenBracketContext(text: string, bracketIndex: number): CursorContext { + // Look backward from '[' to find the parent key via ':' + const beforeBracket = scanBackwardFrom(text, bracketIndex); + + if (beforeBracket && beforeBracket.char === ':') { + const parentKey = extractKeyBeforeColon(text, beforeBracket.index); + if (parentKey && parentKey.startsWith('$')) { + return { position: 'array-element', parentOperator: parentKey }; + } + } + + return { position: 'unknown' }; +} + +/** + * Resolves key context when '{' is found immediately after '['. + * Pattern: `$and: [ { | } ]` โ†’ key at depth 1 + */ +function resolveKeyInsideArray(text: string, bracketIndex: number): CursorContext { + // Check if this array belongs to a logical operator + const beforeBracket = scanBackwardFrom(text, bracketIndex); + if (beforeBracket && beforeBracket.char === ':') { + const parentKey = extractKeyBeforeColon(text, beforeBracket.index); + if (parentKey && parentKey.startsWith('$')) { + // Inside a logical operator array element โ€” treat as key context + const depth = computeDepth(text, bracketIndex); + return { position: 'key', depth: depth + 1 }; + } + } + const depth = computeDepth(text, bracketIndex); + return { position: 'key', depth: depth + 1 }; +} + +/** + * Resolves context when '{' is preceded by ',' inside an array. + * Pattern: `$and: [ {...}, { | } ]` + */ +function resolveCommaInsideArrayForBrace(text: string, commaIndex: number): CursorContext { + const enclosing = findEnclosingBracket(text, commaIndex); + if (enclosing && enclosing.char === '[') { + return resolveKeyInsideArray(text, enclosing.index); + } + return { position: 'key', depth: 0 }; +} + +// ---------- Character scanning utilities ---------- + +/** + * Scans backward from a given index (exclusive), skipping whitespace + * and identifier characters, to find the nearest structural character. + */ +function scanBackwardFrom(text: string, index: number): ScanResult | undefined { + let i = index - 1; + while (i >= 0) { + const ch = text[i]; + if (STRUCTURAL_CHARS.has(ch) || ch === ']' || ch === '}') { + if (ch === ']' || ch === '}') { + return undefined; // Hit a closing bracket โ€” stop + } + return { char: ch, index: i }; + } + if (isSkippable(ch)) { + i--; + continue; + } + return undefined; + } + return undefined; +} + +/** + * Finds the nearest unmatched opening bracket (`{` or `[`) before the given index. + * Properly handles nested brackets by maintaining a balance counter. + */ +function findEnclosingBracket(text: string, index: number): ScanResult | undefined { + let braceDepth = 0; + let bracketDepth = 0; + + for (let i = index - 1; i >= 0; i--) { + const ch = text[i]; + switch (ch) { + case '}': + braceDepth++; + break; + case '{': + if (braceDepth > 0) { + braceDepth--; + } else { + return { char: '{', index: i }; + } + break; + case ']': + bracketDepth++; + break; + case '[': + if (bracketDepth > 0) { + bracketDepth--; + } else { + return { char: '[', index: i }; + } + break; + } + } + return undefined; +} + +/** + * Extracts the key name immediately before a colon. + * + * Handles: + * - Unquoted keys: `age:` โ†’ 'age' + * - Single-quoted keys: `'my.field':` โ†’ 'my.field' + * - Double-quoted keys: `"my.field":` โ†’ 'my.field' + * - Dollar-prefixed: `$and:` โ†’ '$and' + */ +function extractKeyBeforeColon(text: string, colonIndex: number): string | undefined { + let i = colonIndex - 1; + + // Skip whitespace before the colon + while (i >= 0 && /\s/.test(text[i])) { + i--; + } + + if (i < 0) return undefined; + + // Check if the key is quoted + const quoteChar = text[i]; + if (quoteChar === '"' || quoteChar === "'") { + // Find the matching opening quote + const closeQuoteIndex = i; + i--; + while (i >= 0 && text[i] !== quoteChar) { + i--; + } + if (i < 0) return undefined; // Unmatched quote + return text.substring(i + 1, closeQuoteIndex); + } + + // Unquoted key โ€” collect identifier characters (including $ and .) + const end = i + 1; + while (i >= 0 && /[\w$.]/.test(text[i])) { + i--; + } + const key = text.substring(i + 1, end); + return key.length > 0 ? key : undefined; +} + +/** + * Computes the brace nesting depth at a given position. + * Counts unmatched `{` before the index. + */ +function computeDepth(text: string, index: number): number { + let depth = 0; + for (let i = 0; i < index; i++) { + if (text[i] === '{') depth++; + if (text[i] === '}') depth--; + } + return Math.max(0, depth); +} diff --git a/src/webviews/documentdbQuery/documentdbQueryCompletionProvider.test.ts b/src/webviews/documentdbQuery/documentdbQueryCompletionProvider.test.ts new file mode 100644 index 000000000..13521e3a0 --- /dev/null +++ b/src/webviews/documentdbQuery/documentdbQueryCompletionProvider.test.ts @@ -0,0 +1,1998 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + FILTER_COMPLETION_META, + getFilteredCompletions, + PROJECTION_COMPLETION_META, + type OperatorEntry, +} from '@vscode-documentdb/documentdb-constants'; +// eslint-disable-next-line import/no-internal-modules +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { clearAllCompletionContexts, setCompletionContext } from './completionStore'; +import { type CursorContext } from './cursorContext'; +import { + createCompletionItems, + createTypeSuggestions, + escapeSnippetDollars, + getCategoryLabel, + getCompletionKindForMeta, + getMetaTagsForEditorType, + getOperatorSortPrefix, + mapFieldToCompletionItem, + mapOperatorToCompletionItem, + stripOuterBraces, +} from './documentdbQueryCompletionProvider'; +import { EditorType } from './languageConfig'; + +/** + * Minimal mock of `monaco.languages.CompletionItemKind` for testing. + * Uses distinct numeric values matching Monaco's enum. + */ +const mockCompletionItemKind: typeof monacoEditor.languages.CompletionItemKind = { + Method: 0, + Function: 1, + Constructor: 2, + Field: 3, + Variable: 4, + Class: 5, + Struct: 6, + Interface: 7, + Module: 8, + Property: 9, + Event: 10, + Operator: 11, + Unit: 12, + Value: 13, + Constant: 14, + Enum: 15, + EnumMember: 16, + Keyword: 17, + Text: 18, + Color: 19, + File: 20, + Reference: 21, + Customcolor: 22, + Folder: 23, + TypeParameter: 24, + User: 25, + Issue: 26, + Snippet: 27, +}; + +/** Minimal mock of `monaco.languages.CompletionItemInsertTextRule`. */ +const mockInsertTextRule = { + InsertAsSnippet: 4, // Same value as Monaco + KeepWhitespace: 1, + None: 0, +} as typeof monacoEditor.languages.CompletionItemInsertTextRule; + +/** + * Creates a minimal Monaco API mock for testing completion provider functions. + */ +function createMockMonaco(): typeof monacoEditor { + return { + languages: { + CompletionItemKind: mockCompletionItemKind, + CompletionItemInsertTextRule: mockInsertTextRule, + }, + } as unknown as typeof monacoEditor; +} + +/** + * Extracts the label string from a CompletionItem's label, + * which may be a plain string or a CompletionItemLabel object. + */ +function getLabelText(label: string | monacoEditor.languages.CompletionItemLabel): string { + return typeof label === 'string' ? label : label.label; +} + +/** Standard test range for all completion items. */ +const testRange: monacoEditor.IRange = { + startLineNumber: 1, + endLineNumber: 1, + startColumn: 1, + endColumn: 1, +}; + +describe('documentdbQueryCompletionProvider', () => { + describe('getCompletionKindForMeta', () => { + const kinds = mockCompletionItemKind; + + test('maps query operators to Operator kind', () => { + expect(getCompletionKindForMeta('query', kinds)).toBe(kinds.Operator); + expect(getCompletionKindForMeta('query:comparison', kinds)).toBe(kinds.Operator); + expect(getCompletionKindForMeta('query:logical', kinds)).toBe(kinds.Operator); + }); + + test('maps expression operators to Function kind', () => { + expect(getCompletionKindForMeta('expr:arith', kinds)).toBe(kinds.Function); + expect(getCompletionKindForMeta('expr:string', kinds)).toBe(kinds.Function); + }); + + test('maps BSON constructors to Constructor kind', () => { + expect(getCompletionKindForMeta('bson', kinds)).toBe(kinds.Constructor); + }); + + test('maps stages to Module kind', () => { + expect(getCompletionKindForMeta('stage', kinds)).toBe(kinds.Module); + }); + + test('maps accumulators to Method kind', () => { + expect(getCompletionKindForMeta('accumulator', kinds)).toBe(kinds.Method); + }); + + test('maps update operators to Property kind', () => { + expect(getCompletionKindForMeta('update', kinds)).toBe(kinds.Property); + }); + + test('maps variables to Variable kind', () => { + expect(getCompletionKindForMeta('variable', kinds)).toBe(kinds.Variable); + }); + + test('maps window operators to Event kind', () => { + expect(getCompletionKindForMeta('window', kinds)).toBe(kinds.Event); + }); + + test('maps field identifiers to Field kind', () => { + expect(getCompletionKindForMeta('field:identifier', kinds)).toBe(kinds.Field); + }); + + test('maps unknown meta to Text kind', () => { + expect(getCompletionKindForMeta('unknown', kinds)).toBe(kinds.Text); + }); + }); + + describe('mapOperatorToCompletionItem', () => { + const mockMonaco = createMockMonaco(); + + test('maps a simple operator entry without snippet', () => { + const entry: OperatorEntry = { + value: '$eq', + meta: 'query:comparison', + description: 'Matches values equal to a specified value.', + }; + + const item = mapOperatorToCompletionItem(entry, testRange, mockMonaco); + + expect(getLabelText(item.label)).toBe('$eq'); + expect(item.kind).toBe(mockCompletionItemKind.Operator); + expect(item.insertText).toBe('$eq'); + expect(item.insertTextRules).toBeUndefined(); + expect((item.documentation as { value: string }).value).toContain( + 'Matches values equal to a specified value.', + ); + expect(item.range).toBe(testRange); + }); + + test('maps an operator entry with snippet', () => { + const entry: OperatorEntry = { + value: '$gt', + meta: 'query:comparison', + description: 'Greater than', + snippet: '{ $gt: ${1:value} }', + }; + + const item = mapOperatorToCompletionItem(entry, testRange, mockMonaco); + + expect(getLabelText(item.label)).toBe('$gt'); + expect(item.insertText).toBe('{ \\$gt: ${1:value} }'); + expect(item.insertTextRules).toBe(mockInsertTextRule.InsertAsSnippet); + }); + + test('maps a BSON constructor with link', () => { + const entry: OperatorEntry = { + value: 'ObjectId', + meta: 'bson', + description: 'Creates a new ObjectId value.', + snippet: 'ObjectId("${1:hex}")', + link: 'https://docs.example.com/objectid', + }; + + const item = mapOperatorToCompletionItem(entry, testRange, mockMonaco); + + expect(getLabelText(item.label)).toBe('ObjectId'); + expect(item.kind).toBe(mockCompletionItemKind.Constructor); + expect(item.insertText).toBe('ObjectId("${1:hex}")'); + expect(item.insertTextRules).toBe(mockInsertTextRule.InsertAsSnippet); + const docValue = (item.documentation as { value: string }).value; + expect(docValue).toContain('Creates a new ObjectId value.'); + expect(docValue).toContain('https://docs.example.com/objectid'); + }); + + test('uses the provided range', () => { + const customRange: monacoEditor.IRange = { + startLineNumber: 3, + endLineNumber: 3, + startColumn: 5, + endColumn: 10, + }; + + const entry: OperatorEntry = { + value: '$in', + meta: 'query:comparison', + description: 'Matches any value in an array.', + }; + + const item = mapOperatorToCompletionItem(entry, customRange, mockMonaco); + expect(item.range).toBe(customRange); + }); + }); + + describe('getMetaTagsForEditorType', () => { + test('returns FILTER_COMPLETION_META for Filter editor type', () => { + const tags = getMetaTagsForEditorType(EditorType.Filter); + expect(tags).toBe(FILTER_COMPLETION_META); + }); + + test('returns PROJECTION_COMPLETION_META for Project editor type', () => { + const tags = getMetaTagsForEditorType(EditorType.Project); + expect(tags).toBe(PROJECTION_COMPLETION_META); + }); + + test('returns PROJECTION_COMPLETION_META for Sort editor type', () => { + const tags = getMetaTagsForEditorType(EditorType.Sort); + expect(tags).toBe(PROJECTION_COMPLETION_META); + }); + + test('returns FILTER_COMPLETION_META for undefined (fallback)', () => { + const tags = getMetaTagsForEditorType(undefined); + expect(tags).toBe(FILTER_COMPLETION_META); + }); + }); + + describe('createCompletionItems', () => { + const mockMonaco = createMockMonaco(); + + afterEach(() => { + clearAllCompletionContexts(); + }); + + test('returns items for filter context using documentdb-constants', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + // Should return the filter completions from documentdb-constants + expect(items.length).toBeGreaterThan(0); + + // All items should have required CompletionItem properties + for (const item of items) { + expect(item.label).toBeDefined(); + expect(getLabelText(item.label)).toBeDefined(); + expect(item.kind).toBeDefined(); + expect(item.insertText).toBeDefined(); + expect(item.range).toBe(testRange); + } + }); + + test('filter completions include query operators like $eq, $gt, $match at value position', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'value', fieldName: 'x' }, + }); + + const labels = items.map((item) => getLabelText(item.label)); + expect(labels).toContain('$eq'); + expect(labels).toContain('$gt'); + expect(labels).toContain('$in'); + }); + + test('filter completions include BSON constructors like ObjectId at value position', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'value', fieldName: 'x' }, + }); + + const labels = items.map((item) => getLabelText(item.label)); + expect(labels).toContain('ObjectId'); + expect(labels).toContain('UUID'); + expect(labels).toContain('ISODate'); + }); + + test('filter completions do NOT include JS globals like console, Math, function', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + const labels = items.map((item) => getLabelText(item.label)); + expect(labels).not.toContain('console'); + expect(labels).not.toContain('Math'); + expect(labels).not.toContain('function'); + expect(labels).not.toContain('window'); + expect(labels).not.toContain('document'); + expect(labels).not.toContain('Array'); + expect(labels).not.toContain('Object'); + expect(labels).not.toContain('String'); + }); + + test('filter completions do NOT include aggregation stages', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + const labels = items.map((item) => getLabelText(item.label)); + // $match is a query operator AND a stage, but $group/$unwind are stage-only + expect(labels).not.toContain('$group'); + expect(labels).not.toContain('$unwind'); + expect(labels).not.toContain('$lookup'); + }); + + test('filter completions at value position match getFilteredCompletions count for FILTER_COMPLETION_META', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'value', fieldName: 'x' }, + }); + + const expected = getFilteredCompletions({ meta: [...FILTER_COMPLETION_META] }); + // Value position includes operators + BSON constructors (minus key-position operators) + expect(items.length).toBeGreaterThan(0); + expect(items.length).toBeLessThanOrEqual(expected.length); + }); + + test('default (undefined editor type) matches filter completions', () => { + const filterItems = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + const defaultItems = createCompletionItems({ + editorType: undefined, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + expect(defaultItems).toHaveLength(filterItems.length); + }); + }); + + describe('mapFieldToCompletionItem', () => { + const mockMonaco = createMockMonaco(); + + test('maps a simple field to a CompletionItem', () => { + const field = { + fieldName: 'age', + displayType: 'Number', + bsonType: 'int32', + isSparse: false, + insertText: 'age', + referenceText: '$age', + }; + + const item = mapFieldToCompletionItem(field, testRange, mockMonaco); + + expect(item.label).toEqual({ label: 'age', description: 'Number' }); + expect(item.kind).toBe(mockCompletionItemKind.Field); + expect(item.insertText).toBe('age: $1'); + expect(item.insertTextRules).toBe(mockInsertTextRule.InsertAsSnippet); + expect(item.sortText).toBe('0_age'); + expect(item.range).toBe(testRange); + }); + + test('includes (sparse) indicator for sparse fields', () => { + const field = { + fieldName: 'optionalField', + displayType: 'String', + bsonType: 'string', + isSparse: true, + insertText: 'optionalField', + referenceText: '$optionalField', + }; + + const item = mapFieldToCompletionItem(field, testRange, mockMonaco); + + expect((item.label as { description: string }).description).toBe('String (sparse)'); + }); + + test('uses pre-escaped insertText for special field names', () => { + const field = { + fieldName: 'address.city', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: '"address.city"', + referenceText: '$address.city', + }; + + const item = mapFieldToCompletionItem(field, testRange, mockMonaco); + + expect((item.label as { label: string }).label).toBe('address.city'); + expect(item.insertText).toBe('"address.city": $1'); + }); + }); + + describe('field completions via store', () => { + const mockMonaco = createMockMonaco(); + + afterEach(() => { + clearAllCompletionContexts(); + }); + + test('field completions appear when store has data', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + { + fieldName: 'age', + displayType: 'Number', + bsonType: 'int32', + isSparse: false, + insertText: 'age', + referenceText: '$age', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('name'); + expect(labels).toContain('age'); + }); + + test('field completions have sortText prefix so they sort first', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + const fieldItem = items.find((i) => getLabelText(i.label) === 'name'); + expect(fieldItem?.sortText).toBe('0_name'); + }); + + test('empty store returns all operator completions', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'nonexistent-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + // Without cursorContext, falls back to all completions + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('$and'); + expect(labels).toContain('$or'); + expect(labels).toContain('$gt'); + }); + + test('undefined sessionId returns all operator completions', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + // Without cursorContext, falls back to all completions + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('$and'); + expect(labels).toContain('$or'); + expect(labels).toContain('$gt'); + }); + }); + + describe('getOperatorSortPrefix', () => { + test('returns undefined when no fieldBsonTypes provided', () => { + const entry: OperatorEntry = { + value: '$eq', + meta: 'query:comparison', + description: 'Equals', + }; + expect(getOperatorSortPrefix(entry, undefined)).toBeUndefined(); + expect(getOperatorSortPrefix(entry, [])).toBeUndefined(); + }); + + test('returns "1a_" for universal comparison operator (no applicableBsonTypes)', () => { + const entry: OperatorEntry = { + value: '$eq', + meta: 'query:comparison', + description: 'Equals', + }; + expect(getOperatorSortPrefix(entry, ['string'])).toBe('1a_'); + }); + + test('returns "1b_" for universal non-comparison operator', () => { + const entry: OperatorEntry = { + value: '$exists', + meta: 'query:element', + description: 'Exists', + }; + expect(getOperatorSortPrefix(entry, ['string'])).toBe('1b_'); + }); + + test('returns "0_" for type-relevant operator (applicableBsonTypes matches)', () => { + const entry: OperatorEntry = { + value: '$regex', + meta: 'query:evaluation', + description: 'Regex match', + applicableBsonTypes: ['string'], + }; + expect(getOperatorSortPrefix(entry, ['string'])).toBe('0_'); + }); + + test('returns "2_" for non-matching operator (applicableBsonTypes does not match)', () => { + const entry: OperatorEntry = { + value: '$regex', + meta: 'query:evaluation', + description: 'Regex match', + applicableBsonTypes: ['string'], + }; + expect(getOperatorSortPrefix(entry, ['int32'])).toBe('2_'); + }); + + test('handles polymorphic fields (multiple bsonTypes)', () => { + const regexEntry: OperatorEntry = { + value: '$regex', + meta: 'query:evaluation', + description: 'Regex match', + applicableBsonTypes: ['string'], + }; + // Field is sometimes string, sometimes int32 โ€” $regex should match + expect(getOperatorSortPrefix(regexEntry, ['int32', 'string'])).toBe('0_'); + }); + + test('returns "2_" when operator types and field types have no intersection', () => { + const sizeEntry: OperatorEntry = { + value: '$size', + meta: 'query:array', + description: 'Array size', + applicableBsonTypes: ['array'], + }; + expect(getOperatorSortPrefix(sizeEntry, ['string', 'int32'])).toBe('2_'); + }); + }); + + describe('type-aware operator sorting in mapOperatorToCompletionItem', () => { + const mockMonaco = createMockMonaco(); + + test('sortText is undefined when no fieldBsonTypes provided', () => { + const entry: OperatorEntry = { + value: '$eq', + meta: 'query:comparison', + description: 'Equals', + }; + const item = mapOperatorToCompletionItem(entry, testRange, mockMonaco); + expect(item.sortText).toBeUndefined(); + }); + + test('sortText is undefined when empty fieldBsonTypes provided', () => { + const entry: OperatorEntry = { + value: '$eq', + meta: 'query:comparison', + description: 'Equals', + }; + const item = mapOperatorToCompletionItem(entry, testRange, mockMonaco, []); + expect(item.sortText).toBeUndefined(); + }); + + test('universal comparison operator gets "1a_" prefix when fieldBsonTypes provided', () => { + const entry: OperatorEntry = { + value: '$eq', + meta: 'query:comparison', + description: 'Equals', + }; + const item = mapOperatorToCompletionItem(entry, testRange, mockMonaco, ['int32']); + expect(item.sortText).toBe('1a_$eq'); + }); + + test('type-relevant operator gets "0_" prefix', () => { + const entry: OperatorEntry = { + value: '$regex', + meta: 'query:evaluation', + description: 'Regex match', + applicableBsonTypes: ['string'], + }; + const item = mapOperatorToCompletionItem(entry, testRange, mockMonaco, ['string']); + expect(item.sortText).toBe('0_$regex'); + }); + + test('non-matching operator gets "2_" prefix (demoted, not hidden)', () => { + const entry: OperatorEntry = { + value: '$regex', + meta: 'query:evaluation', + description: 'Regex match', + applicableBsonTypes: ['string'], + }; + const item = mapOperatorToCompletionItem(entry, testRange, mockMonaco, ['int32']); + expect(item.sortText).toBe('2_$regex'); + }); + }); + + describe('type-aware sorting via createCompletionItems', () => { + const mockMonaco = createMockMonaco(); + + afterEach(() => { + clearAllCompletionContexts(); + }); + + test('without fieldBsonTypes, operators have no sortText at value position', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'value', fieldName: 'x' }, + }); + + const regexItem = items.find((i) => getLabelText(i.label) === '$regex'); + // At value position, operators get sort prefix 0_ (not type-aware) + expect(regexItem?.sortText).toBe('0_$regex'); + + const eqItem = items.find((i) => getLabelText(i.label) === '$eq'); + expect(eqItem?.sortText).toBe('0_$eq'); + }); + + test('with fieldBsonTypes=["string"] at operator position, $regex gets "0_" and $size gets "2_"', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + fieldBsonTypes: ['string'], + cursorContext: { position: 'operator', fieldName: 'x' }, + }); + + const regexItem = items.find((i) => getLabelText(i.label) === '$regex'); + expect(regexItem?.sortText).toBe('0_$regex'); + + const sizeItem = items.find((i) => getLabelText(i.label) === '$size'); + expect(sizeItem?.sortText).toBe('2_$size'); + + // Comparison operators like $eq get "1a_" (promoted over other universals) + const eqItem = items.find((i) => getLabelText(i.label) === '$eq'); + expect(eqItem?.sortText).toBe('1a_$eq'); + }); + + test('with fieldBsonTypes=["int32"] at operator position, $regex gets "2_" (demoted, still present)', () => { + const context: CursorContext = { position: 'operator', fieldName: 'x' }; + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + fieldBsonTypes: ['int32'], + cursorContext: context, + }); + + const labels = items.map((i) => getLabelText(i.label)); + // $regex is still in the list, just demoted + expect(labels).toContain('$regex'); + + const regexItem = items.find((i) => getLabelText(i.label) === '$regex'); + expect(regexItem?.sortText).toBe('2_$regex'); + + // Bitwise operators should match int + const bitsAllSetItem = items.find((i) => getLabelText(i.label) === '$bitsAllSet'); + expect(bitsAllSetItem?.sortText).toBe('0_$bitsAllSet'); + }); + + test('all operators still present regardless of fieldBsonTypes at operator position', () => { + const itemsWithoutType = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'operator', fieldName: 'x' }, + }); + + const itemsWithType = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + fieldBsonTypes: ['int32'], + cursorContext: { position: 'operator', fieldName: 'x' }, + }); + + // Same number of items โ€” nothing filtered out + expect(itemsWithType).toHaveLength(itemsWithoutType.length); + }); + + test('field items still get "0_" prefix even when fieldBsonTypes is set', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'age', + displayType: 'Number', + bsonType: 'int32', + isSparse: false, + insertText: 'age', + referenceText: '$age', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + fieldBsonTypes: ['int32'], + cursorContext: { position: 'key', depth: 1 }, + }); + + const fieldItem = items.find((i) => getLabelText(i.label) === 'age'); + expect(fieldItem?.sortText).toBe('0_age'); + }); + }); + + describe('stripOuterBraces', () => { + test('strips outer { } from operator snippets', () => { + expect(stripOuterBraces('{ $gt: ${1:value} }')).toBe('$gt: ${1:value}'); + }); + + test('preserves inner brackets', () => { + expect(stripOuterBraces('{ $in: [${1:value}] }')).toBe('$in: [${1:value}]'); + }); + + test('preserves inner braces', () => { + expect(stripOuterBraces('{ $elemMatch: { ${1:query} } }')).toBe('$elemMatch: { ${1:query} }'); + }); + + test('returns unchanged if not wrapped', () => { + expect(stripOuterBraces('ObjectId("${1:hex}")')).toBe('ObjectId("${1:hex}")'); + }); + + test('returns unchanged for non-matching patterns', () => { + expect(stripOuterBraces('$gt')).toBe('$gt'); + }); + }); + + describe('getCategoryLabel', () => { + test('extracts sub-category from qualified meta tag', () => { + expect(getCategoryLabel('query:comparison')).toBe('comparison'); + expect(getCategoryLabel('query:logical')).toBe('logical'); + expect(getCategoryLabel('query:element')).toBe('element'); + expect(getCategoryLabel('query:array')).toBe('array'); + }); + + test('returns whole tag when no colon', () => { + expect(getCategoryLabel('bson')).toBe('bson'); + expect(getCategoryLabel('variable')).toBe('variable'); + }); + }); + + describe('escapeSnippetDollars', () => { + test('escapes $ before operator names in snippets', () => { + expect(escapeSnippetDollars('{ $gt: ${1:value} }')).toBe('{ \\$gt: ${1:value} }'); + }); + + test('preserves tab stop syntax', () => { + expect(escapeSnippetDollars('${1:value}')).toBe('${1:value}'); + expect(escapeSnippetDollars('$1')).toBe('$1'); + }); + + test('escapes multiple operator names', () => { + expect(escapeSnippetDollars('{ $and: [{ $gt: ${1:value} }] }')).toBe('{ \\$and: [{ \\$gt: ${1:value} }] }'); + }); + + test('does not escape BSON constructor snippets', () => { + expect(escapeSnippetDollars('ObjectId("${1:hex}")')).toBe('ObjectId("${1:hex}")'); + }); + + test('escapes stripped operator snippets', () => { + expect(escapeSnippetDollars('$gt: ${1:value}')).toBe('\\$gt: ${1:value}'); + expect(escapeSnippetDollars('$in: [${1:value}]')).toBe('\\$in: [${1:value}]'); + }); + }); + + // --------------------------------------------------------------- + // Context-sensitive completions (Step 4.5) + // --------------------------------------------------------------- + describe('context-sensitive completions', () => { + const mockMonaco = createMockMonaco(); + + afterEach(() => { + clearAllCompletionContexts(); + }); + + describe('key position', () => { + const keyContext: CursorContext = { position: 'key', depth: 1 }; + + test('shows field names when store has data', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('name'); + }); + + test('shows key-position operators ($and, $or, $nor)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('$and'); + expect(labels).toContain('$or'); + expect(labels).toContain('$nor'); + expect(labels).toContain('$comment'); + expect(labels).toContain('$expr'); + // $not is a field-level operator, NOT a key-position operator + expect(labels).not.toContain('$not'); + }); + + test('does NOT show value-level operators ($gt, $lt, $regex, $eq)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('$gt'); + expect(labels).not.toContain('$lt'); + expect(labels).not.toContain('$regex'); + expect(labels).not.toContain('$eq'); + expect(labels).not.toContain('$in'); + expect(labels).not.toContain('$exists'); + }); + + test('does NOT show BSON constructors', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('ObjectId'); + expect(labels).not.toContain('UUID'); + expect(labels).not.toContain('ISODate'); + }); + + test('fields sort before operators', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'age', + displayType: 'Number', + bsonType: 'int32', + isSparse: false, + insertText: 'age', + referenceText: '$age', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + + const fieldItem = items.find((i) => getLabelText(i.label) === 'age'); + const andItem = items.find((i) => getLabelText(i.label) === '$and'); + expect(fieldItem?.sortText).toBe('0_age'); + expect(andItem?.sortText).toBe('1_$and'); + }); + }); + + describe('value position', () => { + const valueContext: CursorContext = { position: 'value', fieldName: 'age' }; + + test('shows BSON constructors', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('ObjectId'); + expect(labels).toContain('UUID'); + expect(labels).toContain('ISODate'); + }); + + test('shows query operators (with brace-wrapping snippets)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('$gt'); + expect(labels).toContain('$eq'); + expect(labels).toContain('$in'); + + // Operators should have their full brace-wrapping snippets at value position + const gtItem = items.find((i) => getLabelText(i.label) === '$gt'); + expect(gtItem?.insertText).toBe('{ \\$gt: ${1:value} }'); + }); + + test('operators sort before BSON constructors', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const gtItem = items.find((i) => getLabelText(i.label) === '$gt'); + const objectIdItem = items.find((i) => getLabelText(i.label) === 'ObjectId'); + expect(gtItem?.sortText).toBe('0_$gt'); + expect(objectIdItem?.sortText).toBe('3_ObjectId'); + }); + + test('includes JS globals and common methods after BSON constructors', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + // Class constructors + expect(labels).toContain('Date'); + expect(labels).toContain('RegExp'); + // Static methods + expect(labels).toContain('Date.now()'); + expect(labels).toContain('Math.floor()'); + expect(labels).toContain('Math.min()'); + expect(labels).toContain('Math.max()'); + // Primitives + expect(labels).toContain('Infinity'); + + // JS globals sort after BSON constructors (4_ > 3_) + const dateItem = items.find((i) => getLabelText(i.label) === 'Date'); + expect(dateItem?.sortText).toBe('4_Date'); + }); + + test('does NOT show key-position operators ($and, $or)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('$and'); + expect(labels).not.toContain('$or'); + }); + + test('does NOT show field names', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('name'); + }); + }); + + describe('operator position', () => { + const operatorContext: CursorContext = { position: 'operator', fieldName: 'age' }; + + test('shows comparison operators ($gt, $lt, $eq, $in) and $not', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: operatorContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('$gt'); + expect(labels).toContain('$lt'); + expect(labels).toContain('$eq'); + expect(labels).toContain('$in'); + expect(labels).toContain('$exists'); + expect(labels).toContain('$regex'); + // $not is a field-level operator, valid at operator position + expect(labels).toContain('$not'); + }); + + test('does NOT show key-position operators ($and, $or)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: operatorContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('$and'); + expect(labels).not.toContain('$or'); + expect(labels).not.toContain('$nor'); + }); + + test('does NOT show BSON constructors', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: operatorContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('ObjectId'); + expect(labels).not.toContain('UUID'); + }); + + test('does NOT show field names', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: operatorContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('name'); + }); + + test('applies type-aware sorting when fieldBsonType is available', () => { + const typedContext: CursorContext = { + position: 'operator', + fieldName: 'age', + fieldBsonType: 'int32', + }; + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: typedContext, + }); + + // $regex has applicableBsonTypes=['string'], doesn't match 'int32' โ†’ demoted + const regexItem = items.find((i) => getLabelText(i.label) === '$regex'); + expect(regexItem?.sortText).toBe('2_$regex'); + + // $bitsAllSet has applicableBsonTypes containing 'int32' โ†’ promoted + const bitsItem = items.find((i) => getLabelText(i.label) === '$bitsAllSet'); + expect(bitsItem?.sortText).toBe('0_$bitsAllSet'); + + // $eq is universal comparison โ†’ promoted tier + const eqItem = items.find((i) => getLabelText(i.label) === '$eq'); + expect(eqItem?.sortText).toBe('1a_$eq'); + }); + + test('strips outer braces from operator snippets (Issue A fix)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: operatorContext, + }); + + // At operator position, snippets should NOT have outer { } + const gtItem = items.find((i) => getLabelText(i.label) === '$gt'); + expect(gtItem?.insertText).toBe('\\$gt: ${1:value}'); + + const inItem = items.find((i) => getLabelText(i.label) === '$in'); + expect(inItem?.insertText).toBe('\\$in: [${1:value}]'); + + const regexItem = items.find((i) => getLabelText(i.label) === '$regex'); + expect(regexItem?.insertText).toBe('\\$regex: /${1:pattern}/'); + }); + }); + + describe('array-element position', () => { + const arrayContext: CursorContext = { position: 'array-element', parentOperator: '$and' }; + + test('behaves like key position (shows fields + key operators)', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'age', + displayType: 'Number', + bsonType: 'int32', + isSparse: false, + insertText: 'age', + referenceText: '$age', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: arrayContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + // Should include fields + expect(labels).toContain('age'); + // Should include key-position operators + expect(labels).toContain('$and'); + expect(labels).toContain('$or'); + // Should NOT include value-level operators + expect(labels).not.toContain('$gt'); + expect(labels).not.toContain('$regex'); + // Should NOT include BSON constructors + expect(labels).not.toContain('ObjectId'); + }); + }); + + describe('unknown position', () => { + const unknownContext: CursorContext = { position: 'unknown' }; + + test('falls back to all completions', () => { + const itemsWithContext = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: unknownContext, + }); + + const itemsWithoutContext = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + }); + + // Both should produce the same all-completions list + expect(itemsWithContext).toHaveLength(itemsWithoutContext.length); + const labels = itemsWithContext.map((i) => getLabelText(i.label)); + // All completions include key-position operators + expect(labels).toContain('$and'); + expect(labels).toContain('$or'); + // Also include value-position operators and BSON constructors + expect(labels).toContain('$gt'); + expect(labels).toContain('ObjectId'); + }); + }); + + describe('no cursorContext (undefined)', () => { + test('falls back to all completions (fields + operators + BSON + JS globals)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: undefined, + }); + + // Without cursorContext, shows all completions + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('$and'); + expect(labels).toContain('$or'); + expect(labels).toContain('$gt'); + expect(labels).toContain('ObjectId'); + }); + }); + + describe('needsWrapping (empty editor, no braces)', () => { + test('field insertText is wrapped with { } when needsWrapping is true', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'unknown' }, + needsWrapping: true, + }); + + const fieldItem = items.find((i) => getLabelText(i.label) === 'name'); + expect(fieldItem?.insertText).toBe('{ name: $1 }'); + }); + + test('field insertText is NOT wrapped when needsWrapping is false', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'unknown' }, + needsWrapping: false, + }); + + const fieldItem = items.find((i) => getLabelText(i.label) === 'name'); + expect(fieldItem?.insertText).toBe('name: $1'); + }); + + test('operators keep full brace-wrapping snippets when needsWrapping is true', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'unknown' }, + needsWrapping: true, + }); + + // Operator snippets include { } already โ€” they should NOT be stripped + const andItem = items.find((i) => getLabelText(i.label) === '$and'); + expect(andItem?.insertText).toContain('{'); + expect(andItem?.insertText).toContain('}'); + }); + }); + + // --------------------------------------------------------------- + // Category coverage: verify operator categories at each position + // --------------------------------------------------------------- + describe('operator category coverage by position', () => { + test('key position: only key-position operators, no field-level operators', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'key', depth: 1 }, + }); + + const labels = items.map((i) => getLabelText(i.label)); + // Key-position: logical combinators and meta operators + expect(labels).toContain('$and'); // query:logical + expect(labels).toContain('$or'); // query:logical + expect(labels).toContain('$nor'); // query:logical + expect(labels).toContain('$comment'); // query:comment + expect(labels).toContain('$expr'); // query:expr + // Field-level operators must NOT appear at key position + expect(labels).not.toContain('$all'); // query:array โ€” field-level + expect(labels).not.toContain('$elemMatch'); // query:array โ€” field-level + expect(labels).not.toContain('$size'); // query:array โ€” field-level + expect(labels).not.toContain('$gt'); // query:comparison + expect(labels).not.toContain('$regex'); // query:evaluation + expect(labels).not.toContain('$exists'); // query:element + expect(labels).not.toContain('$not'); // query:logical โ€” field-level + }); + + test('value position: includes operators from all field-level categories', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'value', fieldName: 'x' }, + }); + + const labels = items.map((i) => getLabelText(i.label)); + // Should include field-level operators from every category + expect(labels).toContain('$gt'); // query:comparison + expect(labels).toContain('$eq'); // query:comparison + expect(labels).toContain('$in'); // query:comparison + expect(labels).toContain('$regex'); // query:evaluation + expect(labels).toContain('$exists'); // query:element + expect(labels).toContain('$type'); // query:element + expect(labels).toContain('$all'); // query:array + expect(labels).toContain('$elemMatch'); // query:array + expect(labels).toContain('$size'); // query:array + expect(labels).toContain('$not'); // query:logical (field-level) + // Key-position operators should NOT be at value position + expect(labels).not.toContain('$and'); + expect(labels).not.toContain('$or'); + }); + + test('operator position: same field-level categories as value position', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'operator', fieldName: 'x' }, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('$gt'); // query:comparison + expect(labels).toContain('$regex'); // query:evaluation + expect(labels).toContain('$exists'); // query:element + expect(labels).toContain('$all'); // query:array + expect(labels).toContain('$not'); // query:logical (field-level) + expect(labels).not.toContain('$and'); // key-position only + }); + }); + }); + + // --------------------------------------------------------------- + // Type-aware value suggestions + // --------------------------------------------------------------- + describe('createTypeSuggestions', () => { + const mockMonaco = createMockMonaco(); + + test('returns empty array for undefined bsonType', () => { + const items = createTypeSuggestions(undefined, testRange, mockMonaco); + expect(items).toHaveLength(0); + }); + + test('returns empty array for unknown bsonType', () => { + const items = createTypeSuggestions('unknownType', testRange, mockMonaco); + expect(items).toHaveLength(0); + }); + + test('returns true/false for boolean fields', () => { + const items = createTypeSuggestions('boolean', testRange, mockMonaco); + expect(items).toHaveLength(2); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('true'); + expect(labels).toContain('false'); + + // Plain text, not snippets + const trueItem = items.find((i) => getLabelText(i.label) === 'true'); + expect(trueItem?.insertText).toBe('true'); + expect(trueItem?.insertTextRules).toBeUndefined(); + expect(trueItem?.kind).toBe(mockCompletionItemKind.Value); + }); + + test('returns range query for int fields', () => { + const items = createTypeSuggestions('int32', testRange, mockMonaco); + expect(items.length).toBeGreaterThanOrEqual(1); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels[0]).toContain('$gt'); + expect(labels[0]).toContain('$lt'); + + // Should be a snippet + expect(items[0].kind).toBe(mockCompletionItemKind.Snippet); + }); + + test('returns regex and empty string for string fields', () => { + const items = createTypeSuggestions('string', testRange, mockMonaco); + expect(items.length).toBeGreaterThanOrEqual(1); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('{ $regex: /โ€ฆ/ }'); + }); + + test('returns ISODate for date fields', () => { + const items = createTypeSuggestions('date', testRange, mockMonaco); + expect(items.length).toBeGreaterThanOrEqual(1); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('ISODate("โ€ฆ")'); + }); + + test('returns ObjectId for objectid fields', () => { + const items = createTypeSuggestions('objectid', testRange, mockMonaco); + expect(items).toHaveLength(1); + + expect(getLabelText(items[0].label)).toBe('ObjectId("โ€ฆ")'); + }); + + test('returns null for null fields', () => { + const items = createTypeSuggestions('null', testRange, mockMonaco); + expect(items).toHaveLength(1); + + expect(getLabelText(items[0].label)).toBe('null'); + expect(items[0].insertText).toBe('null'); + }); + + test('returns elemMatch and size for array fields', () => { + const items = createTypeSuggestions('array', testRange, mockMonaco); + expect(items.length).toBeGreaterThanOrEqual(2); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('{ $elemMatch: { โ€ฆ } }'); + expect(labels).toContain('{ $size: โ€ฆ }'); + }); + + test('suggestions have sort prefix 00_ (highest priority)', () => { + const items = createTypeSuggestions('boolean', testRange, mockMonaco); + for (const item of items) { + expect(item.sortText).toMatch(/^00_/); + } + }); + + test('first suggestion is preselected', () => { + const items = createTypeSuggestions('int32', testRange, mockMonaco); + expect(items[0].preselect).toBe(true); + }); + }); + + describe('type suggestions in value position integration', () => { + const mockMonaco = createMockMonaco(); + + test('boolean field at value position shows true/false first', () => { + const context: CursorContext = { position: 'value', fieldName: 'isActive', fieldBsonType: 'boolean' }; + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + const labels = items.map((i) => getLabelText(i.label)); + // true/false should be present + expect(labels).toContain('true'); + expect(labels).toContain('false'); + + // Operators should also be present + expect(labels).toContain('$eq'); + expect(labels).toContain('$gt'); + + // true/false should sort before operators (00_ < 0_) + const trueItem = items.find((i) => getLabelText(i.label) === 'true'); + const eqItem = items.find((i) => getLabelText(i.label) === '$eq'); + expect(trueItem!.sortText! < eqItem!.sortText!).toBe(true); + }); + + test('int field at value position shows range query first', () => { + const context: CursorContext = { position: 'value', fieldName: 'age', fieldBsonType: 'int32' }; + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + // Range query suggestion should be first (sort 00_00) + const first = items[0]; + expect(getLabelText(first.label)).toContain('$gt'); + expect(first.sortText).toBe('00_00'); + }); + + test('unknown type at value position has no type suggestions', () => { + const context: CursorContext = { position: 'value', fieldName: 'data' }; + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + // No type suggestions, but operators and BSON should still be present + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('$eq'); + expect(labels).toContain('ObjectId'); + + // No items with 00_ sort prefix + expect(items.filter((i) => i.sortText?.startsWith('00_'))).toHaveLength(0); + }); + }); + + // --------------------------------------------------------------- + // Project and Sort value completions + // --------------------------------------------------------------- + describe('project editor value completions', () => { + const mockMonaco = createMockMonaco(); + + test('shows 1 (include) and 0 (exclude) at value position', () => { + const context: CursorContext = { position: 'value', fieldName: 'name' }; + const items = createCompletionItems({ + editorType: EditorType.Project, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + expect(items).toHaveLength(2); + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('1'); + expect(labels).toContain('0'); + }); + + test('1 (include) has description "include field"', () => { + const context: CursorContext = { position: 'value', fieldName: 'name' }; + const items = createCompletionItems({ + editorType: EditorType.Project, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + const includeItem = items.find((i) => getLabelText(i.label) === '1'); + expect((includeItem?.label as { description: string }).description).toBe('include field'); + }); + + test('does NOT show operators, BSON constructors, or JS globals', () => { + const context: CursorContext = { position: 'value', fieldName: 'name' }; + const items = createCompletionItems({ + editorType: EditorType.Project, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('$gt'); + expect(labels).not.toContain('ObjectId'); + expect(labels).not.toContain('Date'); + }); + + test('1 is preselected', () => { + const context: CursorContext = { position: 'value', fieldName: 'name' }; + const items = createCompletionItems({ + editorType: EditorType.Project, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + const includeItem = items.find((i) => getLabelText(i.label) === '1'); + expect(includeItem?.preselect).toBe(true); + }); + }); + + describe('sort editor value completions', () => { + const mockMonaco = createMockMonaco(); + + test('shows 1 (ascending) and -1 (descending) at value position', () => { + const context: CursorContext = { position: 'value', fieldName: 'age' }; + const items = createCompletionItems({ + editorType: EditorType.Sort, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + expect(items).toHaveLength(2); + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('1'); + expect(labels).toContain('-1'); + }); + + test('-1 has description "descending"', () => { + const context: CursorContext = { position: 'value', fieldName: 'age' }; + const items = createCompletionItems({ + editorType: EditorType.Sort, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + const descItem = items.find((i) => getLabelText(i.label) === '-1'); + expect((descItem?.label as { description: string }).description).toBe('descending'); + }); + + test('does NOT show operators, BSON constructors, or JS globals', () => { + const context: CursorContext = { position: 'value', fieldName: 'age' }; + const items = createCompletionItems({ + editorType: EditorType.Sort, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('$gt'); + expect(labels).not.toContain('ObjectId'); + expect(labels).not.toContain('Date'); + }); + + test('1 is preselected', () => { + const context: CursorContext = { position: 'value', fieldName: 'age' }; + const items = createCompletionItems({ + editorType: EditorType.Sort, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: context, + }); + + const ascItem = items.find((i) => getLabelText(i.label) === '1'); + expect(ascItem?.preselect).toBe(true); + }); + }); + + // --------------------------------------------------------------- + // Category-based completion coverage by cursor position + // --------------------------------------------------------------- + describe('completion categories by cursor position', () => { + const mockMonaco = createMockMonaco(); + + /** + * Helper: extracts the description (category label) from a CompletionItem. + * For operator items this is getCategoryLabel(meta), e.g., "comparison", "array". + * For JS globals it is "JS global". + * For fields it is the type, e.g., "Number". + */ + function getDescription(label: string | monacoEditor.languages.CompletionItemLabel): string | undefined { + return typeof label === 'string' ? undefined : label.description; + } + + /** Returns Set of distinct category descriptions from a completion list. */ + function getCategories(items: monacoEditor.languages.CompletionItem[]): Set { + const categories = new Set(); + for (const item of items) { + const desc = getDescription(item.label); + if (desc) categories.add(desc); + } + return categories; + } + + describe('key position ({ })', () => { + const keyContext: CursorContext = { position: 'key', depth: 1 }; + + test('includes "logical" category operators', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + + const categories = getCategories(items); + expect(categories.has('logical')).toBe(true); + }); + + test('does NOT include purely field-level categories (comparison, array, element)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + + const categories = getCategories(items); + // These categories have NO operators in KEY_POSITION_OPERATORS + expect(categories.has('comparison')).toBe(false); + expect(categories.has('array')).toBe(false); + expect(categories.has('element')).toBe(false); + // Note: 'evaluation' IS present because $expr, $jsonSchema, $text are key-position + }); + + test('does NOT include "bson" or "JS global"', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + + const categories = getCategories(items); + expect(categories.has('bson')).toBe(false); + expect(categories.has('JS global')).toBe(false); + }); + }); + + describe('value position ({ field: })', () => { + const valueContext: CursorContext = { position: 'value', fieldName: 'x' }; + + test('includes field-level categories: comparison, array, evaluation, element', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const categories = getCategories(items); + expect(categories.has('comparison')).toBe(true); + expect(categories.has('array')).toBe(true); + expect(categories.has('evaluation')).toBe(true); + expect(categories.has('element')).toBe(true); + }); + + test('includes "bson" and "JS global"', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const categories = getCategories(items); + expect(categories.has('bson')).toBe(true); + expect(categories.has('JS global')).toBe(true); + }); + + test('does NOT include key-position-only operators', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('$and'); + expect(labels).not.toContain('$or'); + expect(labels).not.toContain('$nor'); + }); + }); + + describe('operator position ({ field: { } })', () => { + const operatorContext: CursorContext = { position: 'operator', fieldName: 'x' }; + + test('includes field-level categories: comparison, array, evaluation, element', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: operatorContext, + }); + + const categories = getCategories(items); + expect(categories.has('comparison')).toBe(true); + expect(categories.has('array')).toBe(true); + expect(categories.has('evaluation')).toBe(true); + expect(categories.has('element')).toBe(true); + }); + + test('does NOT include "bson", "JS global", or key-position operators', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: operatorContext, + }); + + const categories = getCategories(items); + expect(categories.has('bson')).toBe(false); + expect(categories.has('JS global')).toBe(false); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).not.toContain('$and'); + expect(labels).not.toContain('$or'); + }); + }); + + describe('unknown position (genuinely ambiguous โ€” shows everything)', () => { + const unknownContext: CursorContext = { position: 'unknown' }; + + test('includes all categories (full discovery fallback)', () => { + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: unknownContext, + }); + + const categories = getCategories(items); + // UNKNOWN shows everything as discovery + expect(categories.has('logical')).toBe(true); + expect(categories.has('comparison')).toBe(true); + expect(categories.has('array')).toBe(true); + expect(categories.has('bson')).toBe(true); + expect(categories.has('JS global')).toBe(true); + }); + + test('includes field names if store has data', () => { + setCompletionContext('test-session', { + fields: [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + ], + }); + + const items = createCompletionItems({ + editorType: EditorType.Filter, + sessionId: 'test-session', + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: unknownContext, + }); + + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('name'); + }); + }); + }); +}); diff --git a/src/webviews/documentdbQuery/documentdbQueryCompletionProvider.ts b/src/webviews/documentdbQuery/documentdbQueryCompletionProvider.ts new file mode 100644 index 000000000..3d3b74643 --- /dev/null +++ b/src/webviews/documentdbQuery/documentdbQueryCompletionProvider.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Barrel re-export for the completions module. + * + * The completion provider logic has been refactored into the `completions/` folder: + * - `completions/createCompletionItems.ts` โ€” main entry point, context branching + * - `completions/mapCompletionItems.ts` โ€” operator/field โ†’ CompletionItem mapping + * - `completions/typeSuggestions.ts` โ€” type-aware value suggestions + * - `completions/snippetUtils.ts` โ€” snippet text manipulation + * + * This file preserves the original import path for existing consumers. + */ + +// eslint-disable-next-line no-restricted-exports +export { + KEY_POSITION_OPERATORS, + createCompletionItems, + createTypeSuggestions, + escapeSnippetDollars, + getCategoryLabel, + getCompletionKindForMeta, + getMetaTagsForEditorType, + getOperatorSortPrefix, + mapFieldToCompletionItem, + mapOperatorToCompletionItem, + stripOuterBraces, + type CreateCompletionItemsParams, +} from './completions'; diff --git a/src/webviews/documentdbQuery/documentdbQueryHoverProvider.test.ts b/src/webviews/documentdbQuery/documentdbQueryHoverProvider.test.ts new file mode 100644 index 000000000..720d7b72a --- /dev/null +++ b/src/webviews/documentdbQuery/documentdbQueryHoverProvider.test.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type FieldCompletionData } from '../../utils/json/data-api/autocomplete/toFieldCompletionItems'; +import { getHoverContent, type FieldDataLookup } from './documentdbQueryHoverProvider'; + +/** Creates a mock field lookup function from an array of fields. */ +function createFieldLookup(fields: FieldCompletionData[]): FieldDataLookup { + return (word: string) => fields.find((f) => f.fieldName === word); +} + +describe('documentdbQueryHoverProvider', () => { + describe('getHoverContent', () => { + test('returns hover for known operator $gt', () => { + const hover = getHoverContent('$gt'); + expect(hover).not.toBeNull(); + expect(hover!.contents).toHaveLength(1); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('**$gt**'); + }); + + test('returns hover with description for $eq', () => { + const hover = getHoverContent('$eq'); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('**$eq**'); + expect(content.split('\n').length).toBeGreaterThan(1); + }); + + test('returns hover for BSON constructor ObjectId', () => { + const hover = getHoverContent('ObjectId'); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('**ObjectId**'); + }); + + test('returns null for unknown word', () => { + const hover = getHoverContent('foo'); + expect(hover).toBeNull(); + }); + + test('returns null for arbitrary text that is not an operator', () => { + const hover = getHoverContent('somethingRandom123'); + expect(hover).toBeNull(); + }); + + test('word without $ prefix matches operator when prefixed', () => { + const hover = getHoverContent('gt'); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('**$gt**'); + }); + + test('includes doc link when available', () => { + const hover = getHoverContent('$gt'); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('Documentation]'); + }); + + test('operator hover has isTrusted set for clickable links', () => { + const hover = getHoverContent('$gt'); + expect(hover).not.toBeNull(); + + const hoverContent = hover!.contents[0] as { isTrusted?: boolean }; + expect(hoverContent.isTrusted).toBe(true); + }); + + test('returns hover for UUID constructor', () => { + const hover = getHoverContent('UUID'); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('**UUID**'); + }); + }); + + describe('field hover', () => { + const fields: FieldCompletionData[] = [ + { + fieldName: 'age', + displayType: 'Number', + bsonType: 'int32', + isSparse: false, + insertText: 'age', + referenceText: '$age', + }, + { + fieldName: 'nickname', + displayType: 'String', + bsonType: 'string', + isSparse: true, + insertText: 'nickname', + referenceText: '$nickname', + }, + { + fieldName: 'rating', + displayType: 'Double', + bsonType: 'double', + bsonTypes: ['double', 'int32'], + displayTypes: ['Double', 'Int32'], + isSparse: true, + insertText: 'rating', + referenceText: '$rating', + }, + ]; + + test('returns hover for a known field name', () => { + const hover = getHoverContent('age', createFieldLookup(fields)); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('**age**'); + }); + + test('shows "Inferred Type" section with type list', () => { + const hover = getHoverContent('age', createFieldLookup(fields)); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('Inferred Type'); + expect(content).toContain('Number'); + }); + + test('shows multiple types for polymorphic fields', () => { + const hover = getHoverContent('rating', createFieldLookup(fields)); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('Inferred Type'); + expect(content).toContain('Double'); + expect(content).toContain('Int32'); + }); + + test('shows sparse indicator for sparse fields', () => { + const hover = getHoverContent('nickname', createFieldLookup(fields)); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('**nickname**'); + expect(content).toContain('sparse'); + expect(content).toContain('not present in all documents'); + }); + + test('does NOT show sparse indicator for non-sparse fields', () => { + const hover = getHoverContent('age', createFieldLookup(fields)); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).not.toContain('sparse'); + }); + + test('field hover does NOT set isTrusted (user data is not trusted)', () => { + const hover = getHoverContent('age', createFieldLookup(fields)); + expect(hover).not.toBeNull(); + + const hoverContent = hover!.contents[0] as { isTrusted?: boolean }; + expect(hoverContent.isTrusted).toBeUndefined(); + }); + + test('returns null for unknown field when no operator match', () => { + const hover = getHoverContent('unknownField', createFieldLookup(fields)); + expect(hover).toBeNull(); + }); + + test('operators take priority over field names', () => { + const fieldsWithOperatorName: FieldCompletionData[] = [ + { + fieldName: 'gt', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'gt', + referenceText: '$gt', + }, + ]; + + const hover = getHoverContent('gt', createFieldLookup(fieldsWithOperatorName)); + expect(hover).not.toBeNull(); + + const content = (hover!.contents[0] as { value: string }).value; + expect(content).toContain('**$gt**'); + }); + + test('returns null for field when no fieldLookup provided', () => { + const hover = getHoverContent('age'); + expect(hover).toBeNull(); + }); + }); +}); diff --git a/src/webviews/documentdbQuery/documentdbQueryHoverProvider.ts b/src/webviews/documentdbQuery/documentdbQueryHoverProvider.ts new file mode 100644 index 000000000..23207293a --- /dev/null +++ b/src/webviews/documentdbQuery/documentdbQueryHoverProvider.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Hover provider logic for the `documentdb-query` language. + * + * Provides inline documentation when hovering over operators, + * BSON constructors, and field names. Uses `documentdb-constants` for + * the operator registry and the completion store for field type info. + */ + +import { getAllCompletions } from '@vscode-documentdb/documentdb-constants'; +// eslint-disable-next-line import/no-internal-modules +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { type FieldCompletionData } from '../../utils/json/data-api/autocomplete/toFieldCompletionItems'; +import { escapeMarkdown } from '../utils/escapeMarkdown'; + +/** + * A callback that resolves a word to field data from the completion store. + */ +export type FieldDataLookup = (word: string) => FieldCompletionData | undefined; + +/** + * Returns hover content for a word under the cursor. + * + * Tries multiple candidates to handle cases where: + * - The cursor is on `gt` after `$` (need to try `$gt`) + * - The cursor is on `ObjectId` (try as-is) + * - The cursor is on a field name like `age` (check field data) + * + * Operators/BSON constructors take priority over field names. + * + * @param word - The word at the cursor position + * @param fieldLookup - optional callback to resolve field names to field data + * @returns A Monaco Hover or null if no match + */ +export function getHoverContent(word: string, fieldLookup?: FieldDataLookup): monacoEditor.languages.Hover | null { + // Try with '$' prefix first (for operators where cursor lands after $) + // Then try the word as-is (for BSON constructors like ObjectId) + const candidates = word.startsWith('$') ? [word] : [`$${word}`, word]; + + const allEntries = getAllCompletions(); + + for (const candidate of candidates) { + const match = allEntries.find((e) => e.value === candidate); + if (match) { + const lines: string[] = [`**${match.value}**`]; + + if (match.description || match.link) { + lines.push('---'); + lines.push('
'); + } + + if (match.description) { + lines.push(match.description); + } + if (match.link) { + lines.push(`[โ“˜ Documentation](${match.link})`); + } + + return { + contents: [{ value: lines.join('\n\n'), isTrusted: true, supportHtml: true }], + }; + } + } + + // If no operator match, try field name lookup + if (fieldLookup) { + const fieldData = fieldLookup(word); + if (fieldData) { + return buildFieldHover(fieldData); + } + } + + return null; +} + +/** + * Builds a hover tooltip for a field name. + */ +function buildFieldHover(field: FieldCompletionData): monacoEditor.languages.Hover { + const safeName = escapeMarkdown(field.fieldName); + let header = `**${safeName}**`; + + if (field.isSparse) { + header += '    sparse: not present in all documents'; + } + + const lines: string[] = [header]; + + // Inferred types section + const typeList = field.displayTypes && field.displayTypes.length > 0 ? field.displayTypes : [field.displayType]; + if (typeList && typeList.length > 0) { + lines.push('---'); + lines.push('
'); + lines.push(`Inferred Type: ${typeList.map((type) => `\`${escapeMarkdown(type)}\``).join(', ')}`); + } + + return { + contents: [{ value: lines.join('\n\n'), supportHtml: true }], + }; +} diff --git a/src/webviews/documentdbQuery/documentdbQueryValidator.test.ts b/src/webviews/documentdbQuery/documentdbQueryValidator.test.ts new file mode 100644 index 000000000..40f807e10 --- /dev/null +++ b/src/webviews/documentdbQuery/documentdbQueryValidator.test.ts @@ -0,0 +1,243 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { levenshteinDistance, validateExpression } from './documentdbQueryValidator'; + +describe('documentdbQueryValidator', () => { + describe('validateExpression', () => { + test('valid expression { age: { $gt: 25 } } produces no diagnostics', () => { + const diagnostics = validateExpression('{ age: { $gt: 25 } }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with multiple fields produces no diagnostics', () => { + const diagnostics = validateExpression('{ name: "Alice", age: 30 }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with BSON constructor produces no diagnostics', () => { + const diagnostics = validateExpression('{ _id: ObjectId("507f1f77bcf86cd799439011") }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with UUID constructor produces no diagnostics', () => { + const diagnostics = validateExpression('{ id: UUID("123e4567-e89b-12d3-a456-426614174000") }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with nested objects produces no diagnostics', () => { + const diagnostics = validateExpression('{ a: { b: { c: 1 } } }'); + expect(diagnostics).toHaveLength(0); + }); + + test('syntax error { age: { $gt: } produces error diagnostic', () => { + const diagnostics = validateExpression('{ age: { $gt: } }'); + expect(diagnostics.length).toBeGreaterThan(0); + + const errorDiag = diagnostics.find((d) => d.severity === 'error'); + expect(errorDiag).toBeDefined(); + }); + + test('syntax error with unclosed brace produces error diagnostic', () => { + const diagnostics = validateExpression('{ age: 25'); + expect(diagnostics.length).toBeGreaterThan(0); + expect(diagnostics[0].severity).toBe('error'); + }); + + test('typo UUUD("...") produces warning "Did you mean UUID?"', () => { + const diagnostics = validateExpression('{ id: UUUD("abc") }'); + + const warnings = diagnostics.filter((d) => d.severity === 'warning'); + expect(warnings.length).toBeGreaterThan(0); + expect(warnings[0].message).toContain('UUID'); + expect(warnings[0].message).toContain('Did you mean'); + }); + + test('typo Objected produces warning "Did you mean ObjectId?"', () => { + const diagnostics = validateExpression('{ id: ObjctId("abc") }'); + + const warnings = diagnostics.filter((d) => d.severity === 'warning'); + expect(warnings.length).toBeGreaterThan(0); + expect(warnings[0].message).toContain('ObjectId'); + }); + + test('unknown identifier foo used as function produces error', () => { + // "foo" is not close to any known identifier (Levenshtein > 2) + const diagnostics = validateExpression('{ id: foo("abc") }'); + const errors = diagnostics.filter((d) => d.severity === 'error'); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain("Unknown function 'foo'"); + }); + + test('unknown identifier as field name is not flagged', () => { + // Field names (non-function identifiers) should never produce diagnostics + const diagnostics = validateExpression('{ unknownField: 1 }'); + expect(diagnostics).toHaveLength(0); + }); + + test('unknown field name ___id is not flagged (field validation is out of scope)', () => { + // The validator does not validate field names against the schema. + // That requires integration with the completion store (known fields). + const diagnostics = validateExpression('{ ___id: 1 }'); + expect(diagnostics).toHaveLength(0); + }); + + test('empty string produces no diagnostics', () => { + const diagnostics = validateExpression(''); + expect(diagnostics).toHaveLength(0); + }); + + test('whitespace-only string produces no diagnostics', () => { + const diagnostics = validateExpression(' '); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with Math.min produces no diagnostics', () => { + const diagnostics = validateExpression('{ rating: Math.min(1.7, 2) }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with Date.now produces no diagnostics', () => { + const diagnostics = validateExpression('{ ts: Date.now() }'); + expect(diagnostics).toHaveLength(0); + }); + + test('typo Daate.now() produces warning "Did you mean Date?"', () => { + const diagnostics = validateExpression('{ _id: Daate.now() }'); + + const warnings = diagnostics.filter((d) => d.severity === 'warning'); + expect(warnings.length).toBeGreaterThan(0); + expect(warnings[0].message).toContain('Date'); + expect(warnings[0].message).toContain('Did you mean'); + }); + + test('typo Maht.min() produces warning "Did you mean Math?"', () => { + const diagnostics = validateExpression('{ val: Maht.min(1, 2) }'); + + const warnings = diagnostics.filter((d) => d.severity === 'warning'); + expect(warnings.length).toBeGreaterThan(0); + expect(warnings[0].message).toContain('Math'); + }); + + test('typo Nubmer.parseInt() produces warning "Did you mean Number?"', () => { + const diagnostics = validateExpression('{ x: Nubmer.parseInt("42") }'); + + const warnings = diagnostics.filter((d) => d.severity === 'warning'); + expect(warnings.length).toBeGreaterThan(0); + expect(warnings[0].message).toContain('Number'); + }); + + test('completely unknown member call UdddddduaD.now() produces error', () => { + const diagnostics = validateExpression('{ _id: UdddddduaD.now() }'); + const errors = diagnostics.filter((d) => d.severity === 'error'); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain("Unknown identifier 'UdddddduaD'"); + }); + + test('completely unknown direct call XyzAbc() produces error', () => { + const diagnostics = validateExpression('{ _id: XyzAbc("123") }'); + const errors = diagnostics.filter((d) => d.severity === 'error'); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain("Unknown function 'XyzAbc'"); + }); + + test('new Daddddte() produces error for unknown constructor', () => { + const diagnostics = validateExpression( + '{ date: { $gt: new Daddddte(Date.now() - 14 * 24 * 60 * 60 * 1000) } }', + ); + const errors = diagnostics.filter((d) => d.severity === 'error'); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain("Unknown constructor 'Daddddte'"); + }); + + test('new Dae() produces warning for near-miss constructor', () => { + const diagnostics = validateExpression('{ date: new Dae("2025-01-01") }'); + const warnings = diagnostics.filter((d) => d.severity === 'warning'); + expect(warnings).toHaveLength(1); + expect(warnings[0].message).toContain('Date'); + }); + + test('new Date() produces no diagnostics', () => { + const diagnostics = validateExpression('{ date: new Date() }'); + expect(diagnostics).toHaveLength(0); + }); + + test('new RegExp() produces no diagnostics', () => { + const diagnostics = validateExpression('{ name: { $regex: new RegExp("^test") } }'); + expect(diagnostics).toHaveLength(0); + }); + + test('Date.nodw() does NOT produce a warning (method validation is out of scope)', () => { + // We validate the object (Date) but not individual method names. + // Date is a known global, so no warning. The .nodw() method name + // is not validated โ€” that would require method-level knowledge. + const diagnostics = validateExpression('{ _id: Date.nodw() }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with ISODate constructor produces no diagnostics', () => { + const diagnostics = validateExpression('{ ts: ISODate("2024-01-01") }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with MinKey produces no diagnostics', () => { + const diagnostics = validateExpression('{ start: MinKey() }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with MaxKey produces no diagnostics', () => { + const diagnostics = validateExpression('{ end: MaxKey() }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with regex produces no diagnostics', () => { + const diagnostics = validateExpression('{ name: /^alice/i }'); + expect(diagnostics).toHaveLength(0); + }); + + test('valid expression with array produces no diagnostics', () => { + const diagnostics = validateExpression('{ tags: { $in: ["a", "b"] } }'); + expect(diagnostics).toHaveLength(0); + }); + + test('diagnostics have valid offsets within the input range', () => { + const code = '{ age: { $gt: } }'; + const diagnostics = validateExpression(code); + + for (const d of diagnostics) { + expect(d.startOffset).toBeGreaterThanOrEqual(0); + expect(d.endOffset).toBeLessThanOrEqual(code.length); + expect(d.startOffset).toBeLessThanOrEqual(d.endOffset); + } + }); + }); + + describe('levenshteinDistance', () => { + test('identical strings have distance 0', () => { + expect(levenshteinDistance('UUID', 'UUID')).toBe(0); + }); + + test('one character difference has distance 1', () => { + expect(levenshteinDistance('UUID', 'UUUD')).toBe(1); + }); + + test('two character difference has distance 2', () => { + expect(levenshteinDistance('ObjectId', 'ObjctId')).toBeLessThanOrEqual(2); + }); + + test('completely different strings have high distance', () => { + expect(levenshteinDistance('UUID', 'something')).toBeGreaterThan(2); + }); + + test('empty string vs non-empty has distance equal to length', () => { + expect(levenshteinDistance('', 'abc')).toBe(3); + expect(levenshteinDistance('abc', '')).toBe(3); + }); + + test('both empty strings have distance 0', () => { + expect(levenshteinDistance('', '')).toBe(0); + }); + }); +}); diff --git a/src/webviews/documentdbQuery/documentdbQueryValidator.ts b/src/webviews/documentdbQuery/documentdbQueryValidator.ts new file mode 100644 index 000000000..95690247c --- /dev/null +++ b/src/webviews/documentdbQuery/documentdbQueryValidator.ts @@ -0,0 +1,320 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Validator for `documentdb-query` editor content. + * + * Uses `acorn` to parse the expression and `acorn-walk` to traverse the AST. + * Produces diagnostics for: + * - Syntax errors (severity: error) + * - Near-miss BSON constructor typos (severity: warning) + * + * This module is pure and testable โ€” it does not depend on Monaco. + * The mapping from Diagnostic[] to Monaco markers happens in the editor mount handler. + */ + +import { getAllCompletions } from '@vscode-documentdb/documentdb-constants'; +import * as acorn from 'acorn'; +import * as walk from 'acorn-walk'; + +/** + * A diagnostic produced by the validator. + * Offsets are 0-based character positions in the original (unwrapped) input. + */ +export interface Diagnostic { + /** 0-based start character offset in the original input */ + startOffset: number; + /** 0-based end character offset in the original input */ + endOffset: number; + severity: 'error' | 'warning' | 'info'; + message: string; +} + +/** + * Known identifiers that should NOT be flagged as typos. + * These are globals available in shell-bson-parser's sandbox. + */ +const KNOWN_GLOBALS = new Set([ + // BSON constructors (populated dynamically below) + // JS globals available in the sandbox + 'Math', + 'Date', + 'ISODate', + 'RegExp', + 'Infinity', + 'NaN', + 'undefined', + 'true', + 'false', + 'null', + 'Map', + 'Symbol', + // Common JS builtins that might appear in expressions + 'Number', + 'String', + 'Boolean', + 'Array', + 'Object', + 'parseInt', + 'parseFloat', + 'isNaN', + 'isFinite', +]); + +// Add all BSON constructors from documentdb-constants +let bsonConstructorsLoaded = false; + +function ensureBsonConstructors(): void { + if (bsonConstructorsLoaded) return; + bsonConstructorsLoaded = true; + + const allEntries = getAllCompletions(); + for (const entry of allEntries) { + if (entry.meta === 'bson') { + KNOWN_GLOBALS.add(entry.value); + } + } +} + +/** + * Computes the Levenshtein edit distance between two strings. + * Used for near-miss detection of BSON constructor typos. + */ +export function levenshteinDistance(a: string, b: string): number { + const m = a.length; + const n = b.length; + + if (m === 0) return n; + if (n === 0) return m; + + const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0) as number[]); + + for (let i = 0; i <= m; i++) dp[i][0] = i; + for (let j = 0; j <= n; j++) dp[0][j] = j; + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (a[i - 1] === b[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]); + } + } + } + + return dp[m][n]; +} + +/** + * Finds the closest known identifier (BSON constructor or known global) to a given name. + * Returns the match and distance if within threshold, otherwise undefined. + * + * Searches both BSON constructor entries (from documentdb-constants) and + * KNOWN_GLOBALS (Date, Math, RegExp, etc.) for near-misses. + */ +function findNearMissKnownIdentifier(name: string): { match: string; distance: number } | undefined { + ensureBsonConstructors(); + + let bestMatch: string | undefined; + let bestDistance = Infinity; + + // Check against BSON constructors + const allEntries = getAllCompletions(); + for (const entry of allEntries) { + if (entry.meta === 'bson') { + const dist = levenshteinDistance(name.toLowerCase(), entry.value.toLowerCase()); + if (dist <= 2 && dist < bestDistance) { + bestDistance = dist; + bestMatch = entry.value; + } + } + } + + // Check against KNOWN_GLOBALS (Date, Math, RegExp, Number, etc.) + for (const known of KNOWN_GLOBALS) { + const dist = levenshteinDistance(name.toLowerCase(), known.toLowerCase()); + if (dist <= 2 && dist < bestDistance) { + bestDistance = dist; + bestMatch = known; + } + } + + if (bestMatch !== undefined && bestDistance <= 2) { + return { match: bestMatch, distance: bestDistance }; + } + + return undefined; +} + +/** + * Validates a documentdb-query expression and returns diagnostics. + * + * @param code - The expression text from the editor (e.g., `{ age: { $gt: 25 } }`) + * @returns Array of diagnostics (empty if the expression is valid) + */ +export function validateExpression(code: string): Diagnostic[] { + ensureBsonConstructors(); + + const trimmed = code.trim(); + if (trimmed.length === 0) { + return []; + } + + const diagnostics: Diagnostic[] = []; + + // Wrap in parentheses for acorn to parse as expression + // The offset adjustment accounts for the added '(' character + const wrapped = `(${code})`; + + let ast: acorn.Node; + try { + ast = acorn.parseExpressionAt(wrapped, 0, { + ecmaVersion: 'latest', + sourceType: 'module', + }); + } catch (error) { + if (error instanceof SyntaxError) { + const syntaxError = error as SyntaxError & { pos?: number; loc?: { line: number; column: number } }; + // Adjust offset for the wrapping parenthesis + const pos = syntaxError.pos !== undefined ? syntaxError.pos - 1 : 0; + const startOffset = Math.max(0, Math.min(pos, code.length)); + const endOffset = Math.min(startOffset + 1, code.length); + + const message = syntaxError.message.replace(/\(\d+:\d+\)/, '').trim(); + diagnostics.push({ + startOffset, + endOffset, + severity: 'error', + message, + }); + } + return diagnostics; + } + + // Walk the AST to check identifiers + try { + walk.simple(ast, { + // Planned no-op: bare identifiers are intentionally not flagged. + // In DocumentDB queries, most identifiers are field names (e.g. `{ age: 1 }`) + // which are valid and shouldn't produce diagnostics. Only identifiers in + // call positions (BSON constructor typos) are checked โ€” see CallExpression + // and MemberExpression handlers below. + Identifier(_node: acorn.Node & { name: string }) { + // no-op by design + }, + CallExpression( + node: acorn.Node & { + callee: acorn.Node & { + name?: string; + type: string; + object?: acorn.Node & { name?: string; type: string }; + }; + }, + ) { + // Case 1: Direct call โ€” e.g., ObjctId("abc") + if (node.callee.type === 'Identifier' && node.callee.name) { + const name = node.callee.name; + + if (KNOWN_GLOBALS.has(name)) { + return; + } + + const nearMiss = findNearMissKnownIdentifier(name); + const startOffset = node.callee.start - 1; + const endOffset = node.callee.end - 1; + if (nearMiss) { + diagnostics.push({ + startOffset, + endOffset, + severity: 'warning', + message: `Did you mean '${nearMiss.match}'?`, + }); + } else { + // No near-miss found โ€” unknown function call will fail at runtime + diagnostics.push({ + startOffset, + endOffset, + severity: 'error', + message: `Unknown function '${name}'. Expected a BSON constructor (e.g., ObjectId, ISODate) or a known global (e.g., Date, Math).`, + }); + } + } + + // Case 2: Member call โ€” e.g., Daate.now(), Maht.min() + // Check if the object is an unknown identifier that's a near-miss + if ( + node.callee.type === 'MemberExpression' && + node.callee.object && + node.callee.object.type === 'Identifier' && + node.callee.object.name + ) { + const objName = node.callee.object.name; + + if (KNOWN_GLOBALS.has(objName)) { + return; + } + + const nearMiss = findNearMissKnownIdentifier(objName); + const startOffset = node.callee.object.start - 1; + const endOffset = node.callee.object.end - 1; + if (nearMiss) { + diagnostics.push({ + startOffset, + endOffset, + severity: 'warning', + message: `Did you mean '${nearMiss.match}'?`, + }); + } else { + // No near-miss found โ€” unknown object will fail at runtime + diagnostics.push({ + startOffset, + endOffset, + severity: 'error', + message: `Unknown identifier '${objName}'. Expected a known global (e.g., Date, Math).`, + }); + } + } + }, + // NewExpression has the same callee shape as CallExpression. + // e.g., `new Daddddte()` โ€” the callee is an Identifier node. + NewExpression( + node: acorn.Node & { + callee: acorn.Node & { name?: string; type: string }; + }, + ) { + if (node.callee.type === 'Identifier' && node.callee.name) { + const name = node.callee.name; + + if (KNOWN_GLOBALS.has(name)) { + return; + } + + const nearMiss = findNearMissKnownIdentifier(name); + const startOffset = node.callee.start - 1; + const endOffset = node.callee.end - 1; + if (nearMiss) { + diagnostics.push({ + startOffset, + endOffset, + severity: 'warning', + message: `Did you mean '${nearMiss.match}'?`, + }); + } else { + diagnostics.push({ + startOffset, + endOffset, + severity: 'error', + message: `Unknown constructor '${name}'. Expected a BSON constructor (e.g., ObjectId, ISODate) or a known global (e.g., Date, RegExp).`, + }); + } + } + }, + }); + } catch { + // If walking fails, just return syntax diagnostics we already have + } + + return diagnostics; +} diff --git a/src/webviews/documentdbQuery/extractQuotedKey.test.ts b/src/webviews/documentdbQuery/extractQuotedKey.test.ts new file mode 100644 index 000000000..8c4f9e3ec --- /dev/null +++ b/src/webviews/documentdbQuery/extractQuotedKey.test.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { extractQuotedKey } from './extractQuotedKey'; + +describe('extractQuotedKey', () => { + test('extracts double-quoted key when cursor is inside', () => { + const line = '{ "address.street": "value" }'; + // 01234567890123456789 + const col = 5; // on 'a' of address + const result = extractQuotedKey(line, col); + expect(result).not.toBeNull(); + expect(result!.key).toBe('address.street'); + }); + + test('extracts single-quoted key when cursor is inside', () => { + const line = "{ 'address.street': 'value' }"; + const col = 5; + const result = extractQuotedKey(line, col); + expect(result).not.toBeNull(); + expect(result!.key).toBe('address.street'); + }); + + test('returns null when cursor is not inside quotes', () => { + const line = '{ name: "value" }'; + const col = 3; // on 'a' of name (unquoted) + const result = extractQuotedKey(line, col); + expect(result).toBeNull(); + }); + + test('returns null when cursor is on a structural character', () => { + const line = '{ "key": "value" }'; + const col = 0; // on '{' + const result = extractQuotedKey(line, col); + expect(result).toBeNull(); + }); + + test('returns correct start/end for range highlighting', () => { + const line = '{ "address.street": 1 }'; + // 0123456789012345678 + const col = 10; // somewhere inside the quoted string + const result = extractQuotedKey(line, col); + expect(result).not.toBeNull(); + expect(result!.start).toBe(2); // position of opening " + expect(result!.end).toBe(18); // position after closing " + expect(result!.key).toBe('address.street'); + }); + + test('handles escaped quotes inside key', () => { + const line = '{ "key\\"name": 1 }'; + const col = 5; + const result = extractQuotedKey(line, col); + expect(result).not.toBeNull(); + expect(result!.key).toBe('key\\"name'); + }); + + test('cursor on opening quote still works', () => { + const line = '{ "address.street": 1 }'; + const col = 2; // on the opening " + const result = extractQuotedKey(line, col); + expect(result).not.toBeNull(); + expect(result!.key).toBe('address.street'); + }); + + test('cursor on closing quote still works', () => { + const line = '{ "address.street": 1 }'; + const col = 17; // on the closing " + const result = extractQuotedKey(line, col); + expect(result).not.toBeNull(); + expect(result!.key).toBe('address.street'); + }); + + test('returns null for empty line', () => { + const result = extractQuotedKey('', 0); + expect(result).toBeNull(); + }); +}); diff --git a/src/webviews/documentdbQuery/extractQuotedKey.ts b/src/webviews/documentdbQuery/extractQuotedKey.ts new file mode 100644 index 000000000..b3e412f58 --- /dev/null +++ b/src/webviews/documentdbQuery/extractQuotedKey.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Extracts a quoted key string if the cursor is inside one. + * + * For `{ "address.street": 1 }`, when the cursor is anywhere between the + * opening and closing quotes, returns the unquoted key `"address.street"` + * along with the 0-based start/end positions of the full quoted string + * (including the quotes themselves, for hover range highlighting). + * + * Returns null if the cursor is not inside a quoted string. + * + * @param line - the full line content + * @param col0 - 0-based column position of the cursor + */ +export function extractQuotedKey(line: string, col0: number): { key: string; start: number; end: number } | null { + if (col0 < 0 || col0 >= line.length) return null; + + // If cursor is on a quote, it could be the closing quote. + // Try treating the current position as the closing quote first. + const chAtCursor = line[col0]; + if (chAtCursor === '"' || chAtCursor === "'") { + // Not escaped? + if (col0 === 0 || line[col0 - 1] !== '\\') { + // Try to find a matching opening quote before this one + const result = tryMatchAsClosingQuote(line, col0, chAtCursor); + if (result) return result; + } + } + + // Scan backward to find the opening quote + let openQuoteIdx = -1; + let quoteChar: string | undefined; + + for (let i = col0; i >= 0; i--) { + const ch = line[i]; + if (ch === '"' || ch === "'") { + if (i > 0 && line[i - 1] === '\\') continue; + openQuoteIdx = i; + quoteChar = ch; + break; + } + if (ch === '{' || ch === '}' || ch === ':' || ch === ',') { + return null; + } + } + + if (openQuoteIdx < 0 || !quoteChar) return null; + + // Scan forward to find the closing quote + let closeQuoteIdx = -1; + for (let i = openQuoteIdx + 1; i < line.length; i++) { + if (line[i] === '\\') { + i++; + continue; + } + if (line[i] === quoteChar) { + closeQuoteIdx = i; + break; + } + } + + if (closeQuoteIdx < 0) return null; + if (col0 < openQuoteIdx || col0 > closeQuoteIdx) return null; + + const key = line.substring(openQuoteIdx + 1, closeQuoteIdx); + return { key, start: openQuoteIdx, end: closeQuoteIdx + 1 }; +} + +function tryMatchAsClosingQuote( + line: string, + closeIdx: number, + quoteChar: string, +): { key: string; start: number; end: number } | null { + // Scan backward from before the closing quote to find the opening quote + for (let i = closeIdx - 1; i >= 0; i--) { + if (line[i] === '\\') continue; + if (line[i] === quoteChar) { + if (i > 0 && line[i - 1] === '\\') continue; + const key = line.substring(i + 1, closeIdx); + return { key, start: i, end: closeIdx + 1 }; + } + // Stop at structural chars + if (line[i] === '{' || line[i] === '}' || line[i] === ':' || line[i] === ',') { + return null; + } + } + return null; +} diff --git a/src/webviews/documentdbQuery/index.ts b/src/webviews/documentdbQuery/index.ts new file mode 100644 index 000000000..cb349f84a --- /dev/null +++ b/src/webviews/documentdbQuery/index.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * DocumentDB Query Language for Monaco Editor. + * + * This module provides the `documentdb-query` custom language that reuses + * the JavaScript Monarch tokenizer for syntax highlighting while providing + * custom completions from the `documentdb-constants` package. + * + * Usage: + * ```typescript + * import { registerDocumentDBQueryLanguage, LANGUAGE_ID } from './documentdbQuery'; + * + * // During Monaco initialization: + * await registerDocumentDBQueryLanguage(monaco); + * + * // In editor props: + * + * ``` + */ + +export { clearCompletionContext, getCompletionContext, setCompletionContext } from './completionStore'; +export { detectCursorContext, type CursorContext, type FieldTypeLookup } from './cursorContext'; +export { validateExpression, type Diagnostic } from './documentdbQueryValidator'; +export { EditorType, LANGUAGE_ID, URI_SCHEME, buildEditorUri, parseEditorUri } from './languageConfig'; +export { registerDocumentDBQueryLanguage } from './registerLanguage'; diff --git a/src/webviews/documentdbQuery/isCursorInsideString.test.ts b/src/webviews/documentdbQuery/isCursorInsideString.test.ts new file mode 100644 index 000000000..1c5468aff --- /dev/null +++ b/src/webviews/documentdbQuery/isCursorInsideString.test.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isCursorInsideString } from './isCursorInsideString'; + +describe('isCursorInsideString', () => { + test('returns false for empty text', () => { + expect(isCursorInsideString('', 0)).toBe(false); + }); + + test('returns false when cursor is outside any string', () => { + const text = '{ name: "Alice", age: 30 }'; + // cursor after the comma, outside the string + const cursorOffset = text.indexOf(',') + 1; + expect(isCursorInsideString(text, cursorOffset)).toBe(false); + }); + + test('returns true when cursor is inside a double-quoted string', () => { + const text = '{ name: "Ali'; + expect(isCursorInsideString(text, text.length)).toBe(true); + }); + + test('returns true when cursor is inside a single-quoted string', () => { + const text = "{ name: 'Ali"; + expect(isCursorInsideString(text, text.length)).toBe(true); + }); + + test('returns false when cursor is after a closed string', () => { + const text = '{ name: "Alice" }'; + // cursor at the space after closing quote + const cursorOffset = text.indexOf('"', 9) + 1; + expect(isCursorInsideString(text, cursorOffset)).toBe(false); + }); + + test('handles escaped quotes inside strings', () => { + const text = '{ name: "has\\"quote'; + // cursor is still inside the string (the \" is escaped) + expect(isCursorInsideString(text, text.length)).toBe(true); + }); + + test('returns false after escaped quote followed by closing quote', () => { + const text = '{ name: "has\\"quote" }'; + // cursor after the closing quote + const closingQuoteIdx = text.lastIndexOf('"'); + expect(isCursorInsideString(text, closingQuoteIdx + 1)).toBe(false); + }); + + // Edge cases from the plan + test('{ name: "Alice", | } โ€” cursor outside string after comma', () => { + const text = '{ name: "Alice", '; + expect(isCursorInsideString(text, text.length)).toBe(false); + }); + + test('{ name: "has:colon" } โ€” cursor inside string at colon', () => { + const text = '{ name: "has:'; + expect(isCursorInsideString(text, text.length)).toBe(true); + }); + + test('{ name: "has:colon", | } โ€” cursor outside string after comma', () => { + const text = '{ name: "has:colon", '; + expect(isCursorInsideString(text, text.length)).toBe(false); + }); + + test('{ tags: ["a", | ] } โ€” cursor outside string in array', () => { + const text = '{ tags: ["a", '; + expect(isCursorInsideString(text, text.length)).toBe(false); + }); + + test('{ msg: "has[bracket" } โ€” cursor inside string at bracket', () => { + const text = '{ msg: "has['; + expect(isCursorInsideString(text, text.length)).toBe(true); + }); + + test('{ $and: [ | ] } โ€” cursor outside string in array', () => { + const text = '{ $and: [ '; + expect(isCursorInsideString(text, text.length)).toBe(false); + }); + + test('handles mixed quote types correctly', () => { + const text = '{ name: "it\'s" }'; + // The single quote inside double quotes doesn't close anything + const cursorAfterClosingDouble = text.indexOf('"', 9) + 1; + expect(isCursorInsideString(text, cursorAfterClosingDouble)).toBe(false); + }); +}); diff --git a/src/webviews/documentdbQuery/isCursorInsideString.ts b/src/webviews/documentdbQuery/isCursorInsideString.ts new file mode 100644 index 000000000..7c605d1fe --- /dev/null +++ b/src/webviews/documentdbQuery/isCursorInsideString.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Determines whether the cursor is inside a string literal. + * + * Scans the text from the beginning up to the cursor offset, tracking whether + * we are inside a single-quoted or double-quoted string. Escaped quotes + * (preceded by `\`) do not toggle the state. + * + * This is a lightweight heuristic for suppressing auto-trigger completions + * when the trigger character (`:`, `,`, `[`) appears inside a string value + * rather than as structural syntax. + * + * @param text - the full text of the editor + * @param cursorOffset - the 0-based character offset of the cursor + * @returns true if the cursor is inside a string literal + */ +export function isCursorInsideString(text: string, cursorOffset: number): boolean { + let inString: "'" | '"' | false = false; + + for (let i = 0; i < cursorOffset && i < text.length; i++) { + const ch = text[i]; + + if (inString) { + // Check for escape character + if (ch === '\\') { + // Skip the next character (escaped) + i++; + continue; + } + // Check for closing quote + if (ch === inString) { + inString = false; + } + } else { + // Check for opening quote + if (ch === '"' || ch === "'") { + inString = ch; + } + } + } + + return inString !== false; +} diff --git a/src/webviews/documentdbQuery/languageConfig.test.ts b/src/webviews/documentdbQuery/languageConfig.test.ts new file mode 100644 index 000000000..97f276b52 --- /dev/null +++ b/src/webviews/documentdbQuery/languageConfig.test.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { buildEditorUri, EditorType, LANGUAGE_ID, parseEditorUri, URI_SCHEME } from './languageConfig'; + +describe('languageConfig', () => { + describe('constants', () => { + test('LANGUAGE_ID is "documentdb-query"', () => { + expect(LANGUAGE_ID).toBe('documentdb-query'); + }); + + test('URI_SCHEME is "documentdb"', () => { + expect(URI_SCHEME).toBe('documentdb'); + }); + }); + + describe('EditorType', () => { + test('has expected enum values', () => { + expect(EditorType.Filter).toBe('filter'); + expect(EditorType.Project).toBe('project'); + expect(EditorType.Sort).toBe('sort'); + expect(EditorType.Aggregation).toBe('aggregation'); + }); + }); + + describe('buildEditorUri', () => { + test('builds filter URI with session ID', () => { + const uri = buildEditorUri(EditorType.Filter, 'session-abc-123'); + expect(uri).toBe('documentdb://filter/session-abc-123'); + }); + + test('builds project URI with session ID', () => { + const uri = buildEditorUri(EditorType.Project, 'my-session'); + expect(uri).toBe('documentdb://project/my-session'); + }); + + test('builds sort URI with session ID', () => { + const uri = buildEditorUri(EditorType.Sort, 'sess-1'); + expect(uri).toBe('documentdb://sort/sess-1'); + }); + + test('builds aggregation URI with session ID', () => { + const uri = buildEditorUri(EditorType.Aggregation, 'agg-session'); + expect(uri).toBe('documentdb://aggregation/agg-session'); + }); + }); + + describe('parseEditorUri', () => { + test('parses valid filter URI', () => { + const result = parseEditorUri('documentdb://filter/session-abc-123'); + expect(result).toEqual({ + editorType: EditorType.Filter, + sessionId: 'session-abc-123', + }); + }); + + test('parses valid project URI', () => { + const result = parseEditorUri('documentdb://project/my-session'); + expect(result).toEqual({ + editorType: EditorType.Project, + sessionId: 'my-session', + }); + }); + + test('parses valid sort URI', () => { + const result = parseEditorUri('documentdb://sort/sess-1'); + expect(result).toEqual({ + editorType: EditorType.Sort, + sessionId: 'sess-1', + }); + }); + + test('parses valid aggregation URI', () => { + const result = parseEditorUri('documentdb://aggregation/agg-123'); + expect(result).toEqual({ + editorType: EditorType.Aggregation, + sessionId: 'agg-123', + }); + }); + + test('returns undefined for unrecognized scheme', () => { + const result = parseEditorUri('vscode://filter/session-1'); + expect(result).toBeUndefined(); + }); + + test('returns undefined for unknown editor type', () => { + const result = parseEditorUri('documentdb://unknown/session-1'); + expect(result).toBeUndefined(); + }); + + test('returns undefined for malformed URI (no session)', () => { + const result = parseEditorUri('documentdb://filter'); + expect(result).toBeUndefined(); + }); + + test('returns undefined for empty string', () => { + const result = parseEditorUri(''); + expect(result).toBeUndefined(); + }); + + test('roundtrips with buildEditorUri', () => { + for (const editorType of Object.values(EditorType)) { + const sessionId = `test-session-${editorType}`; + const uri = buildEditorUri(editorType, sessionId); + const parsed = parseEditorUri(uri); + expect(parsed).toEqual({ editorType, sessionId }); + } + }); + }); +}); diff --git a/src/webviews/documentdbQuery/languageConfig.ts b/src/webviews/documentdbQuery/languageConfig.ts new file mode 100644 index 000000000..5ad101a25 --- /dev/null +++ b/src/webviews/documentdbQuery/languageConfig.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Constants and configuration for the `documentdb-query` custom Monaco language. + * + * This language reuses the JavaScript Monarch tokenizer for syntax highlighting + * but does NOT attach the TypeScript/JavaScript language service worker. + * Completions are driven entirely by custom providers using `documentdb-constants`. + */ + +/** The language identifier registered with Monaco. */ +export const LANGUAGE_ID = 'documentdb-query'; + +/** URI scheme used for query editor models. */ +export const URI_SCHEME = 'documentdb'; + +/** + * Known editor types for URI-based routing. + * The completion provider inspects `model.uri` to determine which + * completions to offer. + */ +export enum EditorType { + Filter = 'filter', + Project = 'project', + Sort = 'sort', + Aggregation = 'aggregation', +} + +/** + * Builds a Monaco model URI for a given editor type and session. + * + * @param editorType - the type of query editor (filter, project, sort) + * @param sessionId - unique session identifier for this editor instance + * @returns a URI string like `documentdb://filter/session-abc-123` + */ +export function buildEditorUri(editorType: EditorType, sessionId: string): string { + return `${URI_SCHEME}://${editorType}/${sessionId}`; +} + +/** + * Parses a Monaco model URI to extract the editor type. + * + * @param uri - the URI string (e.g., `documentdb://filter/session-abc-123`) + * @returns the EditorType or undefined if the URI doesn't match + */ +export function parseEditorUri(uri: string): { editorType: EditorType; sessionId: string } | undefined { + // Handle both URI objects and strings + const uriString = typeof uri === 'string' ? uri : String(uri); + + const match = uriString.match(new RegExp(`^${URI_SCHEME}://([^/]+)/(.+)$`)); + if (!match) { + return undefined; + } + + const editorType = match[1] as EditorType; + const sessionId = match[2]; + + // Validate that it's a known editor type + if (!Object.values(EditorType).includes(editorType)) { + return undefined; + } + + return { editorType, sessionId }; +} diff --git a/src/webviews/documentdbQuery/registerLanguage.ts b/src/webviews/documentdbQuery/registerLanguage.ts new file mode 100644 index 000000000..231eb9461 --- /dev/null +++ b/src/webviews/documentdbQuery/registerLanguage.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Registers the `documentdb-query` custom language with Monaco Editor. + * + * This module: + * 1. Registers the language ID with Monaco + * 2. Imports the JavaScript Monarch tokenizer for syntax highlighting + * 3. Registers a custom CompletionItemProvider scoped to `documentdb-query` + * 4. Registers a HoverProvider for operator/constructor documentation + * + * The JS tokenizer provides correct highlighting for: + * - Unquoted identifiers: `{ name: 1 }` + * - Single-quoted strings: `{ 'name': 1 }` + * - Double-quoted strings: `{ "name": 1 }` + * - BSON constructors: `ObjectId("...")` + * - Regex literals: `/^alice/i` + * - Comments, template literals, function bodies (for future $function support) + * + * Because this is a custom language ID, the TypeScript worker is NOT loaded, + * keeping the bundle ~400-600 KB lighter and ensuring a clean completion slate. + */ + +// eslint-disable-next-line import/no-internal-modules +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { getCompletionContext } from './completionStore'; +import { detectCursorContext } from './cursorContext'; +import { createCompletionItems } from './documentdbQueryCompletionProvider'; +import { getHoverContent } from './documentdbQueryHoverProvider'; +import { extractQuotedKey } from './extractQuotedKey'; +import { isCursorInsideString } from './isCursorInsideString'; +import { LANGUAGE_ID, parseEditorUri } from './languageConfig'; + +/** Coalesces concurrent registrations into a single promise. */ +let registrationPromise: Promise | undefined; + +/** Callback used to open external URLs via the extension host. */ +let openUrlHandler: ((url: string) => void) | undefined; + +/** + * Registers the `documentdb-query` language with Monaco. + * + * Safe to call multiple times โ€” concurrent calls coalesce into one registration. + * The `openUrl` callback is updated on every call so the tRPC client reference + * stays current even after hot-reloads. + * + * @param monaco - the Monaco editor API instance + * @param openUrl - callback to open a URL via the extension host (avoids webview sandbox restrictions) + */ +export function registerDocumentDBQueryLanguage( + monaco: typeof monacoEditor, + openUrl?: (url: string) => void, +): Promise { + openUrlHandler = openUrl ?? openUrlHandler; + if (!registrationPromise) { + registrationPromise = doRegisterLanguage(monaco); + } + return registrationPromise; +} + +async function doRegisterLanguage(monaco: typeof monacoEditor): Promise { + // Step 1: Register the language ID + monaco.languages.register({ id: LANGUAGE_ID }); + + // Step 2: Import the JS Monarch tokenizer + // This path has been stable since Monaco 0.20 and exports { conf, language } + // eslint-disable-next-line import/no-internal-modules + const jsLanguage = (await import('monaco-editor/esm/vs/basic-languages/javascript/javascript.js')) as { + language: monacoEditor.languages.IMonarchLanguage; + conf: monacoEditor.languages.LanguageConfiguration; + }; + + // Step 3: Apply the JS tokenizer and language configuration to our custom language + monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, jsLanguage.language); + monaco.languages.setLanguageConfiguration(LANGUAGE_ID, jsLanguage.conf); + + // Register a link opener so that documentation links in hover tooltips + // are opened via the extension host (which calls vscode.env.openExternal). + // VS Code webview sandboxing blocks window.open/popups, so we route through + // the tRPC openUrl mutation when available, or fall back to window.open. + monaco.editor.registerLinkOpener({ + open(resource) { + const url = resource.toString(true); + if (openUrlHandler) { + openUrlHandler(url); + } else { + window.open(url, '_blank'); + } + return true; + }, + }); + + // Step 4: Register the completion provider + monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, { + triggerCharacters: ['$', '"', "'", '{', '.', ':', ',', '['], + provideCompletionItems: ( + model: monacoEditor.editor.ITextModel, + position: monacoEditor.Position, + ): monacoEditor.languages.CompletionList => { + // Parse the model URI to determine editor context + const uriString = model.uri.toString(); + const parsed = parseEditorUri(uriString); + + // Get the word at the current position for range calculation + const wordInfo = model.getWordUntilPosition(position); + let range: monacoEditor.IRange = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: wordInfo.startColumn, + endColumn: wordInfo.endColumn, + }; + + // Check if cursor is preceded by '$' (for operator completions) + // Monaco's getWordUntilPosition() does not treat '$' as part of a word boundary. + // When the user types '$g', wordInfo.startColumn points to 'g', not '$'. + // Without this fix, selecting '$gt' would insert '$$gt' (double dollar). + const lineContent = model.getLineContent(position.lineNumber); + // -2 because columns are 1-based: e.g. startColumn=1 โ†’ index -1 โ†’ undefined (safe). + // JS returns undefined for out-of-bounds array access, so (undefined === '$') โ†’ false. + const charBefore = lineContent[wordInfo.startColumn - 2]; + + if (charBefore === '$') { + range = { ...range, startColumn: range.startColumn - 1 }; + } + + // Detect cursor context for context-sensitive completions + const text = model.getValue(); + const cursorOffset = model.getOffsetAt(position); + + // Suppress completions when the cursor is inside a string literal. + // This prevents trigger characters like ':', ',', '[' from firing + // inside strings like { name: "has:colon" } or { msg: "has[bracket" }. + if (isCursorInsideString(text, cursorOffset)) { + return { suggestions: [] }; + } + + const sessionId = parsed?.sessionId; + + // Build field lookup from completion store to enrich context with BSON types + const fieldLookup = (fieldName: string): string | undefined => { + if (!sessionId) return undefined; + const ctx = getCompletionContext(sessionId); + return ctx?.fields.find((f) => f.fieldName === fieldName)?.bsonType; + }; + + const cursorContext = detectCursorContext(text, cursorOffset, fieldLookup); + + // Detect whether the editor content has braces. When the user clears + // the editor (deleting initial `{ }`), completions need to include + // wrapping braces so inserted snippets produce valid query syntax. + const needsWrapping = !text.includes('{'); + + // Build completion items based on context + const items = createCompletionItems({ + editorType: parsed?.editorType, + sessionId, + range, + isDollarPrefix: charBefore === '$', + monaco, + cursorContext, + needsWrapping, + }); + + return { suggestions: items }; + }, + }); + + // Step 5: Register the hover provider + monaco.languages.registerHoverProvider(LANGUAGE_ID, { + provideHover: ( + model: monacoEditor.editor.ITextModel, + position: monacoEditor.Position, + ): monacoEditor.languages.Hover | null => { + // Build field lookup from completion store for field hover info + const uriString = model.uri.toString(); + const parsedUri = parseEditorUri(uriString); + const hoverFieldLookup = parsedUri?.sessionId + ? (word: string) => { + const ctx = getCompletionContext(parsedUri.sessionId); + return ctx?.fields.find((f) => f.fieldName === word); + } + : undefined; + + // Try to extract a quoted string key (e.g., "address.street") + // Monaco's getWordAtPosition treats quotes and dots as word boundaries, + // so for { "address.street": 1 } hovering on "address" would only match + // "address", not the full field name "address.street". + const lineContent = model.getLineContent(position.lineNumber); + const col0 = position.column - 1; // 0-based + + const quotedResult = extractQuotedKey(lineContent, col0); + if (quotedResult) { + const hover = getHoverContent(quotedResult.key, hoverFieldLookup); + if (hover) { + return { + ...hover, + range: { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: quotedResult.start + 1, // 1-based + endColumn: quotedResult.end + 1, // 1-based + }, + }; + } + } + + // Fall back to standard word-based hover + const wordAtPosition = model.getWordAtPosition(position); + if (!wordAtPosition) { + return null; + } + + const hover = getHoverContent(wordAtPosition.word, hoverFieldLookup); + if (!hover) { + return null; + } + + // Set the range for the hover highlight + return { + ...hover, + range: { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: wordAtPosition.startColumn, + endColumn: wordAtPosition.endColumn, + }, + }; + }, + }); +} + +/** + * Resets the registration state. For testing only. + * @internal + */ +export function _resetRegistration(): void { + registrationPromise = undefined; +} diff --git a/src/webviews/documentdbQuery/tdd/README.md b/src/webviews/documentdbQuery/tdd/README.md new file mode 100644 index 000000000..27cbe2a63 --- /dev/null +++ b/src/webviews/documentdbQuery/tdd/README.md @@ -0,0 +1,33 @@ +# TDD Behavior Tests + +This folder contains **behavior-driven TDD tests** for the `documentdb-query` +autocompletion system. These tests define the expected behavior contract โ€” they +were written BEFORE the implementation and drive the design. + +## Important: TDD Contract + +> **โš ๏ธ LLM/Agent Instruction:** +> These tests represent intentional behavior specifications. If a test in this +> folder fails after a code change, **do NOT automatically fix the test**. +> Instead, alert the user that a TDD behavior contract has been violated. +> The failure likely means the implementation change broke an expected behavior. +> The user must decide whether to update the test (changing the contract) or +> fix the implementation. + +## Test Files + +| File | What it tests | +|------|---------------| +| `completionBehavior.test.ts` | Which completion categories appear at each cursor position, sorting order, and snippet wrapping | + +## Specification + +See [readme.completionBehavior.md](readme.completionBehavior.md) for the full +behavior specification with ASCII art examples. + +## Why a separate folder? + +These tests verify cross-cutting **behavior** (the completion matrix), not a +single class or module. They sit at the `documentdbQuery/tdd/` level because +they test the combined output of `cursorContext`, `createCompletionItems`, +`mapCompletionItems`, and `completionKnowledge` working together. diff --git a/src/webviews/documentdbQuery/tdd/completionBehavior.test.ts b/src/webviews/documentdbQuery/tdd/completionBehavior.test.ts new file mode 100644 index 000000000..b2187b114 --- /dev/null +++ b/src/webviews/documentdbQuery/tdd/completionBehavior.test.ts @@ -0,0 +1,534 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * TDD Behavior Tests โ€” Completion Categories by Cursor Position + * + * These tests define the expected behavior CONTRACT for the autocompletion + * system. They were written BEFORE the implementation (TDD redโ†’green cycle) + * and verify WHAT completions appear at each cursor position, not HOW they + * are produced internally. + * + * โš ๏ธ LLM/Agent Instruction: + * If these tests fail after a code change, do NOT automatically fix the tests. + * Alert the user that a TDD behavior contract has been violated. The failure + * means the implementation broke expected behavior. The user must decide + * whether to update the spec or fix the implementation. + * + * Tests are category-based (not specific-operator-based) to be resilient to + * changes in documentdb-constants. Categories come from the `description` + * field in CompletionItemLabel (e.g., "comparison", "logical", "bson"). + * + * See: readme.completionBehavior.md for the full specification. + */ + +// eslint-disable-next-line import/no-internal-modules +import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { clearAllCompletionContexts, setCompletionContext } from '../completionStore'; +import { type CursorContext } from '../cursorContext'; +import { createCompletionItems } from '../documentdbQueryCompletionProvider'; +import { EditorType } from '../languageConfig'; + +// ---------- Test infrastructure ---------- + +const mockCompletionItemKind: typeof monacoEditor.languages.CompletionItemKind = { + Method: 0, + Function: 1, + Constructor: 2, + Field: 3, + Variable: 4, + Class: 5, + Struct: 6, + Interface: 7, + Module: 8, + Property: 9, + Event: 10, + Operator: 11, + Unit: 12, + Value: 13, + Constant: 14, + Enum: 15, + EnumMember: 16, + Keyword: 17, + Text: 18, + Color: 19, + File: 20, + Reference: 21, + Customcolor: 22, + Folder: 23, + TypeParameter: 24, + User: 25, + Issue: 26, + Snippet: 27, +}; + +const mockInsertTextRule = { + InsertAsSnippet: 4, + KeepWhitespace: 1, + None: 0, +} as typeof monacoEditor.languages.CompletionItemInsertTextRule; + +function createMockMonaco(): typeof monacoEditor { + return { + languages: { + CompletionItemKind: mockCompletionItemKind, + CompletionItemInsertTextRule: mockInsertTextRule, + }, + } as unknown as typeof monacoEditor; +} + +const testRange: monacoEditor.IRange = { + startLineNumber: 1, + endLineNumber: 1, + startColumn: 1, + endColumn: 1, +}; + +// ---------- Helpers ---------- + +/** Extracts the description (category) from a CompletionItem label. */ +function getDescription(label: string | monacoEditor.languages.CompletionItemLabel): string | undefined { + return typeof label === 'string' ? undefined : label.description; +} + +/** Returns the set of distinct categories present in a completion list. */ +function getCategories(items: monacoEditor.languages.CompletionItem[]): Set { + const categories = new Set(); + for (const item of items) { + const desc = getDescription(item.label); + if (desc) categories.add(desc); + } + return categories; +} + +/** Returns the label text from a CompletionItem. */ +function getLabelText(label: string | monacoEditor.languages.CompletionItemLabel): string { + return typeof label === 'string' ? label : label.label; +} + +/** + * Returns all distinct sortText prefixes (the part before the underscore) + * found in a completion list. + */ +function getSortPrefixes(items: monacoEditor.languages.CompletionItem[]): Set { + const prefixes = new Set(); + for (const item of items) { + if (item.sortText) { + const underscoreIdx = item.sortText.indexOf('_'); + if (underscoreIdx > 0) { + prefixes.add(item.sortText.substring(0, underscoreIdx + 1)); + } + } + } + return prefixes; +} + +// ---------- Field data for tests ---------- + +const testFields = [ + { + fieldName: 'name', + displayType: 'String', + bsonType: 'string', + isSparse: false, + insertText: 'name', + referenceText: '$name', + }, + { + fieldName: 'age', + displayType: 'Number', + bsonType: 'int32', + isSparse: false, + insertText: 'age', + referenceText: '$age', + }, +]; + +// ---------- Key-position operator categories ---------- +// These are the categories that should appear at KEY / EMPTY positions. +// We test by category name, not specific operators, for resilience. +// (Used in assertions, not as a lookup โ€” individual tests check specific categories.) + +// Field-level categories that should NOT appear at key/empty positions. +// These categories have NO operators in KEY_POSITION_OPERATORS. +// Note: 'logical' and 'evaluation' are shared โ€” they have both key-position +// operators ($and/$or for logical, $expr/$text for evaluation) and field-level +// operators ($not for logical, $regex/$mod for evaluation). +const FIELD_LEVEL_ONLY_CATEGORIES = ['comparison', 'array', 'element', 'bitwise', 'geospatial']; + +// ===================================================================== +// Tests +// ===================================================================== + +describe('TDD: Completion Behavior', () => { + const mockMonaco = createMockMonaco(); + + beforeAll(() => { + console.warn( + '\nโš ๏ธ TDD CONTRACT TESTS โ€” If any test below fails, do NOT auto-fix the test.\n' + + ' Alert the user that a TDD behavior contract has been violated.\n' + + ' The user must decide whether to update the spec or fix the implementation.\n', + ); + }); + + afterEach(() => { + clearAllCompletionContexts(); + }); + + // ----------------------------------------------------------------- + // EMPTY position โ€” no braces in editor + // ----------------------------------------------------------------- + describe('EMPTY position (no braces, needsWrapping=true)', () => { + /** + * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + * โ”‚ | โ”‚ โ† cursor, no braces + * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + * + * Expected: fields + key operators, all wrapped with { } + * NOT expected: comparison, array, evaluation, element, bson, JS global + */ + + function getEmptyCompletions(sessionId?: string): monacoEditor.languages.CompletionItem[] { + return createCompletionItems({ + editorType: EditorType.Filter, + sessionId, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'unknown' }, + needsWrapping: true, + }); + } + + test('includes field names when store has data', () => { + setCompletionContext('s1', { fields: testFields }); + const items = getEmptyCompletions('s1'); + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('name'); + expect(labels).toContain('age'); + }); + + test('field insertText is wrapped with { }', () => { + setCompletionContext('s1', { fields: testFields }); + const items = getEmptyCompletions('s1'); + const nameItem = items.find((i) => getLabelText(i.label) === 'name'); + expect(nameItem?.insertText).toMatch(/^\{.*\}$/); + }); + + test('includes key-position operator categories (logical)', () => { + const items = getEmptyCompletions(); + const categories = getCategories(items); + expect(categories.has('logical')).toBe(true); + }); + + test('does NOT include field-level categories', () => { + const items = getEmptyCompletions(); + const categories = getCategories(items); + for (const cat of FIELD_LEVEL_ONLY_CATEGORIES) { + expect(categories.has(cat)).toBe(false); + } + }); + + test('does NOT include "bson"', () => { + const items = getEmptyCompletions(); + const categories = getCategories(items); + expect(categories.has('bson')).toBe(false); + }); + + test('does NOT include "JS global"', () => { + const items = getEmptyCompletions(); + const categories = getCategories(items); + expect(categories.has('JS global')).toBe(false); + }); + + test('fields sort before operators (0_ < 1_)', () => { + setCompletionContext('s1', { fields: testFields }); + const items = getEmptyCompletions('s1'); + const fieldItem = items.find((i) => getLabelText(i.label) === 'name'); + const operatorItems = items.filter((i) => getDescription(i.label) === 'logical'); + expect(fieldItem?.sortText).toMatch(/^0_/); + expect(operatorItems.length).toBeGreaterThan(0); + expect(operatorItems[0]?.sortText).toMatch(/^1_/); + }); + }); + + // ----------------------------------------------------------------- + // KEY position โ€” inside { } + // ----------------------------------------------------------------- + describe('KEY position ({ | })', () => { + /** + * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + * โ”‚ { | } โ”‚ โ† cursor inside braces + * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + * + * Expected: fields + key operators + * NOT expected: comparison, array, evaluation, element, bson, JS global + */ + + const keyContext: CursorContext = { position: 'key', depth: 1 }; + + function getKeyCompletions(sessionId?: string): monacoEditor.languages.CompletionItem[] { + return createCompletionItems({ + editorType: EditorType.Filter, + sessionId, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: keyContext, + }); + } + + test('includes key-position operator categories', () => { + const categories = getCategories(getKeyCompletions()); + expect(categories.has('logical')).toBe(true); + }); + + test('does NOT include field-level categories', () => { + const categories = getCategories(getKeyCompletions()); + for (const cat of FIELD_LEVEL_ONLY_CATEGORIES) { + expect(categories.has(cat)).toBe(false); + } + }); + + test('does NOT include "bson" or "JS global"', () => { + const categories = getCategories(getKeyCompletions()); + expect(categories.has('bson')).toBe(false); + expect(categories.has('JS global')).toBe(false); + }); + + test('field sortText starts with 0_, operator sortText starts with 1_', () => { + setCompletionContext('s1', { fields: testFields }); + const items = getKeyCompletions('s1'); + + // Every field item should have sortText starting with 0_ + const fieldItems = items.filter((i) => getLabelText(i.label) === 'name' || getLabelText(i.label) === 'age'); + for (const item of fieldItems) { + expect(item.sortText).toMatch(/^0_/); + } + + // Every operator item should have sortText starting with 1_ + const operatorItems = items.filter((i) => { + const desc = getDescription(i.label); + return desc === 'logical' || desc === 'evaluation' || desc === 'misc'; + }); + for (const item of operatorItems) { + expect(item.sortText).toMatch(/^1_/); + } + }); + }); + + // ----------------------------------------------------------------- + // VALUE position โ€” { field: | } + // ----------------------------------------------------------------- + describe('VALUE position ({ field: | })', () => { + /** + * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + * โ”‚ { age: | } โ”‚ โ† cursor at value position + * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + * + * Expected: type suggestions + field-level operators + bson + JS globals + * NOT expected: key-position operators ($and, $or at root) + */ + + const valueContext: CursorContext = { position: 'value', fieldName: 'age' }; + + function getValueCompletions(): monacoEditor.languages.CompletionItem[] { + return createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + } + + test('includes field-level categories', () => { + const categories = getCategories(getValueCompletions()); + for (const cat of FIELD_LEVEL_ONLY_CATEGORIES) { + expect(categories.has(cat)).toBe(true); + } + }); + + test('includes "bson" and "JS global"', () => { + const categories = getCategories(getValueCompletions()); + expect(categories.has('bson')).toBe(true); + expect(categories.has('JS global')).toBe(true); + }); + + test('does NOT include key-position operators by label', () => { + const labels = getValueCompletions().map((i) => getLabelText(i.label)); + // Check just a couple representative key operators + expect(labels).not.toContain('$and'); + expect(labels).not.toContain('$or'); + }); + + test('sort order: operators (0_) before bson (3_) before JS globals (4_)', () => { + const prefixes = getSortPrefixes(getValueCompletions()); + expect(prefixes.has('0_')).toBe(true); + expect(prefixes.has('3_')).toBe(true); + expect(prefixes.has('4_')).toBe(true); + }); + + test('project editor shows only 1/0 at value position', () => { + const items = createCompletionItems({ + editorType: EditorType.Project, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + expect(items).toHaveLength(2); + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('1'); + expect(labels).toContain('0'); + }); + + test('sort editor shows only 1/-1 at value position', () => { + const items = createCompletionItems({ + editorType: EditorType.Sort, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: valueContext, + }); + expect(items).toHaveLength(2); + const labels = items.map((i) => getLabelText(i.label)); + expect(labels).toContain('1'); + expect(labels).toContain('-1'); + }); + }); + + // ----------------------------------------------------------------- + // OPERATOR position โ€” { field: { | } } + // ----------------------------------------------------------------- + describe('OPERATOR position ({ field: { | } })', () => { + /** + * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + * โ”‚ { age: { | } } โ”‚ โ† cursor inside operator object + * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + * + * Expected: field-level operators (braces stripped) + * NOT expected: bson, JS global, key-position operators + */ + + const operatorContext: CursorContext = { position: 'operator', fieldName: 'age' }; + + function getOperatorCompletions(): monacoEditor.languages.CompletionItem[] { + return createCompletionItems({ + editorType: EditorType.Filter, + sessionId: undefined, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: operatorContext, + }); + } + + test('includes field-level categories', () => { + const categories = getCategories(getOperatorCompletions()); + for (const cat of FIELD_LEVEL_ONLY_CATEGORIES) { + expect(categories.has(cat)).toBe(true); + } + }); + + test('does NOT include "bson" or "JS global"', () => { + const categories = getCategories(getOperatorCompletions()); + expect(categories.has('bson')).toBe(false); + expect(categories.has('JS global')).toBe(false); + }); + + test('does NOT include key-position operators', () => { + const labels = getOperatorCompletions().map((i) => getLabelText(i.label)); + expect(labels).not.toContain('$and'); + expect(labels).not.toContain('$or'); + }); + }); + + // ----------------------------------------------------------------- + // ARRAY-ELEMENT position โ€” { $and: [|] } + // ----------------------------------------------------------------- + describe('ARRAY-ELEMENT position ({ $and: [|] })', () => { + /** + * Same behavior as KEY position + */ + + const arrayContext: CursorContext = { position: 'array-element', parentOperator: '$and' }; + + function getArrayElementCompletions(sessionId?: string): monacoEditor.languages.CompletionItem[] { + return createCompletionItems({ + editorType: EditorType.Filter, + sessionId, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: arrayContext, + }); + } + + test('behaves like KEY: includes logical, excludes field-level categories', () => { + const categories = getCategories(getArrayElementCompletions()); + expect(categories.has('logical')).toBe(true); + for (const cat of FIELD_LEVEL_ONLY_CATEGORIES) { + expect(categories.has(cat)).toBe(false); + } + }); + + test('includes fields when store has data', () => { + setCompletionContext('s1', { fields: testFields }); + const labels = getArrayElementCompletions('s1').map((i) => getLabelText(i.label)); + expect(labels).toContain('name'); + }); + }); + + // ----------------------------------------------------------------- + // UNKNOWN position โ€” genuinely ambiguous (show everything) + // ----------------------------------------------------------------- + describe('UNKNOWN position (ambiguous, needsWrapping=false)', () => { + /** + * Genuinely unknown cursor position. Show everything as discovery. + * This is the fallback when the parser can't determine position AND + * the editor is not empty (has some content with braces but ambiguous). + */ + + function getUnknownCompletions(sessionId?: string): monacoEditor.languages.CompletionItem[] { + return createCompletionItems({ + editorType: EditorType.Filter, + sessionId, + range: testRange, + isDollarPrefix: false, + monaco: mockMonaco, + cursorContext: { position: 'unknown' }, + needsWrapping: false, + }); + } + + test('includes key-position categories', () => { + const categories = getCategories(getUnknownCompletions()); + expect(categories.has('logical')).toBe(true); + }); + + test('includes field-level categories (full discovery)', () => { + const categories = getCategories(getUnknownCompletions()); + for (const cat of FIELD_LEVEL_ONLY_CATEGORIES) { + expect(categories.has(cat)).toBe(true); + } + }); + + test('includes "bson" and "JS global"', () => { + const categories = getCategories(getUnknownCompletions()); + expect(categories.has('bson')).toBe(true); + expect(categories.has('JS global')).toBe(true); + }); + + test('includes fields when store has data', () => { + setCompletionContext('s1', { fields: testFields }); + const labels = getUnknownCompletions('s1').map((i) => getLabelText(i.label)); + expect(labels).toContain('name'); + }); + }); +}); diff --git a/src/webviews/documentdbQuery/tdd/readme.completionBehavior.md b/src/webviews/documentdbQuery/tdd/readme.completionBehavior.md new file mode 100644 index 000000000..575f4f9f8 --- /dev/null +++ b/src/webviews/documentdbQuery/tdd/readme.completionBehavior.md @@ -0,0 +1,135 @@ +# Completion Behavior Specification + +> **โš ๏ธ LLM/Agent Instruction:** +> This specification defines the expected completion behavior contract. +> If tests based on this spec fail after a code change, **do NOT automatically +> fix the tests**. Alert the user that a TDD behavior contract has been violated. +> The user must decide whether the behavior change is intentional. + +## Cursor Positions & Expected Completions + +The completion system shows different items depending on the cursor's semantic +position within a query expression. This spec defines what categories appear, +their sort order, and how snippets are wrapped at each position. + +### Categories + +Completions are grouped by category (shown as the `description` field in the +completion item label). The categories come from the operator's `meta` tag: + +| Category | Source | Example operators | +|----------|--------|-------------------| +| `logical` | `query:logical` | `$and`, `$or`, `$nor` | +| `comparison` | `query:comparison` | `$eq`, `$gt`, `$in` | +| `array` | `query:array` | `$all`, `$elemMatch`, `$size` | +| `evaluation` | `query:evaluation` | `$regex`, `$mod` | +| `element` | `query:element` | `$exists`, `$type` | +| `bson` | `bson` | `ObjectId`, `UUID`, `ISODate` | +| `JS global` | (hardcoded) | `Date`, `Math`, `RegExp` | +| (field type) | field data | `String`, `Number`, etc. | + +### Position: EMPTY (no braces in editor) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ | โ”‚ โ† cursor, editor has no braces +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Shows:** Fields + key-position operators only (same items as KEY) +**Wrapping:** All insertions wrapped with `{ ... }` +**Sort:** `0_` fields, `1_` key operators + +``` +Expected completions: + name String โ† field, inserts: { name: $1 } + age Number โ† field, inserts: { age: $1 } + $and logical โ† key operator, inserts: { $and: [...] } + $or logical โ† key operator + $nor logical โ† key operator + +NOT shown: + $gt comparison โ† field-level, invalid at root + $all array โ† field-level, invalid at root + ObjectId bson โ† not valid at root key position + Date JS global โ† not valid at root key position +``` + +### Position: KEY (`{ | }`) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ { | } โ”‚ โ† cursor inside braces +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Shows:** Fields + key-position operators +**Wrapping:** None (already inside braces) +**Sort:** `0_` fields, `1_` key operators +**Snippets:** Outer `{ }` stripped from operator snippets + +``` +NOT shown: comparison, array, evaluation, element, bson, JS global +``` + +### Position: VALUE (`{ field: | }`) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ { age: | } โ”‚ โ† cursor at value position +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Shows:** Type suggestions + field-level operators + BSON constructors + JS globals +**Sort:** `00_` type suggestions, `0_`โ€“`2_` operators, `3_` BSON, `4_` JS globals +**Special:** Project editor โ†’ `1`/`0` only. Sort editor โ†’ `1`/`-1` only. + +``` +Shown categories: comparison, array, evaluation, element, logical ($not), bson, JS global +NOT shown: key-position operators ($and, $or, $nor at root) +``` + +### Position: OPERATOR (`{ field: { | } }`) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ { age: { | } } โ”‚ โ† cursor inside operator object +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Shows:** Field-level operators only (braces stripped) +**Sort:** `0_` type-relevant, `1a_` comparison, `1b_` other universal, `2_` non-matching +**Snippets:** Outer `{ }` stripped + +``` +Shown categories: comparison, array, evaluation, element, logical ($not) +NOT shown: bson, JS global, key-position operators +``` + +### Position: ARRAY-ELEMENT (`{ $and: [|] }`) + +**Shows:** Same as KEY position +**Sort:** Same as KEY position + +### Position: UNKNOWN (genuinely ambiguous) + +**Shows:** ALL completions (fields + all operators + BSON + JS globals) +**Purpose:** Discovery fallback for positions the parser can't classify + +``` +Shown: everything โ€” logical, comparison, array, evaluation, element, bson, JS global +``` + +## Sort Order Contract + +Each position has a defined sort prefix hierarchy. Items with lower prefixes +appear higher in the completion list. + +| Position | Sort hierarchy | +|----------|---------------| +| EMPTY | `0_` fields โ†’ `1_` key operators | +| KEY | `0_` fields โ†’ `1_` key operators | +| VALUE | `00_` type suggestions โ†’ `0_`โ€“`2_` operators โ†’ `3_` BSON โ†’ `4_` JS globals | +| OPERATOR | `0_` type-relevant โ†’ `1a_` comparison โ†’ `1b_` universal โ†’ `2_` non-matching | +| ARRAY-ELEMENT | same as KEY | +| UNKNOWN | no enforced sort (Monaco default) | diff --git a/src/webviews/index.scss b/src/webviews/index.scss index f5fb4a7cd..561d1c072 100644 --- a/src/webviews/index.scss +++ b/src/webviews/index.scss @@ -87,3 +87,13 @@ $media-breakpoint-query-control-area: 1024px; @include input-focus-animation; @include input-hover; } + +/** + * Monaco suggest-details panel: ensure links show a pointer cursor. + * The hover widget applies this automatically, but the completion + * documentation panel does not โ€” VS Code's webview CSS reset overrides it. + */ +.monaco-editor .suggest-details a, +.monaco-editor .suggest-details-container a { + cursor: pointer; +} diff --git a/src/webviews/utils/escapeMarkdown.test.ts b/src/webviews/utils/escapeMarkdown.test.ts new file mode 100644 index 000000000..4dbdfc1d5 --- /dev/null +++ b/src/webviews/utils/escapeMarkdown.test.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { escapeMarkdown } from './escapeMarkdown'; + +describe('escapeMarkdown', () => { + test('returns plain text unchanged', () => { + expect(escapeMarkdown('age')).toBe('age'); + }); + + test('escapes markdown bold characters', () => { + expect(escapeMarkdown('**bold**')).toBe('\\*\\*bold\\*\\*'); + }); + + test('escapes markdown link syntax', () => { + expect(escapeMarkdown('[click](https://evil.com)')).toBe('\\[click\\]\\(https://evil\\.com\\)'); + }); + + test('escapes angle brackets (HTML tags)', () => { + expect(escapeMarkdown('')).toBe('\\alert\\(1\\)\\'); + }); + + test('escapes backticks', () => { + expect(escapeMarkdown('`code`')).toBe('\\`code\\`'); + }); + + test('escapes ampersands', () => { + expect(escapeMarkdown('a&b')).toBe('a\\&b'); + }); + + test('handles dotted field names', () => { + expect(escapeMarkdown('address.street')).toBe('address\\.street'); + }); + + test('passes through numbers and underscores', () => { + // underscore IS a markdown metacharacter, so it gets escaped + expect(escapeMarkdown('field_1')).toBe('field\\_1'); + }); +}); diff --git a/src/webviews/utils/escapeMarkdown.ts b/src/webviews/utils/escapeMarkdown.ts new file mode 100644 index 000000000..245a63a29 --- /dev/null +++ b/src/webviews/utils/escapeMarkdown.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Escapes markdown metacharacters so user data renders as literal text. + * + * Covers characters that Markdown/HTML would otherwise interpret: + * `\`, `*`, `_`, `{`, `}`, `[`, `]`, `(`, `)`, `#`, `+`, `-`, `.`, `!`, + * `|`, `<`, `>`, `` ` ``, `~`, `&` + */ +export function escapeMarkdown(text: string): string { + return text.replace(/[\\*_{}[\]()#+\-.!|<>`~&]/g, '\\$&'); +} diff --git a/syntaxes/documentdb-scratchpad.tmGrammar.json b/syntaxes/documentdb-scratchpad.tmGrammar.json new file mode 100644 index 000000000..a4312464f --- /dev/null +++ b/syntaxes/documentdb-scratchpad.tmGrammar.json @@ -0,0 +1,9 @@ +{ + "scopeName": "source.documentdb-scratchpad", + "injectionSelector": "", + "patterns": [ + { + "include": "source.js" + } + ] +} diff --git a/test/mongoGetCommand.test.ts b/test/mongoGetCommand.test.ts deleted file mode 100644 index 7b4ce3f4d..000000000 --- a/test/mongoGetCommand.test.ts +++ /dev/null @@ -1,1174 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { nonNullProp, parseError } from '@microsoft/vscode-azext-utils'; -import assert from 'assert'; -import { ObjectId } from 'bson'; -import { Position } from 'vscode'; -import { findCommandAtPosition, getAllCommandsFromText, type MongoCommand } from '../extension.bundle'; - -function expectSingleCommand(text: string): MongoCommand { - const commands = getAllCommandsFromText(text); - if (commands.length > 1) { - assert.ok(false, 'Too many commands found'); - } - - return commands[0]; -} - -function testParse( - text: string, - expectedCommand: - | { collection: string | undefined; name: string | undefined; args: any[] | undefined; firstErrorText?: string } - | undefined, -): void { - function testCore(coreText: string): void { - const command = expectSingleCommand(coreText); - if (expectedCommand) { - assert.ok(command, 'Expected a command, but found none'); - - assert.equal( - command.collection || '', - expectedCommand.collection || '', - 'Parsed collection name is not correct', - ); - assert.equal(command.name || '', expectedCommand.name || '', 'Parsed command name is not correct'); - - const actualArgs = (command.arguments || []).map((arg) => JSON.parse(arg)); - assert.deepEqual(actualArgs, expectedCommand.args || [], 'Parsed arguments are not correct'); - } else { - assert.ok(!command, 'Found a command, but expecting to find none'); - return; - } - - if (expectedCommand && expectedCommand.firstErrorText) { - assert.equal((command.errors || []).length > 0, true, 'Expected at least one error'); - assert.equal( - nonNullProp(command, 'errors')[0].message, - expectedCommand.firstErrorText, - 'First error text was incorrect', - ); - } else { - assert.equal((command.errors || []).length, 0, 'Expected no errors'); - } - } - - testCore(text); - - // Test again with LF changed to CR/LF - const crlfText = text.replace(/\n/g, '\r\n'); - testCore(crlfText); - - // Test again with LF changed to multiple CR/LF - const crlf2Text = text.replace(/\n/g, '\r\n\r\n'); - testCore(crlf2Text); - - // Test again with LF changed to CR - const lfText = text.replace(/\n/g, '\r'); - testCore(lfText); - - // Test again with LF changed to tab - const tabText = text.replace(/\n/g, '\t'); - testCore(tabText); - - // Test again with LF changed to space - const spaceText = text.replace(/\n/g, ' '); - testCore(spaceText); -} - -function wrapInQuotes(word: string, numQuotes: number): string { - //0 to do nothing, 1 for single quotes, 2 for double quotes - let result: string; - if (numQuotes === 1) { - result = `'${word}'`; - } else if (numQuotes === 2) { - result = `"${word}"`; - } else { - result = word; - } - return result; -} - -suite('scrapbook parsing Tests', () => { - test('find', () => { - const text = 'db.find()'; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.equal(command.text, text); - }); - - test('find with semicolon', () => { - const text = 'db.find();'; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.equal(command.text, text); - }); - - test('first of two commands, Mac/Linux', () => { - const line1 = 'db.find()'; - const line2 = "db.insertOne({'a': 'b'})"; - const text = `${line1}\n${line2}`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.equal(command.text, line1); - }); - - test('second of two commands, Mac/Linux', () => { - const line1 = 'db.find()'; - for (let q = 0; q <= 2; q++) { - const line2 = `db.insertOne({${wrapInQuotes('a', q)}:'b'})`; - const text = `${line1}\n${line2}`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(2, 0)); - assert.equal(command.text, line2); - } - }); - - test('second of two commands, Mac/Linux, semicolon', () => { - const line1 = 'db.find();'; - for (let q = 0; q <= 2; q++) { - const line2 = `db.insertOne({${wrapInQuotes('a', q)}:'b'})`; - const text = `${line1}\n${line2}`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(2, 0)); - assert.equal(command.text, line2); - } - }); - - test('first of two commands, Windows', () => { - const line1 = 'db.find()'; - const line2 = "db.insertOne({'a': 'b'})"; - const text = `${line1}\r\n${line2}`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.equal(command.text, line1); - }); - - test('second of two commands, Windows', () => { - const line1 = 'db.find()'; - const line2 = "db.insertOne({'a':'b'})"; - const text = `${line1}\r\n${line2}`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(2, 0)); - assert.equal(command.text, line2); - }); - - test('second of two commands, lots of blank lines, Windows', () => { - const line1 = 'db.find()'; - const line2 = "db.insertOne({'a':'b'})"; - const text = `\r\n\r\n\r\n\r\n\r\n\r\n${line1}\r\n\r\n\r\n\r\n\r\n\r\n${line2}\r\n\r\n\r\n\r\n\r\n\r\n`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(5, 0)); - assert.equal(command.text, line2); - }); - - test('first of two commands, Windows, on blank line before second command', () => { - const line1 = 'db.find()'; - for (let q = 0; q <= 2; q++) { - const line2 = `db.insertOne({${wrapInQuotes('a', q)}:1})`; - const text = `${line1}\r\n\r\n\r\n${line2}`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(2, 0)); - assert.equal(command.text, line1); - } - }); - - test('drop', () => { - testParse(`db.test.drop()`, { collection: 'test', name: 'drop', args: [] }); - }); - - test('find, with empty object argument', () => { - testParse(`db.test.find({})`, { collection: 'test', name: 'find', args: [{}] }); - }); - - test('end-of-line comment', () => { - testParse(`db.test.drop() // Ignore error "ns not found", it means "test" does not exist yet`, { - collection: 'test', - name: 'drop', - args: [], - }); - }); - - test('multi-line insert from #214', () => { - for (let q = 0; q <= 2; q++) { - testParse( - `db.heroes.insert({\n${wrapInQuotes('id', q)}: 2,\r\n${wrapInQuotes('name', q)}: "Batman",\r\n\r\n${wrapInQuotes('saying', q)}: "I'm Batman"\r})`, - { - collection: 'heroes', - name: 'insert', - args: [ - { - id: 2, - name: 'Batman', - saying: "I'm Batman", - }, - ], - }, - ); - } - }); - - test('find/project from #214', () => { - testParse(`db.heroes.find({ "id": 2 }, { "saying": 1 })`, { - collection: 'heroes', - name: 'find', - args: [ - { - id: 2, - }, - { - saying: 1, - }, - ], - }); - }); - - test('extraneous input', () => { - testParse( - `db.heros.find(); - hello there`, - { - collection: 'heros', - name: 'find', - args: [], - firstErrorText: - "mismatched input 'hello' expecting {, SingleLineComment, MultiLineComment, ';', 'db'}", - }, - ); - }); - - test('empty', () => { - testParse('// hello there', undefined); - }); - - test('no command found, errors (will be tacked on to a blank command)', () => { - testParse('hello there', { - collection: undefined, - name: undefined, - args: undefined, - firstErrorText: - "mismatched input 'hello' expecting {, SingleLineComment, MultiLineComment, ';', 'db'}", - }); - }); - - test('expect error: missing comma in arguments', () => { - testParse(`db.heroes.find({ "id": 2 } { "saying": 1 })`, { - collection: 'heroes', - name: 'find', - args: [ - { - id: 2, - }, - ], - firstErrorText: "mismatched input '{' expecting {',', ')'}", - }); - - testParse(`db.c.find({"a":[1,2,3]"b":1});`, { - collection: 'c', - name: 'find', - args: [{ a: [1, 2, 3] }], - firstErrorText: "mismatched input '\"b\"' expecting {',', '}'}", - }); - }); - - //https://github.com/Microsoft/vscode-cosmosdb/issues/467 - test('single quoted property names', () => { - testParse(`db.heroes.find({ 'id': 2 }, { 'saying': 1 })`, { - collection: 'heroes', - name: 'find', - args: [ - { - id: 2, - }, - { - saying: 1, - }, - ], - }); - }); - test('expect error: missing function name', () => { - // From https://github.com/Microsoft/vscode-cosmosdb/issues/659 - testParse(`db.c1.`, { - collection: 'c1', - name: '', - args: [], - firstErrorText: "mismatched input '' expecting IDENTIFIER", - }); - - testParse(`db.c1.;`, { - collection: 'c1', - name: '', - args: [], - firstErrorText: "mismatched input ';' expecting IDENTIFIER", - }); - - testParse(`db.c1.(1, "a");`, { - collection: 'c1', - name: '', - args: [1, 'a'], - firstErrorText: "missing IDENTIFIER at '('", - }); - - testParse(`..(1, "a");`, { - collection: undefined, - name: undefined, - args: undefined, - firstErrorText: "mismatched input '.' expecting {, SingleLineComment, MultiLineComment, ';', 'db'}", - }); - - // Just make sure doesn't throw - expectSingleCommand(`db..(1, "a");`); - expectSingleCommand(`..c1(1, "a");`); - }); - - test('multi-line insert from #214', () => { - testParse(`db.heroes.insert({\n"id": 2,\r\n"name": "Batman",\r\n\r\n"saying": "I'm Batman"\r})`, { - collection: 'heroes', - name: 'insert', - args: [ - { - id: 2, - name: 'Batman', - saying: "I'm Batman", - }, - ], - }); - }); - - test('Array followed by } on separate line, from #73', () => { - testParse( - `db.createUser({ - "user": "buddhi", - "pwd": "123", - "roles": ["readWrite", "dbAdmin"] - } - )`, - { - collection: undefined, - name: 'createUser', - args: [ - { - user: 'buddhi', - pwd: '123', - roles: ['readWrite', 'dbAdmin'], - }, - ], - }, - ); - }); - - test('Multiple line arrays, from #489', () => { - testParse( - ` - db.c2.insertMany([ - {"name": "Stephen", "age": 38, "male": true}, - {"name": "Stephen", "age": 38, "male": true} - ]) - `, - { - collection: 'c2', - name: 'insertMany', - args: [ - [ - { - name: 'Stephen', - age: 38, - male: true, - }, - { - name: 'Stephen', - age: 38, - male: true, - }, - ], - ], - }, - ); - }); - - test('test function call that has 2 arguments', () => { - const arg0 = `{"Age": 31}`; - const arg1 = `{"Name": true}`; - const text = `db.find(${arg0}\r\n,\r\n\r\n${arg1})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(JSON.parse(command.arguments![0]), JSON.parse(arg0)); - assert.deepEqual(JSON.parse(command.arguments![1]), JSON.parse(arg1)); - }); - test('test function call with nested parameters - documents in an array', () => { - const arg0 = `[{"name": "a"}, {"name": "b"}, {"name": "c"}]`; - const arg1 = `{"ordered": true}`; - const text = `db.test1.insertMany(${arg0},\r\n\r\n\r\n${arg1})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(JSON.parse(command.arguments![0]), JSON.parse(arg0)); - assert.deepEqual(JSON.parse(command.arguments![1]), JSON.parse(arg1)); - }); - test('test function call that has a nested parameter', () => { - const arg0 = `{"name": {"First" : "a", "Last":"b"} }`; - const text = `db.test1.insertMany(${arg0})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(JSON.parse(command.arguments![0]), JSON.parse(arg0)); - }); - test('test function call with erroneous syntax: missing comma', () => { - const arg0 = `{"name": {"First" : "a", "Last":"b"} }`; - const arg1 = `{"ordered": true}`; - const text = `db.test1.insertMany(${arg0} ${arg1})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const err = nonNullProp(command, 'errors')[0]; - assert.deepEqual(err.message, "mismatched input '{' expecting {',', ')'}"); - assert.deepEqual(err.range.start.line, 0); - assert.deepEqual(err.range.start.character, 61); - }); - test('test function call with erroneous syntax: missing comma, parameters separated with newline', () => { - const arg0 = `{"name": {"First" : "a", "Last":"b"} }`; - const arg1 = `{"ordered": \ntrue}`; - const text = `db.test1.insertMany(${arg0} \n ${arg1})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const err = nonNullProp(command, 'errors')[0]; - assert.deepEqual(err.message, "mismatched input '{' expecting {',', ')'}"); - assert.deepEqual(err.range.start.line, 1); - assert.deepEqual(err.range.start.character, 2); - }); - test('test function call with erroneous syntax: missing double quote', () => { - const text = `db.test1.insertMany({name": {"First" : "a", "Last":"b"} })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const err = nonNullProp(command, 'errors')[0]; - assert.deepEqual(err.message, "missing ':' at '\": {\"'"); - assert.deepEqual(err.range.start.line, 0); - assert.deepEqual(err.range.start.character, 25); - }); - test('test function call with erroneous syntax: missing opening brace', () => { - const text = `db.test1.insertMany("name": {"First" : "a", "Last":"b"} })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const err = nonNullProp(command, 'errors')[0]; - assert.deepEqual(err.message, "mismatched input ':' expecting {',', ')'}"); - assert.deepEqual(err.range.start.line, 0); - assert.deepEqual(err.range.start.character, 26); - }); - - test('Chained command: to use pretty()', () => { - testParse('db.timesheets.find().pretty();', { - collection: 'timesheets', - name: 'pretty', - args: [], - }); - }); - - test('ISODate with standard date string', () => { - testParse( - 'db.c1.insertOne({ "_id": ObjectId("5aecf1a63d8af732f07e4275"), "name": "Stephen", "date": ISODate("2018-05-01T00:00:00Z") });', - { - collection: 'c1', - name: 'insertOne', - args: [ - { - _id: { - $oid: '5aecf1a63d8af732f07e4275', - }, - date: { - $date: '2018-05-01T00:00:00.000Z', - }, - name: 'Stephen', - }, - ], - }, - ); - }); - - test('ISODate without Z in date string', () => { - testParse( - 'db.c1.insertOne({ "_id": ObjectId("5aecf1a63d8af732f07e4275"), "name": "Stephen", "date": ISODate("2018-05-01T00:00:00") });', - { - collection: 'c1', - name: 'insertOne', - args: [ - { - _id: { - $oid: '5aecf1a63d8af732f07e4275', - }, - date: { - $date: '2018-05-01T00:00:00.000Z', - }, - name: 'Stephen', - }, - ], - }, - ); - }); - - test('Invalid ISODate', () => { - testParse( - 'db.c1.insertOne({ "_id": ObjectId("5aecf1a63d8af732f07e4275"), "name": "Stephen", "date": ISODate("2018-05-01T00:00:00z") });', - { - collection: 'c1', - name: 'insertOne', - args: [], - firstErrorText: 'Invalid time value', - }, - ); - }); - - test('Date', () => { - const date: Date = new Date('2018-05-01T00:00:00'); - testParse( - `db.c1.insertOne({ "_id": ObjectId("5aecf1a63d8af732f07e4275"), "name": "Stephen", "date": Date("${date.toUTCString()}") });`, - { - collection: 'c1', - name: 'insertOne', - args: [ - { - _id: { - $oid: '5aecf1a63d8af732f07e4275', - }, - date: { - $date: date.toString(), - }, - name: 'Stephen', - }, - ], - }, - ); - }); - - test('Keys with periods', () => { - testParse( - `db.timesheets.update( { - "year":"2018", - "month":"06" - },{ - "$set":{ - "workers.0.days.0.something":"yupy!" - } - }); - `, - { - collection: 'timesheets', - name: 'update', - args: [ - { - year: 2018, - month: '06', - }, - { - $set: { - 'workers.0.days.0.something': 'yupy!', - }, - }, - ], - }, - ); - }); - - test('nested objects', () => { - testParse( - `db.users.update({},{ - "$pull":{ - "proposals":{ - "$elemMatch":{"_id":"4qsBHLDCb755c3vPH"} - } - } - })`, - { - collection: 'users', - name: 'update', - args: [ - {}, - { - $pull: { - proposals: { - $elemMatch: { - _id: '4qsBHLDCb755c3vPH', - }, - }, - }, - }, - ], - }, - ); - }); - test('test function call with and without quotes', () => { - for (let q = 0; q <= 2; q++) { - const text = `db.test1.insertMany({${wrapInQuotes('name', q)}: 'First' })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [{ name: 'First' }]); - } - }); - test('test function call with numbers', () => { - const text = `db.test1.insertMany({'name': 1010101})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [{ name: 1010101 }]); - }); - test('test function call boolean', () => { - const text = `db.test1.insertMany({'name': false})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [{ name: false }]); - }); - test('test function call token inside quotes', () => { - const text = `db.test1.insertMany({'name': 'false'})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [{ name: 'false' }]); - }); - test('test function call with an empty string property value', () => { - const text = `db.test1.insertMany({'name': ''})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [{ name: '' }]); - }); - test('test function call with array and multiple arguments', () => { - const text = `db.test1.find({'roles': ['readWrite', 'dbAdmin']}, {'resources': ['secondary', 'primary']})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [ - { roles: ['readWrite', 'dbAdmin'] }, - { resources: ['secondary', 'primary'] }, - ]); - }); - test('test function call with nested objects', () => { - const text = `db.test1.find({'roles': [{'optional': 'yes'}]}, {'resources': ['secondary', 'primary']})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [ - { roles: [{ optional: 'yes' }] }, - { resources: ['secondary', 'primary'] }, - ]); - }); - - test('test incomplete function call - replicate user typing - no function call yet', () => { - const text = `db.test1.`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, []); - assert.deepEqual(command.collection, 'test1'); - }); - - test('test incomplete function call - replicate user typing - missing propertyValue', () => { - const text = `db.test1.find({"name": {"First" : } })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [{ name: { First: {} } }]); - }); - - test('test incomplete function call - replicate user typing - missing colon & propertyValue', () => { - const text = `db.test1.find({"name": {"First" } })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [{ name: { First: {} } }]); - }); - - test('test incomplete function call - replicate user typing - empty array as argument', () => { - const text = `db.heroes.aggregate([\n])`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [[]]); - }); - - test('test quotes inside a string - 1', () => { - const text = `db.test1.find("That's all")`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, ["That's all"]); - }); - - test('test quotes inside a string - 2', () => { - const text = `db.test1.find('That"s all')`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, ['That"s all']); - }); - - // Note: when escaping a character, escape it twice. - test('test quotes inside a string - 3', () => { - const text = `db.test1.find("Hello \\"there\\"")`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, ['Hello \\"there\\"']); - }); - - test('test quotes inside a string - 4', () => { - const text = `db.test1.find('Hello \\'there\\'')`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, ["Hello \\'there\\'"]); - }); - - test('test nested property names (has dots in the name)', () => { - const text = `db.test1.find({"name.FirstName": 1})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.argumentObjects, [{ 'name.FirstName': 1 }]); - }); - - test('test managed namespace collection names (has dots in the name)', () => { - const text = `db.test1.beep.find({})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'test1.beep'); - }); - - test('test aggregate query', () => { - for (let q = 0; q <= 2; q++) { - const text = `db.orders.aggregate([ - { ${wrapInQuotes('$match', q)}: { ${wrapInQuotes('status', q)} : "A" } }, - { ${wrapInQuotes('$group', q)}: { ${wrapInQuotes('_id', q)}: "$cust_id", ${wrapInQuotes('total', q)}: { ${wrapInQuotes('$sum', q)}: "$amount" } } }, - { ${wrapInQuotes('$sort', q)}: { ${wrapInQuotes('total', q)}: -1 } } - ], - { - ${wrapInQuotes('cursor', q)}: { ${wrapInQuotes('batchSize', q)}: 0 } - })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'orders'); - assert.deepEqual(command.argumentObjects, [ - [ - { $match: { status: 'A' } }, - { $group: { _id: '$cust_id', total: { $sum: '$amount' } } }, - { $sort: { total: -1 } }, - ], - { - cursor: { batchSize: 0 }, - }, - ]); - } - }); - - test('test ObjectID - no parameter', () => { - const text = `db.c1.insert({"name": ObjectId()})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'c1'); - assert.ok((nonNullProp(command, 'argumentObjects')[0]).name instanceof ObjectId); - }); - - test('test ObjectID - hex', () => { - const idParam = 'abcdef123456789012345678'; - const text = `db.c1.insert({"name": ObjectId("${idParam}")})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'c1'); - const id = new ObjectId(idParam); - assert.deepEqual(command.argumentObjects, [{ name: id }]); - }); - - test('test faulty ObjectID - hex - extra characters', () => { - const idParam = 'abcdef12345678901234567890'; - const text = `db.c1.insert({"name": ObjectId("${idParam}")})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'c1'); - assert.deepEqual(command.argumentObjects, [{ name: {} }]); - assert.notStrictEqual(nonNullProp(command, 'errors')[0]?.message, undefined); - }); - - test('test faulty ObjectID - hex - fewer characters', () => { - const idParam = 'abcdef123456789012345'; - for (let i = 1; i < 3; i++) { - const text = `db.c1.insert({"name": ObjectId(${wrapInQuotes(idParam, i)})})`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'c1'); - assert.deepEqual(command.argumentObjects, [{ name: {} }]); - assert.notStrictEqual(nonNullProp(command, 'errors')[0]?.message, undefined); - } - }); - //Examples inspired from https://docs.mongodb.com/manual/reference/operator/query/regex/ - test('test regular expressions - only pattern, no flags', () => { - const text = `db.test1.beep.find({ sku: /789$/ })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '789$'); - }); - - test('test regular expressions - pattern and flags', () => { - const text = `db.test1.beep.find({ sku: /789$/i } )`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - console.log('generatedRegExp', generatedRegExp); - assert.deepEqual(generatedRegExp.options, 'i'); - assert.deepEqual(generatedRegExp.pattern, '789$'); - }); - - test('test regular expressions - Intellisense - flag contains unsupported option', () => { - const text = `db.test1.beep.find({ sku: /789$/g })`; - try { - const commands: MongoCommand[] = getAllCommandsFromText(text); - findCommandAtPosition(commands, new Position(0, 0)); - } catch (error) { - const err = parseError(error); - assert.deepEqual('Unexpected node encountered', err.message); - } - }); - - test('test regular expressions - Intellisense - flag contains invalid option', () => { - const text = `db.test1.beep.find({ sku: /789$/q })`; - try { - const commands: MongoCommand[] = getAllCommandsFromText(text); - findCommandAtPosition(commands, new Position(0, 0)); - } catch (error) { - const err = parseError(error); - assert.deepEqual('Unexpected node encountered', err.message); - } - }); - - test('test regular expressions - wrong escape - throw error', () => { - const text = `db.test1.beep.find({ sku: /789$\\/b\\/q })`; - try { - const commands: MongoCommand[] = getAllCommandsFromText(text); - findCommandAtPosition(commands, new Position(0, 0)); - } catch (error) { - assert.equal(error.message, 'Invalid regular expression: /789$\\/b\\/: \\ at end of pattern'); - } - }); - - //Passing the following test should imply the rest of the tests pass too. - // The regex parsing tests following this test should help zero-in on which case isn't handled properly. - test('test regular expression parsing - with many special cases', () => { - const text = `db.test1.beep.find({ sku: /^(hello?= world).*[^0-9]+|(world\\b\\*){0,2}$/ })`; - console.log(text); - const commands: MongoCommand[] = getAllCommandsFromText(text); - console.log('commands', commands); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - console.log('command', command); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '^(hello?= world).*[^0-9]+|(world\\b\\*){0,2}$'); - }); - - test('test regular expression parsing EJSON syntax - with many special cases', () => { - const text = `db.test1.beep.find({ sku: {$regex: "^(hello?= world).*[^0-9]+|(world\\\\b\\\\*){0,2}$"} })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '^(hello?= world).*[^0-9]+|(world\\b\\*){0,2}$'); - }); - - test('test regular expression parsing interoperability', () => { - const text1 = `db.test1.beep.find({ sku: /^(hello?= world).*[^0-9]+|(world\\b\\*){0,2}$/ })`; - const commands1: MongoCommand[] = getAllCommandsFromText(text1); - const command1: MongoCommand = findCommandAtPosition(commands1, new Position(0, 0)); - const generatedRegExp1 = (nonNullProp(command1, 'argumentObjects')[0]).sku; - const text2 = `db.test1.beep.find({ sku: {$regex: "^(hello?= world).*[^0-9]+|(world\\\\b\\\\*){0,2}$"} })`; - const commands2: MongoCommand[] = getAllCommandsFromText(text2); - const command2: MongoCommand = findCommandAtPosition(commands2, new Position(0, 0)); - const generatedRegExp2 = (nonNullProp(command2, 'argumentObjects')[0]).sku; - assert.deepEqual( - [generatedRegExp1.options, generatedRegExp1.pattern], - ['', '^(hello?= world).*[^0-9]+|(world\\b\\*){0,2}$'], - ); - assert.deepEqual( - [generatedRegExp2.options, generatedRegExp2.pattern], - ['', '^(hello?= world).*[^0-9]+|(world\\b\\*){0,2}$'], - ); - }); - - test('test regular expression parsing interoperability - word break', () => { - const text1 = `db.test1.beep.find({ sku: /ker\\b/ })`; // equivalent to user typing out /ker\b/ - const commands1: MongoCommand[] = getAllCommandsFromText(text1); - const command1: MongoCommand = findCommandAtPosition(commands1, new Position(0, 0)); - const generatedRegExp1 = (nonNullProp(command1, 'argumentObjects')[0]).sku; - const text2 = `db.test1.beep.find({ sku: {$regex: "ker\\\\b"} })`; - const commands2: MongoCommand[] = getAllCommandsFromText(text2); - const command2: MongoCommand = findCommandAtPosition(commands2, new Position(0, 0)); - const generatedRegExp2 = (nonNullProp(command2, 'argumentObjects')[0]).sku; - assert.deepEqual([generatedRegExp1.options, generatedRegExp1.pattern], ['', 'ker\\b']); - assert.deepEqual([generatedRegExp2.options, generatedRegExp2.pattern], ['', 'ker\\b']); - }); - - test('test regular expression parsing interoperability - newline', () => { - const text1 = `db.test1.beep.find({ sku: /ker\\n/ })`; // equivalent to user typing out /ker\n/ - const commands1: MongoCommand[] = getAllCommandsFromText(text1); - const command1: MongoCommand = findCommandAtPosition(commands1, new Position(0, 0)); - const generatedRegExp1 = (nonNullProp(command1, 'argumentObjects')[0]).sku; - const text2 = `db.test1.beep.find({ sku: {$regex: "ker\\\\n"} })`; - const commands2: MongoCommand[] = getAllCommandsFromText(text2); - const command2: MongoCommand = findCommandAtPosition(commands2, new Position(0, 0)); - const generatedRegExp2 = (nonNullProp(command2, 'argumentObjects')[0]).sku; - assert.deepEqual([generatedRegExp2.options, generatedRegExp2.pattern], ['', 'ker\\n']); - assert.deepEqual([generatedRegExp1.options, generatedRegExp1.pattern], ['', 'ker\\n']); - }); - test('test regular expression parsing interoperability - carriage return', () => { - const text1 = `db.test1.beep.find({ sku: /ker\\r/ })`; // equivalent to user typing out /ker\r/ - const commands1: MongoCommand[] = getAllCommandsFromText(text1); - const command1: MongoCommand = findCommandAtPosition(commands1, new Position(0, 0)); - const generatedRegExp1 = (nonNullProp(command1, 'argumentObjects')[0]).sku; - const text2 = `db.test1.beep.find({ sku: {$regex: "ker\\\\r"} })`; - const commands2: MongoCommand[] = getAllCommandsFromText(text2); - const command2: MongoCommand = findCommandAtPosition(commands2, new Position(0, 0)); - const generatedRegExp2 = (nonNullProp(command2, 'argumentObjects')[0]).sku; - assert.deepEqual([generatedRegExp1.options, generatedRegExp1.pattern], ['', 'ker\\r']); - assert.deepEqual([generatedRegExp2.options, generatedRegExp2.pattern], ['', 'ker\\r']); - }); - - test('test regular expressions - only pattern, no flags', () => { - const text = `db.test1.beep.find({ sku: { $regex: "789$" } })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '789$'); - }); - - test('test regular expressions - pattern and flags', () => { - const text = `db.test1.beep.find({ sku: { $regex: "789$", $options:"i" } })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, 'i'); - assert.deepEqual(generatedRegExp.pattern, '789$'); - }); - - test('test regular expressions - Intellisense - flag contains invalid option', () => { - const text = `db.test1.beep.find({ sku: { $regex: "789$", $options:"q" } })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.notStrictEqual(nonNullProp(command, 'errors')[0]?.message, undefined); - assert.deepEqual(nonNullProp(command, 'errors')[0].range.start.character, 19); - }); - - test('test regular expression parsing - with groupings', () => { - const text = `db.test1.beep.find({ sku: /(?:hello)\\3/ })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '(?:hello)\\3'); - }); - - test('test regular expression parsing - with special characters', () => { - const text = `db.test1.beep.find({ sku: /(hello)*(world)?(name)+./ })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '(hello)*(world)?(name)+.'); - }); - - test('test regular expression parsing - with boundaries', () => { - const text = `db.test1.beep.find({ sku: /^(hello world)[^0-9]|(world\\b)$/ })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '^(hello world)[^0-9]|(world\\b)$'); - }); - - test('test regular expression parsing - with quantifiers', () => { - const text = `db.test1.beep.find({ sku: /(hello)*[^0-9]+|(world){0,2}./ })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '(hello)*[^0-9]+|(world){0,2}.'); - }); - - test('test regular expression parsing - with conditional', () => { - const text = `db.test1.beep.find({ sku: /(hello?= world)|(world)/ })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, '(hello?= world)|(world)'); - }); - - test('test regular expression parsing - with escaped special characters', () => { - const text = `db.test1.beep.find({ sku: /world\\*\\.\\?\\+/ })`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - const generatedRegExp = (nonNullProp(command, 'argumentObjects')[0]).sku; - assert.deepEqual(generatedRegExp.options, ''); - assert.deepEqual(generatedRegExp.pattern, 'world\\*\\.\\?\\+'); - }); - - test('test chained command: argument aggregation', () => { - testParse('db.timesheets.find({name: "Andy", surname: "Jackson"}).pretty();', { - collection: 'timesheets', - name: 'pretty', - args: [{ name: 'Andy', surname: 'Jackson' }], - }); - }); - - test('Chained command - order of parsed arguments', () => { - testParse('db.timesheets.find({name:"Andy"}).sort({age: 1}).skip(40);', { - collection: 'timesheets', - name: 'skip', - args: [{ name: 'Andy' }, { age: 1 }, 40], - }); - }); - - test('Chained command - missing period', () => { - testParse('db.timesheets.find({name:"Andy"}).sort({age: 1})skip(40);', { - collection: 'timesheets', - name: 'sort', - args: [{ name: 'Andy' }, { age: 1 }], - firstErrorText: - "mismatched input 'skip' expecting {, SingleLineComment, MultiLineComment, ';', '.', 'db'}", - }); - }); - - test('Chained command - mid-type - missing bracket', () => { - testParse('db.timesheets.find({name:"Andy"}).sort', { - collection: 'timesheets', - name: 'sort', - args: [{ name: 'Andy' }], - firstErrorText: "mismatched input '' expecting '('", - }); - }); - - test('Chained command - mid-type - typed the dot, but not the function call', () => { - testParse('db.timesheets.find({name:"Andy"}).', { - collection: 'timesheets', - name: '', - args: [{ name: 'Andy' }], - firstErrorText: "mismatched input '' expecting IDENTIFIER", - }); - }); - - //TODO: Tests to simulate cases where the user hasn't completed typing - - test('test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/688', () => { - for (let q = 0; q <= 2; q++) { - const text = `db.hdr.aggregate([ - { ${wrapInQuotes('$match', q)}: { "CURRENCY_ID": "USD" } }, - ])`; //Note the trailing comma. There should be 1 argument object returned, an array, that has 2 elements - //one expected, and another empty object. - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'hdr'); - assert.deepEqual(command.argumentObjects, [[{ $match: { CURRENCY_ID: 'USD' } }, {}]]); - } - }); - - test('Chained command- test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/785', () => { - testParse('db.timesheets.find({name:"Andy"}).count();', { - collection: 'timesheets', - name: 'count', - args: [{ name: 'Andy' }], - }); - }); - - test('Chained command- test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/795', () => { - testParse('db.timesheets.find({}).limit(10);', { - collection: 'timesheets', - name: 'limit', - args: [{}, 10], - }); - }); - - test('Chained command alternative for rs.slaveOk()- test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/565', () => { - testParse('db.getMongo().setSlaveOk();', { - collection: '', - name: 'setSlaveOk', - args: [], - }); - }); - - test('Multiple line command, from #489', () => { - testParse( - ` - db.finalists.find({name: "Jefferson"}) - . - limit - (10); - `, - { - collection: 'finalists', - name: 'limit', - args: [ - { - name: 'Jefferson', - }, - 10, - ], - }, - ); - }); - - test('test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/703', () => { - for (let q = 0; q <= 2; q++) { - const text = `db.Users.find({ ${wrapInQuotes('user', q)}: { ${wrapInQuotes('$in', q)}: [ "A80", "HPA" ] } },{ ${wrapInQuotes('_id', q)}: false });`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'Users'); - assert.deepEqual(command.argumentObjects, [{ user: { $in: ['A80', 'HPA'] } }, { _id: false }]); - } - }); - - test('test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/691', () => { - for (let q = 0; q <= 2; q++) { - const text = `db.users.aggregate([ - { ${wrapInQuotes('$match', q)}: {${wrapInQuotes('_id', q)}: {"$oid" :"5b23d2ba92b52cf794bdeb9c")}}}, - { ${wrapInQuotes('$project', q)}: { - ${wrapInQuotes('scores', q)}: {${wrapInQuotes('$filter', q)}: { - ${wrapInQuotes('input', q)}: '$scores', - ${wrapInQuotes('as', q)}: 'score', - ${wrapInQuotes('cond', q)}: {${wrapInQuotes('$gt', q)}: ['$$score', 3]} - }} - }} - ])`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'users'); - assert.deepEqual(nonNullProp(command, 'argumentObjects')[0][0], { - $match: { _id: new ObjectId('5b23d2ba92b52cf794bdeb9c') }, - }); - assert.deepEqual(nonNullProp(command, 'argumentObjects')[0][1], { - $project: { - scores: { - $filter: { - input: '$scores', - as: 'score', - cond: { $gt: ['$$score', 3] }, - }, - }, - }, - }); - } - }); - - test('test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/717', () => { - for (let q = 0; q <= 2; q++) { - const text = `db.Users.find({${wrapInQuotes('age', q)} : { ${wrapInQuotes('$in', q)} : [19, 20, 22, 25]}});`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'Users'); - assert.deepEqual(command.argumentObjects, [{ age: { $in: [19, 20, 22, 25] } }]); - } - }); - - test('test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/737', () => { - const text = `db.c1.insert({},f)`; - const commands: MongoCommand[] = getAllCommandsFromText(text); - const command: MongoCommand = findCommandAtPosition(commands, new Position(0, 0)); - assert.deepEqual(command.collection, 'c1'); - assert.deepEqual(command.argumentObjects, [{}, {}]); - assert.deepEqual( - nonNullProp(command, 'errors')[0].message, - "mismatched input 'f' expecting {'{', '[', RegexLiteral, StringLiteral, 'null', BooleanLiteral, NumericLiteral}", - ); - }); - - test('test user issues: https://github.com/Microsoft/vscode-cosmosdb/issues/899 - multi-line comment, not regex', () => { - for (const escape of ['\n', '\r', '\n\r', '\r\n']) { - const text = `db.heroes.count()${escape}/*db.c2.insertMany([${escape}{"name": "Stephen", "age": 38, "male": true},${escape}{"name": "Stephen", "age": 38, "male": true}${escape}]);${escape}*/${escape}${escape}db.heroes.find();`; - const commands = getAllCommandsFromText(text); - assert.equal(commands.length, 2, `Error in parsing ${text}`); - assert.equal(commands[0].name, 'count'); - assert.equal(commands[1].name, 'find'); - } - }); -}); diff --git a/test/mongoShell.test.ts b/test/mongoShell.test.ts deleted file mode 100644 index 4f258e14c..000000000 --- a/test/mongoShell.test.ts +++ /dev/null @@ -1,308 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// CONSIDER: Run in pipeline -import { AzExtFsExtra, parseError } from '@microsoft/vscode-azext-utils'; -import assert from 'assert'; -import * as cp from 'child_process'; -import * as os from 'os'; -import * as path from 'path'; -import type * as vscode from 'vscode'; -import { ext, isWindows, type IDisposable } from '../extension.bundle'; -import { ShellScriptRunner } from '../src/documentdb/scrapbook/ShellScriptRunner'; -import { runWithSetting } from './runWithSetting'; -import { setEnvironmentVariables } from './util/setEnvironmentVariables'; - -const VERBOSE = false; // If true, the output from the Mongo server and shell will be sent to the console for debugging purposes - -let testsSupported: boolean = true; - -if (!isWindows) { - // CONSIDER: Non-Windows - console.warn(`Not running in Windows - skipping MongoShell tests`); - testsSupported = false; -} - -suite('MongoShell', async function (this: Mocha.Suite): Promise { - // https://github.com/mochajs/mocha/issues/2025 - this.timeout(10000); - - async function testIfSupported(title: string, fn?: Mocha.Func | Mocha.AsyncFunc): Promise { - await runWithSetting(ext.settingsKeys.shellTimeout, '60', async () => { - if (testsSupported) { - test(title, fn); - } else { - test(title); - } - }); - } - - // CONSIDER: Make more generic - let mongodCP: cp.ChildProcess; - const mongodPath = 'c:\\Program Files\\MongoDB\\Server\\4.2\\bin\\mongod.exe'; - const mongoPath = 'c:\\Program Files\\MongoDB\\Server\\4.2\\bin\\mongo.exe'; - let mongoDOutput = ''; - let mongoDErrors = ''; - let isClosed = false; - - if (!(await AzExtFsExtra.pathExists(mongodPath))) { - console.log(`Couldn't find mongod.exe at ${mongodPath} - skipping MongoShell tests`); - testsSupported = false; - } else if (!(await AzExtFsExtra.pathExists(mongodPath))) { - console.log(`Couldn't find mongo.exe at ${mongoPath} - skipping MongoShell tests`); - testsSupported = false; - } else { - // Prevent code 100 error: https://stackoverflow.com/questions/41420466/mongodb-shuts-down-with-code-100 - await AzExtFsExtra.ensureDir('D:\\data\\db\\'); - } - - class FakeOutputChannel implements vscode.OutputChannel { - public name: string; - public output: string; - - public append(value: string): void { - assert(value !== undefined); - assert(!value.includes('undefined')); - this.output = this.output ? this.output + os.EOL + value : value; - log(value, 'Output channel: '); - } - public appendLine(value: string): void { - assert(value !== undefined); - this.append(value + os.EOL); - } - public clear(): void {} - public show(preserveFocus?: boolean): void; - public show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - public show(_column?: any, _preserveFocus?: any): void {} - public hide(): void {} - public dispose(): void {} - public replace(_value: string): void {} - } - - function log(text: string, linePrefix: string): void { - text = text.replace(/(^|[\r\n]+)/g, '$1' + linePrefix); - if (VERBOSE) { - console.log(text); - } - } - - async function delay(milliseconds: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, milliseconds); - }); - } - - function executeInShell(command: string): string { - return cp.execSync(command, {}).toString(); - } - - suiteSetup(async () => { - if (testsSupported) { - assert(await AzExtFsExtra.pathExists(mongodPath), "Couldn't find mongod.exe at " + mongodPath); - assert(await AzExtFsExtra.pathExists(mongoPath), "Couldn't find mongo.exe at " + mongoPath); - - // Shut down any still-running mongo server - try { - executeInShell('taskkill /f /im mongod.exe'); - } catch (error) { - assert( - /The process .* not found/.test(parseError(error).message), - `Error killing mongod: ${parseError(error).message}`, - ); - } - - mongodCP = cp.spawn(mongodPath, ['--quiet']); - - mongodCP.stdout?.on('data', (buffer: Buffer) => { - log(buffer.toString(), 'mongo server: '); - mongoDOutput += buffer.toString(); - }); - mongodCP.stderr?.on('data', (buffer: Buffer) => { - log(buffer.toString(), 'mongo server STDERR: '); - mongoDErrors += buffer.toString(); - }); - mongodCP.on('error', (error: unknown) => { - log(parseError(error).message, 'mongo server Error: '); - mongoDErrors += parseError(error).message + os.EOL; - }); - mongodCP.on('close', (code?: number) => { - console.log(`mongo server: Close code=${code}`); - isClosed = true; - if (typeof code === 'number' && code !== 0) { - mongoDErrors += `mongo server: Closed with code "${code}"${os.EOL}`; - } - }); - } - }); - - suiteTeardown(() => { - if (mongodCP) { - mongodCP.kill(); - } - }); - - await testIfSupported('Verify mongod running', async () => { - while (!mongoDOutput.includes('waiting for connections on port 27017')) { - assert.equal(mongoDErrors, '', 'Expected no errors'); - assert(!isClosed); - await delay(50); - } - }); - - async function testShellCommand(options: { - script: string; - expectedResult?: string; - expectedError?: string | RegExp; - expectedOutput?: RegExp; - title?: string; // Defaults to script - args?: string[]; // Defaults to [] - mongoPath?: string; // Defaults to the correct mongo path - env?: { [key: string]: string }; // Add to environment - timeoutSeconds?: number; - }): Promise { - await testIfSupported(options.title || options.script, async () => { - assert(!isClosed); - assert(mongoDErrors === ''); - - let previousEnv: IDisposable | undefined; - let shell: ShellScriptRunner | undefined; - const outputChannel = new FakeOutputChannel(); - - try { - previousEnv = setEnvironmentVariables(options.env || {}); - shell = await ShellScriptRunner.createShellProcessHelper( - options.mongoPath || mongoPath, - options.args || [], - '', - outputChannel, - options.timeoutSeconds || 5, - { isEmulator: false, disableEmulatorSecurity: false }, - ); - const result = await shell.executeScript(options.script); - if (options.expectedError) { - assert(false, `Expected error did not occur: '${options.expectedError}'`); - } - if (options.expectedResult !== undefined) { - assert.equal(result, options.expectedResult); - } - } catch (error) { - const message = parseError(error).message; - - if (options.expectedError instanceof RegExp) { - assert( - options.expectedError.test(message), - `Actual error did not match expected error regex. Actual error: ${message}`, - ); - } else if (typeof options.expectedError === 'string') { - assert.equal(message, options.expectedError); - } else { - assert(false, `Unexpected error during the test: ${message}`); - } - - if (options.expectedOutput instanceof RegExp) { - assert( - options.expectedOutput.test(outputChannel.output), - `Actual contents written to output channel did not match expected regex. Actual output channel contents: ${outputChannel.output}`, - ); - } - } finally { - if (shell) { - shell.dispose(); - } - if (previousEnv) { - previousEnv.dispose(); - } - } - }); - } - - await testShellCommand({ - script: 'use abc', - expectedResult: 'switched to db abc', - }); - - await testShellCommand({ - title: 'Incorrect path', - script: 'use abc', - mongoPath: '/notfound/mongo.exe', - expectedError: /Could not find .*notfound.*mongo.exe/, - }); - - await testShellCommand({ - title: 'Find mongo through PATH', - script: 'use abc', - mongoPath: 'mongo', - expectedResult: 'switched to db abc', - env: { - PATH: process.env.path! + ';' + path.dirname(mongoPath), - }, - }); - - await testShellCommand({ - title: 'With valid argument', - script: 'use abc', - args: ['--quiet'], - expectedResult: 'switched to db abc', - }); - - await testShellCommand({ - title: 'With invalid argument', - script: '', - args: ['--hey-man-how-are-you'], - expectedError: /Error parsing command line: unrecognised option/, - }); - - await testShellCommand({ - title: 'Output window may contain additional information', - script: '', - args: ['-u', 'baduser', '-p', 'badpassword'], - expectedError: /The output window may contain additional information/, - }); - - await testShellCommand({ - title: 'With bad credentials', - script: '', - args: ['-u', 'baduser', '-p', 'badpassword'], - expectedError: /The process exited with code 1/, - expectedOutput: /Authentication failed/, - }); - - await testShellCommand({ - title: 'Process exits immediately', - script: '', - args: ['--version'], - expectedError: /The process exited prematurely/, - }); - - await testShellCommand({ - title: 'Javascript', - script: 'for (var i = 0; i < 123; ++i) { }; i', - expectedResult: '123', - }); - - await testShellCommand({ - title: 'Actual timeout', - script: 'for (var i = 0; i < 10000000; ++i) { }; i', - expectedError: - /Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting./, - timeoutSeconds: 2, - }); - - await testIfSupported("More results than displayed (type 'it' for more -> (More))", async () => { - const shell = await ShellScriptRunner.createShellProcessHelper(mongoPath, [], '', new FakeOutputChannel(), 5, { - disableEmulatorSecurity: false, - isEmulator: false, - }); - await shell.executeScript('db.mongoShellTest.drop()'); - await shell.executeScript('for (var i = 0; i < 50; ++i) { db.mongoShellTest.insert({a:i}); }'); - - const result = await shell.executeScript('db.mongoShellTest.find().pretty()'); - - assert(!result.includes('Type "it" for more')); - assert(result.includes('(More)')); - - shell.dispose(); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 894220ad0..f8f79d3a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,6 @@ ] */ }, - "exclude": ["node_modules", ".vscode-test"] + "exclude": ["node_modules", ".vscode-test", "packages/*/dist"], + "references": [{ "path": "packages/schema-analyzer" }, { "path": "packages/documentdb-constants" }] } diff --git a/webpack.config.ext.js b/webpack.config.ext.js index 79e16d1fd..de547515f 100644 --- a/webpack.config.ext.js +++ b/webpack.config.ext.js @@ -21,10 +21,8 @@ module.exports = (env, { mode }) => { mode: mode || 'none', node: { __filename: false, __dirname: false }, entry: { - // 'extension.bundle.ts': './src/extension.ts', // Is still necessary? - './vscode-documentdb-scrapbook-language-languageServer.bundle': - './src/documentdb/scrapbook/languageServer.ts', main: './main.ts', + scratchpadWorker: './src/documentdb/scratchpad/scratchpadWorker.ts', }, output: { path: path.resolve(__dirname, 'dist'), @@ -56,32 +54,42 @@ module.exports = (env, { mode }) => { ], }, externalsType: 'node-commonjs', - externals: { - vs: 'vs', - vscode: 'commonjs vscode', - /* Mongodb optional dependencies */ - kerberos: 'commonjs kerberos', - '@mongodb-js/zstd': 'commonjs @mongodb-js/zstd', - '@aws-sdk/credential-providers': 'commonjs @aws-sdk/credential-providers', - 'gcp-metadata': 'commonjs gcp-metadata', - snappy: 'commonjs snappy', - socks: 'commonjs socks', - aws4: 'commonjs aws4', - 'mongodb-client-encryption': 'commonjs mongodb-client-encryption', - /* PG optional dependencies */ - 'pg-native': 'commonjs pg-native', - }, + externals: [ + { + vs: 'vs', + vscode: 'commonjs vscode', + /* Mongodb optional dependencies */ + kerberos: 'commonjs kerberos', + '@mongodb-js/zstd': 'commonjs @mongodb-js/zstd', + '@aws-sdk/credential-providers': 'commonjs @aws-sdk/credential-providers', + 'gcp-metadata': 'commonjs gcp-metadata', + snappy: 'commonjs snappy', + socks: 'commonjs socks', + aws4: 'commonjs aws4', + 'mongodb-client-encryption': 'commonjs mongodb-client-encryption', + /* @mongosh transitive optional dependencies */ + electron: 'commonjs electron', + 'os-dns-native': 'commonjs os-dns-native', + 'cpu-features': 'commonjs cpu-features', + ssh2: 'commonjs ssh2', + 'win-export-certificate-and-key': 'commonjs win-export-certificate-and-key', + 'macos-export-certificate-and-key': 'commonjs macos-export-certificate-and-key', + /* PG optional dependencies */ + 'pg-native': 'commonjs pg-native', + }, + // Handle @babel/preset-typescript and its subpath imports (e.g. /package.json) + ({ request }, callback) => { + if (request && request.startsWith('@babel/preset-typescript')) { + return callback(null, `commonjs ${request}`); + } + callback(); + }, + ], resolve: { roots: [__dirname], // conditionNames: ['import', 'require', 'node'], // Uncomment when we will use VSCode what supports modules mainFields: ['module', 'main'], extensions: ['.js', '.ts'], - alias: { - 'vscode-languageserver-types': path.resolve( - __dirname, - 'node_modules/vscode-languageserver-types/lib/esm/main.js', - ), - }, }, module: { rules: [ @@ -124,10 +132,6 @@ module.exports = (env, { mode }) => { // - The dist folder should be ready to be published to the marketplace and be only one working folder new CopyWebpackPlugin({ patterns: [ - { - from: 'grammar', - to: 'grammar', - }, { from: 'l10n', to: 'l10n', @@ -147,6 +151,14 @@ module.exports = (env, { mode }) => { from: 'package.nls.json', to: 'package.nls.json', }, + { + from: 'scratchpad-language-configuration.json', + to: 'scratchpad-language-configuration.json', + }, + { + from: 'syntaxes', + to: 'syntaxes', + }, { from: 'package.nls.*.json', to: '[name][ext]', @@ -200,6 +212,16 @@ module.exports = (env, { mode }) => { }), ].filter(Boolean), devtool: isDev ? 'source-map' : false, + // Filter known warnings from @mongosh transitive dependencies. + // These are all "Critical dependency" warnings from @babel/core, + // browserslist, and express that use dynamic require() patterns + // webpack can't statically analyze. None execute at runtime. + // See docs/plan/06-scrapbook-rebuild.md ยง"Webpack Externals" for details. + ignoreWarnings: [ + { module: /node_modules[\\/]@babel[\\/]core/ }, + { module: /node_modules[\\/]browserslist/ }, + { module: /node_modules[\\/]@mongodb-js[\\/]oidc-plugin[\\/]node_modules[\\/]express/ }, + ], infrastructureLogging: { level: 'log', // enables logging required for problem matchers },