diff --git a/.docsettings.yml b/.docsettings.yml index 63968922ab94..81928de5b3f3 100644 --- a/.docsettings.yml +++ b/.docsettings.yml @@ -39,6 +39,19 @@ known_content_issues: - ['packages/@azure/cognitiveservices-visualsearch/README.md', '#1583'] - ['packages/@azure/cognitiveservices-websearch/README.md', '#1583'] - ['packages/@azure/eventgrid/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/src/test/readme.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/readme.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/UserManagement/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/TodoApp/readme.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/ChangeFeed/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/MultiRegionWrite/readme.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/ServerSideScripts/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/ServerSideScripts/JS/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/IndexManagement/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/ItemManagement/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/DatabaseManagement/README.md', '#1583'] + - ['sdk/cosmosdb/cosmos/samples/ContainerManagement/README.md', '#1583'] - ['sdk/eventhub/README.md', '#1583'] - ['sdk/eventhub/event-hubs/README.md', '#1583'] - ['sdk/eventhub/event-hubs/samples/README.md', '#1583'] diff --git a/package.json b/package.json index 9f4e65fd8141..c33945b025da 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "check:everything": "ts-node ./.scripts/checkEverything.ts", "latest": "ts-node ./.scripts/latest.ts", "local": "ts-node ./.scripts/local.ts", + "install-client-cosmos": "cd sdk/cosmosdb/cosmos && npm install", "install-client-eventhubs-client": "cd sdk/eventhub/event-hubs && npm install", "install-client-eventhubs-processor": "cd sdk/eventhub/event-processor-host && npm install", "install-client-keyvault": "cd sdk/keyvault/keyvault && npm install", @@ -41,6 +42,7 @@ "install-client-storage-file": "cd sdk/storage/storage-file && npm install", "install-client-storage-queue": "cd sdk/storage/storage-queue && npm install", "install-client": "npm-run-all -p -l install-client-*", + "build-client-cosmos": "cd sdk/cosmosdb/cosmos && npm run build", "build-client-eventhubs-client": "cd sdk/eventhub/event-hubs && npm run build", "build-client-eventhubs-processor": "cd sdk/eventhub/event-processor-host && npm run build", "build-client-keyvault": "cd sdk/keyvault/keyvault && npm run build", @@ -50,6 +52,7 @@ "build-client-storage-file": "cd sdk/storage/storage-file && npm run build", "build-client-storage-queue": "cd sdk/storage/storage-queue && npm run build", "build-client": "npm-run-all -p -l build-client-*", + "pack-client-cosmos": "cd sdk/cosmosdb/cosmos && npm pack", "pack-client-eventhubs-client": "cd sdk/eventhub/event-hubs && npm pack", "pack-client-eventhubs-processor": "cd sdk/eventhub/event-processor-host && npm pack", "pack-client-keyvault": "cd sdk/keyvault/keyvault && npm pack", @@ -61,9 +64,10 @@ "pack-client": "npm-run-all -p -l pack-client-*", "test-client-template": "cd sdk/template/template && npm run test", "test-client": "npm-run-all -p -l \"test-client-* -- {@}\"", + "test-client-cosmos": "echo skipped", "test-client-eventhubs-client": "echo skipped", "test-client-eventhubs-processor": "echo skipped", - "test-client-servicebus": "echo skipped", + "test-client-servicebus": "echo skipped", "test-client-storage-blob": "echo skipped", "test-client-storage-file": "echo skipped", "test-client-storage-queue": "echo skipped", @@ -80,6 +84,7 @@ "live-test-client-storage-file-browser": "cd sdk/storage/storage-file && npm run unit-browser", "live-test-client-storage-queue-browser": "cd sdk/storage/storage-queue && npm run unit-browser", "live-test-client": "npm-run-all -p -l \"live-test-client-* -- {@}\"", + "audit-client-cosmos": "cd sdk/cosmosdb/cosmos && npm audit", "audit-client-eventhubs-client": "cd sdk/eventhub/event-hubs && npm i --package-lock-only && npm audit", "audit-client-eventhubs-processor": "cd sdk/eventhub/event-processor-host && npm i --package-lock-only && npm audit", "audit-client-keyvault": "cd sdk/keyvault/keyvault && npm i --package-lock-only && npm audit", @@ -89,6 +94,7 @@ "audit-client-storage-file": "cd sdk/storage/storage-file && npm i --package-lock-only && npm audit", "audit-client-storage-queue": "cd sdk/storage/storage-queue && npm i --package-lock-only && npm audit", "audit-client": "npm-run-all -p -l audit-client-*", + "lint-client-cosmos": "cd sdk/cosmosdb/cosmos && npm run lint", "lint-client-template": "cd sdk/template/template && npm run eslint", "lint-client": "npm-run-all -p -l lint-client-*", "analyze-client": "npm run audit-client && npm run lint-client" diff --git a/sdk/cosmosdb/cosmos/.azure-pipelines/cosmos.test.nightly-emulator.yml b/sdk/cosmosdb/cosmos/.azure-pipelines/cosmos.test.nightly-emulator.yml new file mode 100644 index 000000000000..9080a5fea1ea --- /dev/null +++ b/sdk/cosmosdb/cosmos/.azure-pipelines/cosmos.test.nightly-emulator.yml @@ -0,0 +1,57 @@ +# Node.js +# Build a general Node.js project with npm. +# Add steps that analyze code, save build artifacts, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript + +trigger: + branches: + include: + - master + - cosmos-build-master + paths: + include: + - sdk/cosmosdb/cosmos/* + +pr: + branches: + include: + - master + - cosmos-build-master + paths: + include: + - sdk/cosmosdb/cosmos/* + +jobs: + - job: NightlyEmulator + pool: + vmImage: vs2017-win2016 + steps: + - task: azure-cosmosdb.emulator-internal-preview.run-cosmosdbemulatorcontainer.CosmosDbEmulator@2 + displayName: "Run Azure Cosmos DB Emulator container" + inputs: + username: "$(cosmos-cosmosdb-azurecr-io-username)" + password: "$(cosmos-cosmosdb-azurecr-io-password)" + defaultPartitionCount: 25 + - task: NodeTool@0 + inputs: + versionSpec: "6.x" + displayName: "Install Node.js" + + - script: npm run install-client-cosmos + displayName: "npm install" + + - script: npm run build-client-cosmos + displayName: "npm run build" + + - bash: cd sdk/cosmosdb/cosmos && ACCOUNT_HOST=$COSMOSDBEMULATOR_ENDPOINT NODE_TLS_REJECT_UNAUTHORIZED="0" npm run test -- --reporter mocha-multi-reporters --reporter-options spec=-,mocha-junit-reporter=- $(AdditionalTestArguments) + failOnStderr: true + displayName: "npm test" + env: + MOCHA_TIMEOUT: 100000 + + - task: PublishTestResults@2 + inputs: + testResultsFiles: '**/xunit.xml' + testRunTitle: 'Cosmos $(OSName) Node $(NodeVersion) - Node' + condition: succeededOrFailed() + displayName: 'Publish node test results' diff --git a/sdk/cosmosdb/cosmos/.azure-pipelines/cosmos.test.yml b/sdk/cosmosdb/cosmos/.azure-pipelines/cosmos.test.yml new file mode 100644 index 000000000000..2488e5dd643b --- /dev/null +++ b/sdk/cosmosdb/cosmos/.azure-pipelines/cosmos.test.yml @@ -0,0 +1,55 @@ +# Node.js +# Build a general Node.js project with npm. +# Add steps that analyze code, save build artifacts, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript + +trigger: + branches: + include: + - master + - cosmos-build-master + paths: + include: + - sdk/cosmosdb/cosmos/* + +pr: + branches: + include: + - master + - cosmos-build-master + paths: + include: + - sdk/cosmosdb/cosmos/* + +jobs: + - job: Emulator + pool: + vmImage: vs2017-win2016 + steps: + - task: azure-cosmosdb.emulator-public-preview.run-cosmosdbemulatorcontainer.CosmosDbEmulator@2 + displayName: "Run Azure Cosmos DB Emulator container" + inputs: + defaultPartitionCount: 25 + - task: NodeTool@0 + inputs: + versionSpec: "6.x" + displayName: "Install Node.js" + + - script: npm run install-client-cosmos + displayName: "npm install" + + - script: npm run build-client-cosmos + displayName: "npm run build" + + - bash: cd sdk/cosmosdb/cosmos && ACCOUNT_HOST=$COSMOSDBEMULATOR_ENDPOINT NODE_TLS_REJECT_UNAUTHORIZED="0" npm run test -- --reporter mocha-multi-reporters --reporter-options spec=-,mocha-junit-reporter=- $(AdditionalTestArguments) + failOnStderr: true + displayName: "npm test" + env: + MOCHA_TIMEOUT: 100000 + + - task: PublishTestResults@2 + inputs: + testResultsFiles: '**/xunit.xml' + testRunTitle: 'Cosmos $(OSName) Node $(NodeVersion) - Node' + condition: succeededOrFailed() + displayName: 'Publish node test results' diff --git a/sdk/cosmosdb/cosmos/.editorConfig b/sdk/cosmosdb/cosmos/.editorConfig new file mode 100644 index 000000000000..f8c17c3f155b --- /dev/null +++ b/sdk/cosmosdb/cosmos/.editorConfig @@ -0,0 +1,16 @@ +# http://editorconfig.org +root = true + +[*] +end_of_line = crlf +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.ts] +indent_size = 2 diff --git a/sdk/cosmosdb/cosmos/.gitattributes b/sdk/cosmosdb/cosmos/.gitattributes new file mode 100644 index 000000000000..412eeda78dc9 --- /dev/null +++ b/sdk/cosmosdb/cosmos/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/sdk/cosmosdb/cosmos/.gitignore b/sdk/cosmosdb/cosmos/.gitignore new file mode 100644 index 000000000000..c8495698d1fb --- /dev/null +++ b/sdk/cosmosdb/cosmos/.gitignore @@ -0,0 +1,269 @@ +*.csproj +*.cmd + +config.js +.vs/ + +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +obj/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +#packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + + +############# +## Python +############# + +*.py[cod] + +# Packages +*.egg +*.egg-info +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +############# +## Node.js +############# + +# Logs +logs + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Dependency sub-directories for samples +**/node_nodules/ + +*.dat + +lib/** + +*.tgz + +ts-test/package-lock.json +ts-test/package.json +ts-test/*.js + +dist-esm +dist-test \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/.npmignore b/sdk/cosmosdb/cosmos/.npmignore new file mode 100644 index 000000000000..27ff3204bfd0 --- /dev/null +++ b/sdk/cosmosdb/cosmos/.npmignore @@ -0,0 +1,8 @@ +.git +.vscode +samples +.gitattributes +.gitignore +*.code-workspace +*.tgz +ts-test/ \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/.prettierrc.json b/sdk/cosmosdb/cosmos/.prettierrc.json new file mode 100644 index 000000000000..ac615848e118 --- /dev/null +++ b/sdk/cosmosdb/cosmos/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "tabWidth": 2 +} \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/.travis.yml b/sdk/cosmosdb/cosmos/.travis.yml new file mode 100644 index 000000000000..55589b9b5ca7 --- /dev/null +++ b/sdk/cosmosdb/cosmos/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +notifications: + email: false +node_js: + - 10 +script: + - npm run ci diff --git a/sdk/cosmosdb/cosmos/.vscode/launch.json b/sdk/cosmosdb/cosmos/.vscode/launch.json new file mode 100644 index 000000000000..3050394177f6 --- /dev/null +++ b/sdk/cosmosdb/cosmos/.vscode/launch.json @@ -0,0 +1,53 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": ["-u", "tdd", "--colors", "${workspaceFolder}/lib/test/**/*.js", "-g", ".*Location Cache.*"], + "internalConsoleOptions": "openOnSessionStart", + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/lib/**"], + "env": { + "MOCHA_TIMEOUT": "999999" + } + }, + { + "type": "node", + "request": "attach", + "name": "Attach by Process ID", + "processId": "${command:PickProcess}" + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/samples/TodoApp/bin/www" + }, + { + "type": "node", + "request": "launch", + "name": "Debug file", + "program": "${file}", + "cwd": "${fileDirname}", + "env": { + "NODE_TLS_REJECT_UNAUTHORIZED": "0" + } + }, + { + "type": "node", + "request": "launch", + "name": "MutliRegionWrite - Debug", + "args": ["${relativeFile}"], + "runtimeArgs": ["-r", "ts-node/register"], + "sourceMaps": true, + "cwd": "${workspaceRoot}", + "protocol": "inspector" + } + ] +} diff --git a/sdk/cosmosdb/cosmos/.vscode/settings.json b/sdk/cosmosdb/cosmos/.vscode/settings.json new file mode 100644 index 000000000000..75dab0788698 --- /dev/null +++ b/sdk/cosmosdb/cosmos/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "mocha.files.glob":"test/legacy/**/*.js", + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/.vscode/tasks.json b/sdk/cosmosdb/cosmos/.vscode/tasks.json new file mode 100644 index 000000000000..cc78a3611cd1 --- /dev/null +++ b/sdk/cosmosdb/cosmos/.vscode/tasks.json @@ -0,0 +1,22 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "problemMatcher": [ + "$tsc", + "$tslint5" + ] + }, + { + "type": "npm", + "script": "compile", + "problemMatcher": [ + "$tsc" + ] + } + ] +} diff --git a/sdk/cosmosdb/cosmos/Contributing.md b/sdk/cosmosdb/cosmos/Contributing.md new file mode 100644 index 000000000000..4e2d3191db38 --- /dev/null +++ b/sdk/cosmosdb/cosmos/Contributing.md @@ -0,0 +1 @@ +Please read the contributing guidelines from the [Azure Team](https://azure.microsoft.com/en-us/blog/simple-contribution-to-azure-documentation-and-sdk/) diff --git a/sdk/cosmosdb/cosmos/LICENSE b/sdk/cosmosdb/cosmos/LICENSE new file mode 100644 index 000000000000..862b2ee3ad32 --- /dev/null +++ b/sdk/cosmosdb/cosmos/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) 2017 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/PoliCheckExclusions.txt b/sdk/cosmosdb/cosmos/PoliCheckExclusions.txt new file mode 100644 index 000000000000..483a9c42c3c2 --- /dev/null +++ b/sdk/cosmosdb/cosmos/PoliCheckExclusions.txt @@ -0,0 +1 @@ +package-lock.json \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/README.md b/sdk/cosmosdb/cosmos/README.md new file mode 100644 index 000000000000..ecd5048429ba --- /dev/null +++ b/sdk/cosmosdb/cosmos/README.md @@ -0,0 +1,61 @@ +# Microsoft Azure Cosmos JavaScript SDK + +This project provides JavaScript & Node.js SDK library for [SQL API](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-sql-query) of [Azure Cosmos +Database Service](https://azure.microsoft.com/en-us/services/cosmos-db/). This project also includes samples, tools, and utilities. + +[![latest npm badge](https://img.shields.io/npm/v/%40azure%2Fcosmos/latest.svg)](https://www.npmjs.com/package/@azure/cosmos) [![Build Status](https://travis-ci.org/Azure/azure-cosmos-js.svg?branch=master)](https://travis-ci.org/Azure/azure-cosmos-js) [![Build Status](https://cosmos-db-sdk-public.visualstudio.com/cosmos-db-sdk-public/_apis/build/status/azure-cosmos-js-Emulator?branchName=master)](https://cosmos-db-sdk-public.visualstudio.com/cosmos-db-sdk-public/_build/latest?definitionId=1&branchName=master) + +```js +// JavaScript +const cosmos = require("@azure/cosmos"); +const CosmosClient = cosmos.CosmosClient; + +const endpoint = "[hostendpoint]"; // Add your endpoint +const masterKey = "[database account masterkey]"; // Add the masterkey of the endpoint +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +const databaseDefinition = { id: "sample database" }; +const collectionDefinition = { id: "sample collection" }; +const documentDefinition = { id: "hello world doc", content: "Hello World!" }; + +async function helloCosmos() { + const { database: db } = await client.databases.create(databaseDefinition); + console.log("created db"); + + const { container } = await db.containers.create(collectionDefinition); + console.log("created collection"); + + const { body } = await container.items.create(documentDefinition); + console.log("Created item with content: ", body.content); + + await db.delete(); + console.log("Deleted database"); +} + +helloCosmos().catch(err => { + console.error(err); +}); +``` + +## Useful links + +- [Welcome to Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/community) +- [Quick start](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-nodejs-get-started) +- [Tutorial](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-nodejs-application) +- [Samples](https://github.com/Azure/azure-cosmos-js/tree/master/samples) +- [Introduction to Resource Model of Azure Cosmos DB Service](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-resources) +- [Introduction to SQL API of Azure Cosmos DB Service](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-sql-query) +- [Partitioning](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-partition-data) +- [API Documentation](https://docs.microsoft.com/en-us/javascript/api/%40azure/cosmos/?view=azure-node-latest) + +## Need Help? + +Tweet us with #CosmosDB and we'll respond on Twitter. Be sure to check out the Microsoft Azure [Developer Forums on MSDN](https://social.msdn.microsoft.com/forums/azure/en-US/home?forum=AzureDocument) or the [Developer Forums on Stack Overflow](https://stackoverflow.com/questions/tagged/azure-cosmosdb) if you have trouble with the provided code. + +## Contribute Code or Provide Feedback + +For our rules and guidelines on contributing, please see [Microsoft's contributor guide].(https://docs.microsoft.com/en-us/contribute/). + +For information on how build and test this repo, please see [./dev.md](./dev.md). + +If you encounter any bugs with the library please file an issue in the [Issues](https://github.com/Azure/azure-cosmos-js/issues) section of the project. diff --git a/sdk/cosmosdb/cosmos/SDK + Samples Workspace.code-workspace b/sdk/cosmosdb/cosmos/SDK + Samples Workspace.code-workspace new file mode 100644 index 000000000000..f4a3a59a67e2 --- /dev/null +++ b/sdk/cosmosdb/cosmos/SDK + Samples Workspace.code-workspace @@ -0,0 +1,29 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "samples\\TodoApp" + } + ], + "settings": { + "mocha.files.glob":"test/**/*.spec.ts", + "mocha.sideBarOptions": { + "lens": true, + "decoration": true, + "autoUpdateTime": 0, + "showDebugTestStatus": true + }, + "mocha.runTestsOnSave": "false", + "mocha.logVerbose": true, + "mocha.options": { + "compilers":{ + "ts": "ts-node/register" + } + }, + "mocha.requires": [ + "ts-node/register" + ] + } +} \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/browser-test.js b/sdk/cosmosdb/cosmos/browser-test.js new file mode 100644 index 000000000000..84cae900af46 --- /dev/null +++ b/sdk/cosmosdb/cosmos/browser-test.js @@ -0,0 +1,3 @@ +const tests = require.context("./lib/", true, /\.spec\.js$/); + +tests.keys().forEach(tests); \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/changelog.md b/sdk/cosmosdb/cosmos/changelog.md new file mode 100644 index 000000000000..64ccd102f59a --- /dev/null +++ b/sdk/cosmosdb/cosmos/changelog.md @@ -0,0 +1,198 @@ +## Changes in 2.0.1 + +- Fix type issue (See #141) + +## Changes in 2.0.0 + +- Multi-region Write support +- Shared resource response properties added to responses +- Changed query to allow for customer types for all Resource types +- Modified items.query to allow for cross partition query +- Misc fixes/doc updates + +## Changes in 2.0.0-3 + +- New object model +- Updated documentation and samples +- Improved types +- Added `createdIfNotExists` for database and container +- Added prettier +- Added public CI (Travis and VSTS) + +## Changes in 2.0.0-0 + +- Added Promise support +- Added token handler option for auth +- typings now emitted from source (moved source to TypeScript) +- Added CosmosClient (DocumentClient now considered deprecated) + +## Changes in 1.14.4 : + +- npm documentation fixed. + +## Changes in 1.14.3 : + +- Added support for default retries on connection issues. +- Added support to read collection change feed. +- Fixed session consistency bug that intermittently caused "read session not available". +- Added support for query metrics. +- Modified http Agent's maximum number of connections. + +## Changes in 1.14.2 : + +- Updated documentation to use Azure Cosmos DB. +- Added Support for proxyUrl setting in ConnectionPolicy. + +## Changes in 1.14.1 : + +- Minor fix for case sensitive file systems. + +## Changes in 1.14.0 : + +- Adds support for Session Consistency. +- This SDK version requires the latest version of Azure Cosmos DB Emulator available for download from https://aka.ms/cosmosdb-emulator. + +## Changes in 1.13.0 : + +- Splitproofed cross partition queries. +- Adds supports for resource link with leading and trailing slashes (and corresponding tests). + +## Changes in 1.12.2 : + +- npm documentation fixed. + +## Changes in 1.12.1 : + +- Fixed bug in executeStoredProcedure where documents involved had special unicode characters (LS, PS). +- Fixed bug in handling documents with unicode characters in partition key. +- Fixed support for creating collection with name media (github #114). +- Fixed support for permission authorization token (github #178). + +## Changes in 1.12.0 : + +- Added support for Request Unit per Minute (RU/m) feature. +- Added support for a new consistency level called ConsistentPrefix. +- Added support for UriFactory. +- Fixed the unicode support bug (github #171) + +## Changes in 1.11.0 : + +- Added the support for aggregation queries (COUNT, MIN, MAX, SUM, and AVG). +- Added the option for controlling degree of parallelism for cross partition queries. +- Added the option for disabling SSL verification when running against Emulator. +- Lowered minimum throughput on partitioned collections from 10,100 RU/s to 2500 RU/s. +- Fixed the continuation token bug for single partition collection (github #107). +- Fixed the executeStoredProcedure bug in handling 0 as single param (github #155). + +## Changes in 1.10.2 : + +- Fixed user-agent header to include the SDK version. +- Minor code cleanup. + +## Changes in 1.10.1 : + +- Disabling SSL verification when using the SDK to target the emulator(hostname=localhost). +- Added support for enabling script logging during stored procedure execution. + +## Changes in 1.10.0 : + +- Added support for cross partition parallel queries. +- Added support for TOP/ORDER BY queries for partitioned collections. + +## Changes in 1.9.0 : + +- Added retry policy support for throttled requests. (Throttled requests receive a request rate too large exception, error code 429.) + By default, DocumentClient retries nine times for each request when error code 429 is encountered, honoring the retryAfter time in the response header. + A fixed retry interval time can now be set as part of the RetryOptions property on the ConnectionPolicy object if you want to ignore the retryAfter time returned by server between the retries. + DocumentClient now waits for a maximum of 30 seconds for each request that is being throttled (irrespective of retry count) and returns the response with error code 429. + This time can also be overriden in the RetryOptions property on ConnectionPolicy object. + +- DocumentClient now returns x-ms-throttle-retry-count and x-ms-throttle-retry-wait-time-ms as the response headers in every request to denote the throttle retry count and the cummulative time the request waited between the retries. + +- The RetryOptions class was added, exposing the RetryOptions property on the ConnectionPolicy class that can be used to override some of the default retry options. + +## Changes in 1.8.0 : + +- Added the support for geo-replicated database accounts. + +## Changes in 1.7.0 : + +- Added the support for TimeToLive(TTL) feature for documents. + +## Changes in 1.6.0 : + +- Added support for Partitioned Collections. +- Added support for new offer types. + +## Changes in 1.5.6 : + +- Fixed RangePartitionResolver.resolveForRead bug where it was not returning links due to a bad concat of results. +- Move compareFunction from Range class to RangePartitionResolver class. + +## Changes in 1.5.5 : + +- Fixed hashParitionResolver resolveForRead(): When no partition key supplied was throwing exception, instead of returning a list of all registered links. + +## Changes in 1.5.4 : + +- Dedicated HTTPS Agent: Avoid modifying the global. Use a dedicated agent for all of the lib’s requests. + +## Changes in 1.5.3 : + +- Properly handle dashes in the mediaIds. + +## Changes in 1.5.2 : + +- Fix memory leak. + +## Changes in 1.5.1 : + +- Renamed "Hash" directory to "hash". + +## Changes in 1.5.0 : + +- Added client-side sharding support. +- Added hash partition resolver implementation. +- Added range partitoin resolver implementation. + +## Changes in 1.4.0 : + +- Implement Upsert. New upsertXXX methods on documentClient. + +## Changes in 1.3.0 : + +- Skipped to bring version numbers in alignment with other SDKs. + +## Changes in 1.2.2 : + +- Split Q promises wrapper to new repository. +- Update to package file for npm registry. + +## Changes in 1.2.1 : + +- Implements ID Based Routing. +- Fixes Issue [#49](https://github.com/Azure/azure-documentdb-node/issues/49) - current property conflicts with method current(). + +## Changes in 1.2.0 : + +- Added support for GeoSpatial index. +- Validates id property for all resources. Ids for resources cannot contain ?, /, #, \\, characters or end with a space. +- Adds new header "index transformation progress" to ResourceResponse. + +## Changes in 1.1.0 : + +- Implements V2 indexing policy. + +## Changes in 1.0.3 : + +- Issue [#40](https://github.com/Azure/azure-documentdb-node/issues/40) - Implemented eslint and grunt configurations in the core and promise SDK. + +## Changes in 1.0.2 : + +- Issue [#45](https://github.com/Azure/azure-documentdb-node/issues/45) - Promises wrapper does not include header with error. + +## Changes in 1.0.1 : + +- Implemented ability to query for conflicts by adding readConflicts, readConflictAsync, queryConflicts. +- Updated API documentation. +- Issue [#41](https://github.com/Azure/azure-documentdb-node/issues/41) - client.createDocumentAsync error. diff --git a/sdk/cosmosdb/cosmos/dev.md b/sdk/cosmosdb/cosmos/dev.md new file mode 100644 index 000000000000..1b93f1330617 --- /dev/null +++ b/sdk/cosmosdb/cosmos/dev.md @@ -0,0 +1,92 @@ +# Dev docs + +```bash +# Info on how to build the SDK and run the samples + +npm i # install dependencies and tools +npm run build # builds the project +npm run test # runs the tests + +# see below prereqs, more commands, and config options +``` + +## Pre-reqs + +- [Node v6 or above](https://nodejs.org/en/) + - Recommend using Node 8 LTS + - Recommend using a Node version manager ([nvm-windows](https://github.com/coreybutler/nvm-windows/releases), [nvm (mac/linux)](https://github.com/creationix/nvm/), [n (mac/linux)](https://github.com/tj/n)) +- npm (comes with Node)(all tooling is done via npm scripts) +- All OS's should be supported (emulator only runs on windows) +- (Recommended) [VS Code](https://code.visualstudio.com/) +- Cosmos DB (Azure or Local Emulator) (emulator only works on Windows, right now, so mac/linux needs a cloud instance) + +## Available commands + +``` +Lifecycle scripts included in @azure/cosmos: + test + mocha -r ./src/test/common/setup.ts ./lib/test/ --recursive --timeout 100000 -i -g .*ignore.js + +available via `npm run-script`: + clean + rimraf lib + lint + tslint --project tsconfig.json + format + prettier --write --config .prettierrc.json "src/**/*.ts" + check-format + prettier --list-different --config .prettierrc.json "src/**/*.ts" + compile + echo Using TypeScript && tsc --version && tsc --pretty + compile-prod + echo Using TypeScript && tsc --version && tsc -p tsconfig.prod.json --pretty + docs + typedoc --excludePrivate --exclude "**/test/**" --mode file --out ./lib/docs ./src + pack + webpack -d + pack-prod + webpack -p + build + npm run clean && npm run check-format && npm run lint && npm run compile && npm run docs && npm run pack + build-prod + npm run clean && npm run check-format && npm run lint && npm run compile-prod && npm run docs && npm run pack-prod + test-ts + mocha -r ts-node/register -r ./src/test/common/setup.ts ./src/test/**/*.spec.ts --recursive --timeout 100000 -i -g .*ignore.js + test-browser + karma start ./karma.config.js --single-run +``` + +## Building the SDK + +1. Install dependencies `npm i` +2. Build library `npm run build` + +## Testing the SDK + +1. Build the SDK (see above) +2. Run all tests `npm run test` + +The above assumes you have the local emulator installed. If you need to use a remote endpoint, check out the `ACCOUNT_HOST` and `ACCOUNT_KEY` below. + +### Test config + +Extra environment variables you can use: + +- `MOCHA_TIMEOUT`: time in milliseconds before timeout (default is different per test, mostly 10-20 seconds). Useful to set to 999999 during debugging. +- `ACCOUNT_HOST`: account endpoint for testing (default is the emulator running on localhost:8081 +- `ACCOUNT_KEY`: masterkey for testing (default is the emulators default key) +- `TESTS_MULTIREGION`: enables tests that require a multi-region write enabled database account with at least two regions, and disables tests that won't work with multi-region write enabled. + +## VS Code + +You can also run the tests via VS Code. There should already be a launch.json for launching the mocha tests. You can modify the `-g` setting to run a specific test. (aka change `.*` to `.*validate database CRUD.*` or whatever your test cases are called) + +You can also build via the configured tasks (`build` does a full build, and `compile` just does a typescript compile with no linting, formatting, etc.) + +# Samples + +Build the SDK and make sure the tests run before you try any samples (they depend on the SDK) + +- [TodoApp](./samples/TodoApp) + +We recommend using [VS code's multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces) for testing the samples, especially if you're using the samples to test the SDK. There is a `launch.json` for the samples thave have been updated and multi-root workspaces will show all `launch.json`s. diff --git a/sdk/cosmosdb/cosmos/karma.config.js b/sdk/cosmosdb/cosmos/karma.config.js new file mode 100644 index 000000000000..2d428cf845e1 --- /dev/null +++ b/sdk/cosmosdb/cosmos/karma.config.js @@ -0,0 +1,81 @@ +// Karma configuration +// Generated on Thu May 24 2018 16:35:54 GMT-0700 (Pacific Daylight Time) + +module.exports = function (config) { + config.set({ + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + + + // list of files / patterns to load in the browser + files: [ + './browser-test.js' + ], + + + // list of files / patterns to exclude + exclude: [ + './lib/dist/**' + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + './browser-test.js': [ 'webpack', 'sourcemap' ] + }, + + webpack: require('./webpack.config.js'), + + webpackMiddleware: { + stats: "errors-only" + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress', 'mocha'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_DEBUG, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + browserDisconnectTimeout: 120000, + browserNoActivityTimeout: 120000, + browserDisconnectTolerance: 5, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome_without_security'], + + customLaunchers: { + Chrome_without_security: { + base: 'Chrome', + flags: ['--disable-web-security', '--auto-open-devtools-for-tabs'] + } + }, + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/sdk/cosmosdb/cosmos/package-lock.json b/sdk/cosmosdb/cosmos/package-lock.json new file mode 100644 index 000000000000..5f06647eb75f --- /dev/null +++ b/sdk/cosmosdb/cosmos/package-lock.json @@ -0,0 +1,4940 @@ +{ + "name": "@azure/cosmos", + "version": "2.1.5", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "dev": true, + "requires": { + "samsam": "1.3.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.0.tgz", + "integrity": "sha512-beHeJM/RRAaLLsMJhsCvHK31rIqZuobfPLa/80yGH5hnD8PV1hyh9xJBJNFfNmO7yWqm+zomijHsXpI6iTQJfQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", + "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", + "dev": true + }, + "@types/node": { + "version": "8.10.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.22.tgz", + "integrity": "sha512-HCJ1dUJEQVFRekwBAlyv9pJ+2rzxq9uimSmsK2q7YDYMbXR3b4BXcO9rsN+36ZBwSWQ5BNh5o8xdZijDSonS5A==", + "dev": true + }, + "@types/priorityqueuejs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/priorityqueuejs/-/priorityqueuejs-1.0.1.tgz", + "integrity": "sha1-bqrDJHpMXO/JRILl2Hw3MLNfUFM=", + "dev": true + }, + "@types/semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-YD+lyrPhrsJdSOaxmA9K1lzsCoN0J29IsQGMKd67SbkPDXxJPdwdqpok1sytD19NEozUaFpjIsKOWnJDOYO/GA==", + "dev": true + }, + "@types/sinon": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz", + "integrity": "sha512-Tt7w/ylBS/OEAlSCwzB0Db1KbxnkycP/1UkQpbvKFYoUuRn4uYsC3xh5TRPrOjTy0i8TIkSz1JdNL4GPVdf3KQ==", + "dev": true + }, + "@types/tunnel": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.0.tgz", + "integrity": "sha512-FGDp0iBRiBdPjOgjJmn1NH0KDLN+Z8fRmo+9J7XGBhubq1DPrGrbmG4UTlGzrpbCpesMqD0sWkzi27EYkOMHyg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/underscore": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.8.8.tgz", + "integrity": "sha512-EquzRwzAAs04anQ8/6MYXFKvHoD+MIlF+gu87EDda7dN9zrKvQYHsc9VFAPB1xY4tUHQVvBMtjsHrvof2EE1Mg==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "dev": true + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", + "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async-each": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.2.tgz", + "integrity": "sha512-6xrbvN0MOBKSJDdonmSSz2OwFSgxRaVtBDes26mj9KIGtDo+g9xosFRSC+i1gQh2oAN/tQ62AI/pGZGQjVOiRg==", + "dev": true + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "binary-search-bounds": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz", + "integrity": "sha1-X/hhbW3SylOIvIWy1iZuK52lAtw=" + }, + "bluebird": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", + "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "safe-buffer": { + "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 + } + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "cacache": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz", + "integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==", + "dev": true, + "requires": { + "bluebird": "^3.5.3", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "camelcase": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.2.0.tgz", + "integrity": "sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true + }, + "chokidar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz", + "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz", + "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "^1.1.1" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "esprima": { + "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 + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "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, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "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 + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lolex": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.1.tgz", + "integrity": "sha512-Oo2Si3RMKV3+lV5MsSWplDQFoTClz/24S0MMHYcgGWWmFXr6TMlqcqk/l1GtH+d5wLBwNRiqGnwDRMirtFalJw==", + "dev": true + }, + "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, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "dev": true, + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "mem": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", + "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^1.0.0", + "p-is-promise": "^2.0.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "mocha-junit-reporter": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz", + "integrity": "sha1-LlFJ7UD8XS48px5C21qx/snG2Fw=", + "dev": true, + "requires": { + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "strip-ansi": "^4.0.0", + "xml": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "mocha-multi-reporters": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz", + "integrity": "sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI=", + "dev": true, + "requires": { + "debug": "^3.1.0", + "lodash": "^4.16.4" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "neo-async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", + "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "dev": true + }, + "nice-try": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz", + "integrity": "sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==", + "dev": true + }, + "nise": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + } + } + }, + "node-libs-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", + "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.0", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", + "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parse-asn1": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prettier": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.14.3.tgz", + "integrity": "sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg==", + "dev": true + }, + "priorityqueuejs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", + "integrity": "sha1-LuTyPCVgkT4IwHzlzN1t498sWvg=" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "safe-buffer": { + "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 + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", + "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "requirejs": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.5.tgz", + "integrity": "sha512-svnO+aNcR/an9Dpi44C7KSAy5fFGLtmPbaaCeQaklUz8BQhS64tWWIIlvEA5jrWICzlO/X9KSzSeXFnZdBu8nw==", + "dev": true + }, + "resolve": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", + "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semaphore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.0.5.tgz", + "integrity": "sha1-tJJXbmavGT25XWXiXsU/Xxl5jWA=" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "serialize-javascript": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.6.1.tgz", + "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sinon": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-5.1.1.tgz", + "integrity": "sha512-h/3uHscbt5pQNxkf7Y/Lb9/OM44YNCicHakcq73ncbrIS8lXg+ZGOZbtuU+/km4YnyiCYfQQEwANaReJz7KDfw==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^2.0.0", + "diff": "^3.5.0", + "lodash.get": "^4.4.2", + "lolex": "^2.4.2", + "nise": "^1.3.3", + "supports-color": "^5.4.0", + "type-detect": "^4.0.8" + }, + "dependencies": { + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==", + "dev": true + }, + "terser": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", + "integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.10" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "terser-webpack-plugin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz", + "integrity": "sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA==", + "dev": true, + "requires": { + "cacache": "^11.0.2", + "find-cache-dir": "^2.0.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "terser": "^3.16.1", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "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, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "ts-node": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.2.0.tgz", + "integrity": "sha512-ZNT+OEGfUNVMGkpIaDJJ44Zq3Yr0bkU/ugN1PHbU+/01Z7UV1fsELRiTx1KuQNvQ1A3pGh3y25iYF6jXgxV21A==", + "dev": true, + "requires": { + "arrify": "^1.0.0", + "buffer-from": "^1.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" + }, + "dependencies": { + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "tslint": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", + "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.27.2" + } + }, + "tslint-config-prettier": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.14.0.tgz", + "integrity": "sha512-SomD+aLvAwoihMtyCfkhhWKt9wcpSY2ZpgDV6OuxLYi8+7uOwE2g03aa+jJLSmY0Ys8s3ZLM5Iwfuwu3giCSQQ==", + "dev": true + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", + "integrity": "sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.4.tgz", + "integrity": "sha512-JZHJtA6ZL15+Q3Dqkbh8iCUmvxD3iJ7ujXS+fVkKnwIVAdHc5BJTDNM0aTrnr2luKulFjU7W+SRhDZvi66Ru7Q==", + "dev": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", + "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "v8-compile-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz", + "integrity": "sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==", + "dev": true + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "webpack": { + "version": "4.29.6", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.29.6.tgz", + "integrity": "sha512-MwBwpiE1BQpMDkbnUUaW6K8RFZjljJHArC6tWQJoFm0oQtfoSebtg4Y7/QHnJ/SddtjYLHaKGX64CFjG5rehJw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.0.5", + "acorn-dynamic-import": "^4.0.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chrome-trace-event": "^1.0.0", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.0", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "micromatch": "^3.1.8", + "mkdirp": "~0.5.0", + "neo-async": "^2.5.0", + "node-libs-browser": "^2.0.0", + "schema-utils": "^1.0.0", + "tapable": "^1.1.0", + "terser-webpack-plugin": "^1.1.0", + "watchpack": "^1.5.0", + "webpack-sources": "^1.3.0" + }, + "dependencies": { + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + } + } + }, + "webpack-cli": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.2.3.tgz", + "integrity": "sha512-Ik3SjV6uJtWIAN5jp5ZuBMWEAaP5E4V78XJ2nI+paFPh8v4HPSwo/myN0r29Xc/6ZKnd2IdrAlpSgNOu2CDQ6Q==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.1.0", + "findup-sync": "^2.0.0", + "global-modules": "^1.0.0", + "import-local": "^2.0.0", + "interpret": "^1.1.0", + "loader-utils": "^1.1.0", + "supports-color": "^5.5.0", + "v8-compile-cache": "^2.0.2", + "yargs": "^12.0.4" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "worker-farm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", + "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } +} diff --git a/sdk/cosmosdb/cosmos/package.json b/sdk/cosmosdb/cosmos/package.json new file mode 100644 index 000000000000..c4d6c533fc97 --- /dev/null +++ b/sdk/cosmosdb/cosmos/package.json @@ -0,0 +1,73 @@ +{ + "name": "@azure/cosmos", + "description": "Azure Cosmos DB Service Node.js SDK for SQL API", + "keywords": [ + "cosmosdb", + "cosmos db", + "documentdb", + "document database", + "azure", + "nosql", + "database", + "cloud" + ], + "version": "2.1.5", + "author": "Microsoft Corporation", + "main": "./lib/src/index.js", + "types": "./lib/src/index.d.ts", + "engine": { + "node": ">=6.0.0" + }, + "scripts": { + "clean": "rimraf lib", + "lint": "tslint --project tsconfig.json", + "format": "prettier --write --config .prettierrc.json \"src/**/*.ts\"", + "check-format": "prettier --list-different --config .prettierrc.json \"src/**/*.ts\"", + "compile": "echo Using TypeScript && tsc --version && tsc --pretty", + "compile-prod": "echo Using TypeScript && tsc --version && tsc -p tsconfig.prod.json --pretty", + "prepack": "npm install && npm run build", + "webpack": "webpack -d", + "webpack-prod": "webpack -p", + "build": "npm run clean && npm run check-format && npm run lint && npm run compile && npm run webpack", + "build-prod": "npm run clean && npm run check-format && npm run lint && npm run compile-prod && npm run webpack-prod", + "test": "mocha -r ./src/test/common/setup.ts ./lib/src/test/ --recursive --timeout 100000 -i -g .*ignore.js", + "test-ts": "mocha -r ts-node/register -r ./src/test/common/setup.ts ./src/test/**/*.spec.ts --recursive --timeout 100000 -i -g .*ignore.js", + "ci": "npm run build && npm run test && node ts-test.js" + }, + "devDependencies": { + "@types/mocha": "^5.2.5", + "@types/node": "^8.10.22", + "@types/priorityqueuejs": "^1.0.1", + "@types/semaphore": "^1.1.0", + "@types/sinon": "^4.3.3", + "@types/tunnel": "^0.0.0", + "@types/underscore": "^1.8.8", + "execa": "1.0.0", + "mocha": "^5.2.0", + "mocha-junit-reporter": "^1.15.0", + "mocha-multi-reporters": "^1.1.6", + "prettier": "1.14.3", + "requirejs": "^2.3.5", + "sinon": "^5.1.1", + "ts-node": "^6.2.0", + "tslint": "^5.11.0", + "tslint-config-prettier": "^1.14.0", + "typescript": "3.1.4", + "webpack": "^4.29.6", + "webpack-cli": "^3.2.3" + }, + "dependencies": { + "binary-search-bounds": "2.0.3", + "create-hmac": "^1.1.7", + "priorityqueuejs": "1.0.0", + "semaphore": "1.0.5", + "stream-http": "^2.8.3", + "tslib": "^1.9.3", + "tunnel": "0.0.5" + }, + "repository": { + "type": "git", + "url": "https://github.com/Azure/azure-cosmos-js" + }, + "license": "MIT" +} diff --git a/sdk/cosmosdb/cosmos/samples/ChangeFeed/README.md b/sdk/cosmosdb/cosmos/samples/ChangeFeed/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/cosmosdb/cosmos/samples/ChangeFeed/app.js b/sdk/cosmosdb/cosmos/samples/ChangeFeed/app.js new file mode 100644 index 000000000000..29a0cd02ee0f --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ChangeFeed/app.js @@ -0,0 +1,157 @@ +// @ts-check +"use strict"; + +const cosmos = require("../../lib/src"); +const CosmosClient = cosmos.CosmosClient; +const config = require("../Shared/config"); +const databaseId = config.names.database; +const containerId = config.names.container; + +const endpoint = config.connection.endpoint; +const masterKey = config.connection.authKey; + +// Establish a new instance of the CosmosClient to be used throughout this demo +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +// We'll use the same pk value for all these samples +const pk = "0"; + +function doesMatch(actual, expected) { + for (let i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) { + return "❌"; + } + } + return "✅"; +} + +function logResult(scenario, actual, expected) { + const status = doesMatch(actual, expected); + console.log(` ${status} ${scenario} - expected: [${expected.join(", ")}] - actual: [${actual.join(", ")}]`); +} + +async function run() { + const container = await init(); + + try { + console.log(` +✨✨✨ Change Feed Samples ✨✨✨ + + There are 4 scenarios for change feed: + 1. Start from a specific continuation + 2. Start from a specific point in time + 3. Start from the beginning + 4. Start from now + + All 4 scenarios will eventually catch up to each other if read for long enough + + In this sample, we expect the scenario to see the following items, by id: + 1. [3] + 2. [2, 3] + 3. [1, 2, 3] + 4. [] + + After we've read to this point, if we insert a new item id 4, we expect all of them to see it, since they will all be caught up. +`); + + console.log("📢 Phase 1: All scenarios see different results "); + + await container.items.create({ id: "1", pk }); + console.log(" 👉 Inserted id=1"); + + const now = new Date(); + console.log(" 👉 Saved timestamp for the specific point in time scenario"); + const { headers } = await container.items.create({ id: "2", pk }); + const lsn = headers["lsn"]; + console.log(` 👉 Inserted id=2 after timestamp with LSN of ${lsn}`); + + await container.items.create({ id: "3", pk }); + + console.log(` 👉 Inserted id=3`); + + const specificContinuationIterator = container.items.readChangeFeed(pk, { continuation: lsn }); + const specificPointInTimeIterator = container.items.readChangeFeed(pk, { startTime: now }); + const fromBeginningIterator = container.items.readChangeFeed(pk, { startFromBeginning: true }); + const fromNowIterator = container.items.readChangeFeed(pk, {}); + + const { result: specificContinuationResult } = await specificContinuationIterator.executeNext(); + + logResult("initial specific Continuation scenario", [3], specificContinuationResult.map(v => parseInt(v.id))); + + // First page is empty. It is catching up to a valid continuation. + const { result: shouldBeEmpty } = await specificPointInTimeIterator.executeNext(); + logResult( + "initial specific point in time scenario should be empty while it finds the right continuation", + [], + shouldBeEmpty.map(v => parseInt(v.id)) + ); + // Second page should have results + const { result: specificPointInTimeResults } = await specificPointInTimeIterator.executeNext(); + logResult( + "second specific point in time scenario should have caught up now", + [2, 3], + specificPointInTimeResults.map(v => parseInt(v.id)) + ); + + const { result: fromBeginningResults } = await fromBeginningIterator.executeNext(); + logResult("initial from beginning scenario", [1, 2, 3], fromBeginningResults.map(v => parseInt(v.id))); + + const { result: fromNowResultsShouldBeEmpty } = await fromNowIterator.executeNext(); + logResult("initial from now scenario should be empty", [], fromNowResultsShouldBeEmpty.map(v => parseInt(v.id))); + + // Now they should all be caught up to the point after id=3, so if we insert a id=4, they should all get it. + console.log("📢 Phase 2: All scenarios are caught up and should see the same results"); + + await container.items.create({ id: "4", pk }); + console.log(" 👉 Inserting id=4 - all scenarios should see this"); + + const { result: specificContinuationResult2 } = await specificContinuationIterator.executeNext(); + logResult( + "after insert, Specific Continuation scenario", + [4], + specificContinuationResult2.map(v => parseInt(v.id)) + ); + + const { result: specificPointInTimeResults2 } = await specificPointInTimeIterator.executeNext(); + logResult( + "after insert, specific point in time scenario", + [4], + specificPointInTimeResults2.map(v => parseInt(v.id)) + ); + + const { result: fromBeginningResults2 } = await fromBeginningIterator.executeNext(); + logResult("after insert, from beginning scenario", [4], fromBeginningResults2.map(v => parseInt(v.id))); + + const { result: fromNowResults2 } = await fromNowIterator.executeNext(); + logResult("after insert, from now scenario", [4], fromNowResults2.map(v => parseInt(v.id))); + } catch (err) { + handleError(err); + } finally { + await finish(container); + } +} + +async function init() { + const { database } = await client.databases.createIfNotExists({ id: databaseId }); + const { container } = await database.containers.createIfNotExists({ + id: containerId, + partitionKey: { kind: "Hash", paths: ["/pk"] } + }); + return container; +} + +async function handleError(error) { + console.log(`\nAn error with code '${error.code}' has occurred:`); + console.log(`\t${error}`); +} + +async function finish(container) { + try { + await container.database.delete(); + console.log("\nEnd of demo."); + } catch (err) { + console.log(`Database[${databaseId}] might not have deleted properly. You might need to delete it manually.`); + } +} + +run().catch(handleError); diff --git a/sdk/cosmosdb/cosmos/samples/ChangeFeed/package.json b/sdk/cosmosdb/cosmos/samples/ChangeFeed/package.json new file mode 100644 index 000000000000..c5e0e37a610d --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ChangeFeed/package.json @@ -0,0 +1,11 @@ +{ + "name": "cosmos-change-feed", + "version": "0.0.0", + "private": true, + "description": "A sample showing usage of the change feed in Cosmos DB", + "main": "app.js", + "dependencies": {}, + "scripts": { + "start": "node app.js" + } +} diff --git a/sdk/cosmosdb/cosmos/samples/ContainerManagement/README.md b/sdk/cosmosdb/cosmos/samples/ContainerManagement/README.md new file mode 100644 index 000000000000..94fb94376257 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ContainerManagement/README.md @@ -0,0 +1,7 @@ +Samples for performing basic CRUD operations on an Azure Cosmos DB collection + +- createCollection - given an id, create a new Collection with the default indexingPolicy +- listCollections - example of using the QueryIterator to get a list of Collections in a Database +- getOfferType - get the Offer.OfferType for a collection. This is what determines if a Collection is S1, S2, or S3 +- modifyOfferType - change the Offer.OfferType for a collection. This is how you scale a Collection up or down +- deleteCollection - given just the collection id, delete the collection \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/ContainerManagement/app.js b/sdk/cosmosdb/cosmos/samples/ContainerManagement/app.js new file mode 100644 index 000000000000..126f42530a62 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ContainerManagement/app.js @@ -0,0 +1,80 @@ +// @ts-check +"use strict"; +console.log(); +console.log("Azure Cosmos DB Node.js Samples"); +console.log("================================"); +console.log(); +console.log("container MANAGEMENT"); +console.log("====================="); +console.log(); + +const cosmos = require("../../lib/src"); +const CosmosClient = cosmos.CosmosClient; +const config = require("../Shared/config"); +const databaseId = config.names.database; +const containerId = config.names.container; + +const endpoint = config.connection.endpoint; +const masterKey = config.connection.authKey; + +// Establish a new instance of the CosmosClient to be used throughout this demo +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +//--------------------------------------------------------------------------------- +// This demo performs a few steps +// 1. create container - given an id, create a new container with thedefault indexingPolicy +// 2. read all containers - example of using the QueryIterator to get a list of containers in a Database +// 3. read container - Read a container by its _self +// 4. delete container - given just the container id, delete the container +//--------------------------------------------------------------------------------- + +//ensuring a database exists for us to work with +async function run() { + const database = await init(databaseId); + + //1. + console.log(`1. create container with id '${containerId}'`); + await database.containers.createIfNotExists({ id: containerId }); + + //2. + console.log("\n2. read all containers in database"); + const iterator = database.containers.readAll(); + const { result: containersList } = await iterator.toArray(); + console.log(" --- Priting via iterator.toArray"); + console.log(containersList); + + //3. + console.log("\n3. read container definition"); + const container = database.container(containerId); + const { body: containerDef } = await container.read(); + + console.log(`container with url '${container.url}' was found its id is '${containerDef.id}'`); + + //4. + console.log(`\n4. deletecontainer '${containerId}'`); + await container.delete(); + await finish(database); +} + +async function init(databaseId) { + const { database } = await client.databases.createIfNotExists({ id: databaseId }); + return database; +} + +async function handleError(error) { + console.log(`\nAn error with code '${error.code}' has occurred:`); + console.log("\t" + error); + + await finish(); +} + +async function finish(database) { + try { + await database.delete(); + console.log("\nEnd of demo."); + } catch (err) { + console.log(`Database[${databaseId}] might not have deleted properly. You might need to delete it manually.`); + } +} + +run().catch(handleError); diff --git a/sdk/cosmosdb/cosmos/samples/ContainerManagement/package.json b/sdk/cosmosdb/cosmos/samples/ContainerManagement/package.json new file mode 100644 index 000000000000..0f8310e5113e --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ContainerManagement/package.json @@ -0,0 +1,11 @@ +{ + "name": "cosmos-container-management", + "version": "0.0.0", + "private": true, + "description": "A sample showing managing containers in Cosmos DB", + "main": "app.js", + "dependencies": {}, + "scripts": { + "start": "node app.js" + } +} diff --git a/sdk/cosmosdb/cosmos/samples/DatabaseManagement/README.md b/sdk/cosmosdb/cosmos/samples/DatabaseManagement/README.md new file mode 100644 index 000000000000..7987bfbd1125 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/DatabaseManagement/README.md @@ -0,0 +1,9 @@ +Samples for performing basic CRUD operations on an Azure Cosmos DB database + +- createCollection - given an id, create a new Collectionwith thedefault indexingPolicy +- listCollections - example of using the QueryIterator to get a list of Collections in a Database +- readCollection - Read a collection by its _self +- readCollection - Read a collection by its id (using new ID Based Routing) +- getOfferType - get the Offer.OfferType for a collection. This is what determines if aCollection is S1, S2, or S3 +- modifyOfferType - change the Offer.OfferType for a collection. This is how you scale a Collection up or down +- deleteCollection - given just the collection id, delete the collection \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/DatabaseManagement/app.js b/sdk/cosmosdb/cosmos/samples/DatabaseManagement/app.js new file mode 100644 index 000000000000..3812a3c3f0db --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/DatabaseManagement/app.js @@ -0,0 +1,77 @@ +// @ts-check +"use strict"; +console.log(); +console.log("Azure Cosmos DB Node.js Samples"); +console.log("================================"); +console.log(); +console.log("DATABASE MANAGEMENT"); +console.log("==================="); +console.log(); + +const assert = require("assert"); +const cosmos = require("../../lib/src"); +const CosmosClient = cosmos.CosmosClient; +const config = require("../Shared/config"); +const databaseId = config.names.database; + +const endpoint = config.connection.endpoint; +const masterKey = config.connection.authKey; + +// Establish a new instance of the CosmosClient to be used throughout this demo +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +//--------------------------------------------------------------------------------------------------- +// This demo performs the following CRUD operations on a Database +// +// 1. create Database - If the database was not found, try create it +// 2. read all Databases - Once the database was created, list all the databases on the account +// 3. read Database - Read a database by its id +// 4. delete Database - Delete a database given its id +// +//--------------------------------------------------------------------------------------------------- + +async function run() { + // 1. + console.log(`\n1. Create database, if it doesn't already exist '${databaseId}'`); + await client.databases.createIfNotExists({ id: databaseId }); + console.log("Database with id " + databaseId + " created."); + + // 2. + console.log("\n2. Read all databases"); + const { result: dbDefList } = await client.databases.readAll().toArray(); + console.log(dbDefList); + + // 3. + console.log(`\n3. readDatabase - with id '${databaseId}'`); + const { body: dbDef } = await client.database(databaseId).read(); + // This uses Object deconstruction to just grab the body of the response, + // but you can also grab the whole response object to use + const databaseResponse = await client.database(databaseId).read(); + const alsoDbDef = databaseResponse.body; + assert.equal(dbDef.id, alsoDbDef.id); // The bodies will also almost be equal, _ts will defer based on the read time + // This applies for all response types, not just DatabaseResponse. + + console.log(`Database with id of ${dbDef.id}' was found`); + + // 4. + console.log(`\n4. delete database with id '${databaseId}'`); + await client.database(databaseId).delete(); + + await finish(); +} + +function handleError(error) { + console.log(); + console.log(`An error with code '${error.code}' has occurred:`); + console.log(`\t${error.body || error}`); + console.log(); + + finish(); +} + +function finish() { + console.log(); + console.log("End of demo."); +} + +run().catch(handleError); diff --git a/sdk/cosmosdb/cosmos/samples/DatabaseManagement/package.json b/sdk/cosmosdb/cosmos/samples/DatabaseManagement/package.json new file mode 100644 index 000000000000..03f81d6e9c5b --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/DatabaseManagement/package.json @@ -0,0 +1,11 @@ +{ + "name": "cosmos-database-management", + "version": "0.0.0", + "private": true, + "description": "A sample showing managing databases in Cosmos DB", + "main": "app.js", + "dependencies": {}, + "scripts": { + "start": "node app.js" + } +} diff --git a/sdk/cosmosdb/cosmos/samples/IndexManagement/README.md b/sdk/cosmosdb/cosmos/samples/IndexManagement/README.md new file mode 100644 index 000000000000..0547a6cdfe6a --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/IndexManagement/README.md @@ -0,0 +1,15 @@ +While Azure Cosmos DB automatically indexes all paths of all documents in a consistent manner, you have the ability to tweak and customize this +behavior should you need (or want) to. + +Samples for working with Azure Cosmos DB IndexPolicy on a Collection + +1. explictlyExcludeFromIndex - how to manually exclude a document from being indexed +2. useManualIndexing - switch auto indexing off, and then manually add individual docs +3. useLazyIndexing - create a collection with indexing mode set to Lazy instead of consistent +4. forceScanOnHashIndexPath - use a directive to allow a scan on a string path during a range operation +5. useRangeIndexOnStrings - create a range index on string path +6. excludePathsFromIndex - create a custom indexPolicy that excludes specific path in document +7. performIndexTransforms - create a collection with default indexPolicy, then update this online +8. waitForIndexTransforms - waits for index transform to complete by repeatedly doing a readCollection checking and checking headers + + diff --git a/sdk/cosmosdb/cosmos/samples/IndexManagement/app.js b/sdk/cosmosdb/cosmos/samples/IndexManagement/app.js new file mode 100644 index 000000000000..d9048ec4c271 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/IndexManagement/app.js @@ -0,0 +1,555 @@ +// @ts-check + +console.log(); +console.log("Azure CosmosDB Node.js Samples"); +console.log("================================"); +console.log(); +console.log("INDEX MANAGEMENT"); +console.log("================"); +console.log(); + +const cosmos = require("../../lib/src"); +const CosmosClient = cosmos.CosmosClient; +const config = require("../Shared/config"); +const fs = require("fs"); +const databaseId = config.names.database; +const containerId = config.names.container; + +const endpoint = config.connection.endpoint; +const masterKey = config.connection.authKey; + +// Establish a new instance of the CosmosClient to be used throughout this demo +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +//IMPORTANT: +//this sample creates and delete containers at least 7 times. +//each time you execute containers.create() you are charged for 1hr (our smallest billing unit) +//even if that container is only alive for a few seconds. +//so please take note of this before running this sample + +//TODO: Now that index transforms exist, refactor to create only 1 container and just reuse each time + +//NOTE: +//when using the new IDBased Routing URIs, instead of the _self, as we 're doing in this sample +//ensure that the URI does not end with a trailing '/' character +//so dbs/databaseId instead of dbs/databaseId/ +//also, ensure there is no leading space + +//----------------------------------------------------------------------------------------------------------- +// This demo performs a few steps +// 1. explictlyExcludeFromIndex - how to manually exclude an item from being indexed +// 2. useManualIndexing - switch auto indexing off, and then manually add individual items +// 3. useLazyIndexing - create a container with indexing mode set to Lazy instead of consistent +// 4. forceScanOnHashIndexPath - use a directive to allow a scan on a string path during a range operation +// 5. useRangeIndexOnStrings - create a range index on string path +// 6. excludePathsFromIndex - create a custom indexPolicy that excludes specific path in an item +// 7. performIndexTransforms - create a container with default indexPolicy, then update this online +//------------------------------------------------------------------------------------------------------------ +async function run() { + // Gets a database for us to work with + const { database } = await init(); + //1. + console.log("\n1."); + console.log("explictlyExcludeFromIndex - manually exclude an item from being indexed"); + await explictlyExcludeFromIndex(database); + //2. + console.log("\n2."); + console.log("useManualIndexing - switch auto indexing off, and manually index item"); + await useManualIndexing(database); + //3. + console.log("\n3."); + console.log("useLazyIndexing - create container lazy index"); + await useLazyIndexing(database); + //4. + console.log("\n4."); + console.log("forceScanOnHashIndexPath - use index directive to allow range scan on path without range index"); + await forceScanOnHashIndexPath(database); + //5. + console.log("\n5."); + console.log("useRangeIndexOnStrings - create a range index on string path"); + await useRangeIndexOnStrings(database); + //6. + console.log("\n6."); + console.log("excludePathsFromIndex - create a range index on string path"); + await excludePathsFromIndex(database); + //7. + console.log("\n7."); + console.log("performIndexTransforms - update an index policy online"); + await performIndexTransforms(database); + await finish(); +} + +async function init(callback) { + return client.databases.createIfNotExists({ id: databaseId }); +} + +/** + * + * @param {cosmos.Database} database + */ +async function explictlyExcludeFromIndex(database) { + console.log("create container with default index policy"); + + //we're using the default indexing policy because by default indexingMode == consistent & automatic == true + //which means that by default all items added to a container are indexed as the item is written + const containerId = "ExplictExcludeDemo"; + const { body: containerDef, container } = await database.containers.create({ id: containerId }); + const itemSpec = { id: "item1", foo: "bar" }; + + console.log("Create item, but exclude from index"); + + //items.create() takes RequestOptions as 3rd parameter. + //One of these options is indexingDirectives which can be include, or exclude + //we're using exclude this time to manually exclude this item from being indexed + const { body: itemDef, item } = await container.items.create(itemSpec, { indexingDirective: "exclude" }); + console.log(`Item with id '${itemDef.id}' created`); + + const querySpec = { + query: "SELECT * FROM root r WHERE r.foo=@foo", + parameters: [ + { + name: "@foo", + value: "bar" + } + ] + }; + + console.log("Querying all items for the given item should not find any results"); + const { result: results } = await container.items.query(querySpec).toArray(); + if (results.length !== 0) { + throw new Error("there were not meant to be results"); + } + console.log("No results found"); + + console.log("item.read() should still find the item"); + + const { body: readItemDef } = await item.read(); + console.log(`item.read() found item and its _self is '${readItemDef._self}'`); + + await container.delete(); + console.log(`Container '${containerId}' deleted`); +} + +/** + * + * @param {cosmos.Database} database + */ +async function useManualIndexing(database) { + console.log("create container with indexingPolicy.automatic : false"); + + const containerId = "ManualIndexDemo"; + const indexingPolicySpec = { automatic: false }; + + const { container } = await database.containers.create({ + id: containerId, + indexingPolicy: indexingPolicySpec + }); + + // items.create() takes RequestOptions as 2nd parameter. + // One of these options is indexingDirectives which can be include, or exclude + // we're using include this time to manually index this particular item + console.log("Create item, and explicitly include in index"); + const itemSpec = { id: "item1", foo: "bar" }; + const { body: itemDef } = await container.items.create(itemSpec, { indexingDirective: "include" }); + console.log("Item with id '" + itemDef.id + "' created"); + + const querySpec = { + query: "SELECT * FROM root r WHERE r.foo=@foo", + parameters: [ + { + name: "@foo", + value: "bar" + } + ] + }; + + console.log("Querying all items for a given item should find a result as it was indexed"); + const { result: results } = await container.items.query(querySpec).toArray(); + if (results.length === 0) { + throw new Error("There were meant to be results"); + } else { + const itemDef = results[0]; + console.log("Item with id '" + itemDef.id + "' found"); + + await container.delete(); + console.log("Container '" + containerId + "' deleted"); + } +} + +/** + * + * @param {cosmos.Database} database + */ +async function useLazyIndexing(database) { + // Azure Cosmos DB offers synchronous (consistent) and asynchronous (lazy) index updates. + // By default, the index is updated synchronously on each insert, replace or delete of a item to the container. + // There are times when you might want to configure certain containers to update their index asynchronously. + // Lazy indexing boosts the write performance and lowers RU charge of each insert + // and is ideal for bulk ingestion scenarios for primarily read-heavy containers + // It is important to note that you might get inconsistent reads whilst the writes are in progress, + // However once the write volume tapers off and the index catches up, then the reads continue as normal + + // It is difficult to demonstrate this is a code sample as you only really notice this under sustained + // heavy-write workloads. So this code sample shows just how to create the custom index polixy needed + + console.log("create container with indexingPolicy.indexMode : lazy"); + + // allowed values for IndexingMode are consistent (default), lazy and none + const containerId = "LazyIndexDemo"; + /** @type cosmos.DocumentBase.IndexingPolicy */ + const indexingPolicySpec = { indexingMode: cosmos.DocumentBase.IndexingMode.lazy }; + + // You can also set the indexing policy Mode via string + indexingPolicySpec.indexingMode = "lazy"; + + const { body: containerDef, container } = await database.containers.create({ + id: containerId, + indexingPolicy: indexingPolicySpec + }); + console.log("Container '" + containerDef.id + "' created with index policy: "); + console.log(containerDef.indexingPolicy); + + await container.delete(); + console.log("Container '" + containerId + "' deleted"); +} +/** + * + * @param {cosmos.Database} database + */ +async function forceScanOnHashIndexPath(database) { + // Azure Cosmos DB index knows about 3 datatypes - numbers, strings and geojson + // By default, the index on a container does not put range indexes on to string paths + // Therefore, if you try and do a range operation on a string path with a default index policy, you will get an error + // You can override this by using an request option, that is what this demonstrates + // NOTE - it is not recommended to do this often due to the high charge associated with a full container scan + // if you find yourself doing this often on a particular path, create a range index for strings on that path + + console.log("create container with default index policy"); + const containerId = "ForceScanDemo"; + + const { body: containerDef, container } = await database.containers.create({ id: containerId }); + console.log("Container '" + containerDef.id + "' created with default index policy (i.e. no range on strings)"); + + //create an item + console.log("Creating item"); + await container.items.create({ id: "item1", stringField: "a string value" }); + console.log("Item created"); + + //try a range query on the item, expect an error + const querySpec = { + query: "SELECT * FROM root r WHERE r.stringField > @value", + parameters: [ + { + name: "@value", + value: "a" + } + ] + }; + + console.log("Querying for item where stringField > 'a', should fail"); + try { + await container.items.query(querySpec).toArray(); + } catch (err) { + console.log("Query failed with " + err.code); + } + //try same range query again, this time specifying the directive to do a scan, + //be wary of high RU cost that you could get for even a single item! + //we won't particularly see a high charge this time because there is only 1 item in the container + //so a scan on 1 item isn't costly. a few thousand items will be very different + console.log("Repeating query for item where stringField > 'a', this time with enableScanInQuery: true"); + + //notice how we're switching to queryIterator.executeNext instead of calling .toArray() as before + //reason being, toArray will issue multiple requests to the server until it has fetched all results + //here we can control this using executeNext. + //now we can get the headers for each request which includes the charge, continuation tokens etc. + + const queryIterator = container.items.query(querySpec, { enableScanInQuery: true }); + const { result: items, headers } = await queryIterator.executeNext(); + const charge = headers["x-ms-request-charge"]; + const itemDef = items[0]; + + console.log("Item '" + itemDef.id + "' found, request charge: " + charge); + + await container.delete(); + console.log("Container '" + containerId + "' deleted"); +} + +/** + * + * @param {cosmos.Database} database + */ +async function useRangeIndexOnStrings(database) { + // Azure Cosmos DB index knows about 3 datatypes - numbers, strings and geojson + // By default, the index on a container does not put range indexes on to string paths + // In this demo we are going to create a custom index policy which enables range index on a string path + + console.log("create container with range index on string paths"); + const containerId = "RangeIndexDemo"; + /** + * @type cosmos.DocumentBase.IndexingPolicy + */ + const indexPolicySpec = { + includedPaths: [ + { + path: "/*", + indexes: [ + { + kind: cosmos.DocumentBase.IndexKind.Range, + dataType: cosmos.DocumentBase.DataType.String + }, + { + kind: cosmos.DocumentBase.IndexKind.Range, + dataType: cosmos.DocumentBase.DataType.Number + } + ] + } + ] + }; + + const { body: containerDef, container } = await database.containers.create({ + id: containerId, + indexingPolicy: indexPolicySpec + }); + console.log("Container '" + containerDef.id + "' created with custom index policy"); + + //create an item + console.log("Creating item"); + await container.items.create({ id: "item1", stringField: "a string value" }); + console.log("Item created"); + + //try a range query on the item, expect an error + const querySpec = { + query: "SELECT * FROM root r WHERE r.stringField > @value", + parameters: [ + { + name: "@value", + value: "a" + } + ] + }; + + console.log("Querying for item where stringField > 'a', should return results"); + + //notice how we're switching to queryIterator.executeNext instead of calling .toArray() as before + //reason being, toArray will issue multiple requests to the server until it has fetched all results + //here we can control this using executeNext. + //now we can get the headers for each request which includes the charge, continuation tokens etc. + const queryIterator = container.items.query(querySpec, { enableScanInQuery: true }); + const { result: items, headers } = await queryIterator.executeNext(); + const charge = headers["x-ms-request-charge"]; + const itemDef = items[0]; + + console.log("Item '" + itemDef.id + "' found, request charge: " + charge); + + await container.delete(); + console.log("Container '" + containerId + "' deleted"); +} + +/** + * + * @param {cosmos.Database} database + */ +async function excludePathsFromIndex(database) { + console.log("create container with an excluded path"); + const containerId = "ExcludePathDemo"; + const indexPolicySpec = { + //the special "/" must always be included somewhere. in this case we're including root + //and then excluding specific paths + includedPaths: [ + { + path: "/", + indexes: [ + { + kind: cosmos.DocumentBase.IndexKind.Hash, + dataType: cosmos.DocumentBase.DataType.Number, + precision: 2 + } + ] + } + ], + excludedPaths: [ + { + path: "/metaData/*" + } + ] + }; + + const { body: containerDef, container } = await database.containers.create({ + id: containerId, + indexingPolicy: indexPolicySpec + }); + console.log("Container '" + containerDef.id + "' created with excludedPaths"); + + const itemId = "item1"; + + const itemSpec = { + id: itemId, + metaData: "meta", + subDoc: { + searchable: "searchable", + subSubDoc: { someProperty: "value" } + } + }; + + //create an item + console.log("Creating item"); + const { item } = await container.items.create(itemSpec); + console.log("Item created"); + + //try a query on an excluded property, expect no results + const querySpec = { + query: "SELECT * FROM root r WHERE r.metaData = @value", + parameters: [ + { + name: "@value", + value: "meta" + } + ] + }; + + try { + //expecting an exception on this query due to the fact that it includes paths that + //have been excluded. If you want to force a scan, then enableScanInQuery like we did in forceScanOnHashIndexPath() + console.log("Querying for item where metaData = 'meta', should throw an exception"); + await container.items.query(querySpec).toArray(); + throw new Error("Should've produced an error"); + } catch (err) { + if (err.code !== undefined) { + console.log("Threw, as expected"); + } else { + throw err; + } + } //show that you can still read the item by its id + console.log("Can still item.read() using '" + item.id + "'"); + const { body: itemDef } = await item.read(); + console.log("Item '" + item.id + "' read and it's _self is '" + itemDef._self + "'"); + + await container.delete(); + console.log("Container '" + containerId + "' deleted"); +} + +/** + * + * @param {cosmos.Database} database + */ +async function performIndexTransforms(database) { + //create container with default index policy + console.log("Creating container with default index policy (i.e. no range on strings)"); + const containterId = "IndexTransformsDemo"; + + const { body: containerDef, container } = await database.containers.create({ id: containterId }); + console.log("Container '" + containerDef.id + "' created"); + + //create item + const itemSpec = { + id: "item1", + stringField: "a string" + }; + + console.log("Creating item"); + const { body: itemDef, item } = await container.items.create(itemSpec); + console.log("Item with id '" + itemDef.id + "' created"); + + //define a new indexPolicy which includes Range on all string paths (and Hash on all numbers) + const indexPolicySpec = { + includedPaths: [ + { + path: "/*", + indexes: [ + { + kind: "Range", + dataType: "String" + }, + { + kind: "Range", + dataType: "Number" + } + ] + } + ] + }; + + const containerSpec = { id: containterId }; + containerSpec.indexingPolicy = indexPolicySpec; + + //container.replace() to update the indexPolicy + await container.replace(containerSpec); + console.log("Waiting for index transform to be completed"); + + //Index transform is an async operation that is performed on a Container + //You can contiue to use the container while this is happening, but depending + //on the transform and your queries you may get inconsistent results as the index is updated + + //Here, we'll just wait for index transform to complete. + //this will be almost instant because we only have one item + //but this can take some time on larger containers + await waitForIndexTransformToComplete(container); + console.log("Index transform completed"); + + const querySpec = { + query: "SELECT * FROM root r WHERE r.stringField > @value", + parameters: [ + { + name: "@value", + value: "a" + } + ] + }; + + // Querying all items doing a range operation on a string (this would've failed without the transform) + const { result: results } = await container.items.query(querySpec).toArray(); + if (results.length == 0) { + throw new Error("Should've found an item"); + } else { + const queryDoc = results[0]; + console.log("Item with id '" + queryDoc.id + "' found"); + } +} + +async function sleep(timeMS) { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + }, timeMS); + }); +} + +/** + * To figure out the progress of and index transform, + do a container read and check the header property of the response. + The headers container includes a header that indicates progress between 0 and 100 + * @param {cosmos.Container} container + */ +async function waitForIndexTransformToComplete(container) { + // To figure out the progress of and index transform, + // do a container.read() and check the 3rd parameter of the callback + // The headers container includes a header that indicates progress between 0 and 100 + let progress = 0; + let count = 0; + + while (progress >= 0 && progress < 100) { + console.log("Reading container"); + const { headers } = await container.read(); + + progress = headers["x-ms-documentdb-collection-index-transformation-progress"]; + console.log("Progress is currently " + progress); + + console.log("Waiting for 100ms"); + await sleep(100); + } + console.log("Done waiting, progress == 100"); +} + +async function handleError(error) { + console.log(`\nAn error with code '${error.code}' has occurred:`); + console.log("\t" + error.body || error); + + await finish(); +} + +async function finish() { + await client.database(databaseId).delete(); + console.log("\nEnd of demo."); +} + +run().catch(handleError); diff --git a/sdk/cosmosdb/cosmos/samples/IndexManagement/package.json b/sdk/cosmosdb/cosmos/samples/IndexManagement/package.json new file mode 100644 index 000000000000..dd025237dc8e --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/IndexManagement/package.json @@ -0,0 +1,10 @@ +{ + "name": "cosmosdb-index-management", + "version": "0.0.0", + "private": true, + "description": "A Sample to explain the many ways to set indexes on Azure Cosmos DB", + "scripts": { + "start": "node app.js" + }, + "dependencies": {} +} diff --git a/sdk/cosmosdb/cosmos/samples/ItemManagement/README.md b/sdk/cosmosdb/cosmos/samples/ItemManagement/README.md new file mode 100644 index 000000000000..78990bb15034 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ItemManagement/README.md @@ -0,0 +1,8 @@ +Samples for performing basic CRUD operations on Azure Cosmos DB documents + +- createDocuments - Insert some documents in to collection +- listDocuments - Read the document feed for a collection +- readDocument - Read a single document by its id +- queryDocuments - Query for documents by some property +- replaceDocument - Update some properties and replace the document +- deleteDocument - Given a document id, delete it diff --git a/sdk/cosmosdb/cosmos/samples/ItemManagement/app.js b/sdk/cosmosdb/cosmos/samples/ItemManagement/app.js new file mode 100644 index 000000000000..cded1b87f53d --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ItemManagement/app.js @@ -0,0 +1,198 @@ +// @ts-check + +console.log(); +console.log("Azure Cosmos DB Node.js Samples"); +console.log("================================"); +console.log(); +console.log("ITEM MANAGEMENT"); +console.log("==================="); +console.log(); + +const cosmos = require("../../lib/src"); +const CosmosClient = cosmos.CosmosClient; +const config = require("../Shared/config"); +const fs = require("fs"); +const databaseId = config.names.database; +const containerId = config.names.container; + +const endpoint = config.connection.endpoint; +const masterKey = config.connection.authKey; + +const getItemDefinitions = function() { + const data = fs.readFileSync("../Shared/Data/Families.json", "utf8"); + return JSON.parse(data).Families; +}; + +// Establish a new instance of the CosmosClient to be used throughout this demo +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +//------------------------------------------------------------------------------------------------------- +// This demo performs a few steps +// 1. create items - Insert some items in to container +// 2. list items - Read the item feed for a container +// 3. read item +// 3.1 - Read a single item by its id +// 3.2 - Use ETag and AccessCondition to only return a item if ETag does not match +// 4. query items - Query for items by some property +// 5. replace item +// 5.1 - Update some properties and replace the item +// 5.2 - Use ETag and AccessCondition to only replace item if it has not changed +// 6. upsert item - Update a item if it exists, else create new item +// 7. delete item - Given a item id, delete it +//------------------------------------------------------------------------------------------------------- + +async function run() { + //ensuring a database & container exists for us to work with + const { container, database } = await init(); + + //1. + console.log(`\n1. insert items in to database '${databaseId}' and container '${containerId}'`); + const promises = getItemDefinitions().map(itemDef => container.items.create(itemDef)); + const items = await Promise.all(promises); + console.log(`${items.length} items created`); + + //2. + console.log(`\n2. list items in container '${container.id}'`); + const { result: itemDefList } = await container.items.readAll().toArray(); + + itemDefList.forEach(({ id }) => console.log(id)); + + //3.1 + const item = container.item(itemDefList[0].id); + console.log(`\n3.1 read item '${item.id}'`); + const { body: readDoc } = await item.read(); + console.log(`item with id '${item.id}' found`); + + //3.2 + console.log("\n3.2 read item with AccessCondition and no change to _etag"); + const { body: item2, headers } = await item.read({ + accessCondition: { type: "IfNoneMatch", condition: readDoc._etag } + }); + if (!item2 && headers["content-length"] == 0) { + console.log( + "As expected, no item returned. This is because the etag sent matched the etag on the server. i.e. you have the latest version of the item already" + ); + } + + //if we someone else updates this item, its etag on the server would change. + //repeating the above read with the old etag would then get a item in the response + readDoc.foo = "bar"; + await item.replace(readDoc); + const { body: item3, headers: headers3 } = await item.read({ + accessCondition: { type: "IfNoneMatch", condition: readDoc._etag } + }); + if (!item3 && headers3["content-length"] === 0) { + throw "Expected item this time. Something is wrong!"; + } else { + console.log("This time the read request returned the item because the etag values did not match"); + } + + //4. + const querySpec = { + query: "SELECT * FROM Families f WHERE f.lastName = @lastName", + parameters: [ + { + name: "@lastName", + value: "Andersen" + } + ] + }; + + console.log(`\n4. query items in container '${container.id}'`); + const { result: results } = await container.items.query(querySpec).toArray(); + + if (results.length == 0) { + throw "No items found matching"; + } else if (results.length > 1) { + throw "More than 1 item found matching"; + } + + const person = results[0]; + console.log(`The '${person.id}' family has lastName '${person.lastName}'`); + console.log(`The '${person.id}' family has ${person.children.length} children '`); + + //add a new child to this family, and change the family's lastName + const childDef = { + firstName: "Newborn", + gender: "unknown", + fingers: 10, + toes: 10 + }; + + person.children.push(childDef); + person.lastName = "Updated Family"; + + //5.1 + console.log(`\n5.1 replace item with id '${item.id}'`); + const { body: updatedPerson } = await item.replace(person); + + console.log(`The '${person.id}' family has lastName '${updatedPerson.lastName}'`); + console.log(`The '${person.id}' family has ${updatedPerson.children.length} children '`); + + // 5.2 + console.log("\n5.2 trying to replace item when item has changed in the database"); + // The replace item above will work even if there's a new version of item on the server from what you originally read + // If you want to prevent this from happening you can opt-in to a conditional update + // Using accessCondition and etag you can specify that the replace only occurs if the etag you are sending matches the etag on the server + // i.e. Only replace if the item hasn't changed + + // let's go update item + person.foo = "bar"; + await item.replace(person); + + // now let's try another update to item with accessCondition and etag set + person.foo = "should never get set"; + try { + await item.replace(person, { accessCondition: { type: "IfMatch", condition: person._etag } }); + throw new Error("This should have failed!"); + } catch (err) { + if (err.code == 412) { + console.log("As expected, the replace item failed with a pre-condition failure"); + } else { + throw err; + } + } + + //6. + const upsertSource = itemDefList[1]; + console.log(`6. Upserting person ${upsertSource.id} with _rid ${upsertSource._rid}...`); + + // a non-identity change will cause an update on upsert + upsertSource.foo = "baz"; + const { body: upsertedPerson1 } = await container.items.upsert(upsertSource); + console.log(`Upserted ${upsertedPerson1.id} to _rid ${upsertedPerson1._rid}.`); + + // an identity change will cause an insert on upsert + upsertSource.id = "HazzardFamily"; + const { body: upsertedPerson2 } = await container.items.upsert(upsertSource); + console.log(`Upserted ${upsertedPerson2.id} to _rid ${upsertedPerson2._rid}.`); + + if (upsertedPerson1._rid === upsertedPerson2._rid) + throw new Error("These two upserted records should have different resource IDs."); + + //7. + console.log("\n6. delete item '" + item.id + "'"); + await item.delete(); + + await finish(); +} + +async function init() { + const { database } = await client.databases.createIfNotExists({ id: databaseId }); + const { container } = await database.containers.createIfNotExists({ id: containerId }); + return { database, container }; +} + +async function handleError(error) { + console.log(`\nAn error with code '${error.code}' has occurred:`); + console.log("\t" + error.body || error); + + await finish(); +} + +async function finish() { + await client.database(databaseId).delete(); + console.log("\nEnd of demo."); +} + +run().catch(handleError); diff --git a/sdk/cosmosdb/cosmos/samples/ItemManagement/package.json b/sdk/cosmosdb/cosmos/samples/ItemManagement/package.json new file mode 100644 index 000000000000..0ec8f505a3df --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ItemManagement/package.json @@ -0,0 +1,10 @@ +{ + "name": "cosmos-item-management", + "version": "0.0.0", + "private": true, + "description": "Sample showing how to do item management in Cosmos", + "dependencies": {}, + "scripts": { + "start": "node app.js" + } +} diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/ConflictWorker.ts b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/ConflictWorker.ts new file mode 100644 index 000000000000..f3ba3503dbd6 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/ConflictWorker.ts @@ -0,0 +1,712 @@ +// tslint:disable:no-console + +import { v4 as guid } from "uuid"; +import { Container, CosmosClient, Item, ItemDefinition, ItemResponse, Items } from "../../lib"; +import { ItemBody } from "../../lib/client/Item/ItemBody"; +import { Constants, StatusCodes } from "../../lib/common"; +import logger from "./logger"; +import lwwSprocDef from "./lwwSprocDef"; + +export class ConflictWorker { + private readonly clients: Map = new Map(); + constructor( + private readonly databaseName: string, + private readonly basicContainerName: string, + private readonly manualContainerName: string, + private readonly lwwContainerName: string, + private readonly udpContainerName: string + ) {} + + public addClient(region: string, client: CosmosClient) { + this.clients.set(region, client); + } + + public async init(): Promise { + const createClient = this.clients.values().next().value; + + const { database } = await createClient.databases.createIfNotExists({ id: this.databaseName }); + const { container: basicContainer } = await database.containers.createIfNotExists({ id: this.basicContainerName }); + const { container: manualContainer } = await database.containers.createIfNotExists({ + id: this.manualContainerName, + conflictResolutionPolicy: { + mode: "Custom" + } + }); + const { container: lwwContainer } = await database.containers.createIfNotExists({ + id: this.lwwContainerName, + conflictResolutionPolicy: { + mode: "LastWriterWins", + conflictResolutionPath: "/regionId" + } + }); + const { container: udpContainer } = await database.containers.createIfNotExists({ + id: this.udpContainerName, + conflictResolutionPolicy: { + mode: "Custom", + conflictResolutionProcedure: `dbs/${this.databaseName}/colls/${this.udpContainerName}/sprocs/resolver` + } + }); + + // See ./lwwSprocDef for the stored procedure definition include the logic + const { sproc: lwwSproc } = await udpContainer.storedProcedures.upsert(lwwSprocDef); + } + + public async RunManualConflict() { + console.log("Insert Conflict"); + await this.RunInsertConflictonManual(); + + console.log("Update Conflict"); + await this.RunUpdateConflictOnManual(); + + console.log("Delete Conflict"); + await this.RunDeleteConflictOnManual(); + } + + public async RunLWWConflict() { + console.log("Insert Conflict"); + await this.RunInsertConflictOnLWW(); + + console.log("Update Conflict"); + await this.RunUpdateConflictOnLWW(); + + console.log("Delete Conflict"); + await this.RunDeleteConflictOnLWW(); + } + + public async RunUDP() { + console.log("Insert Conflict"); + await this.RunInsertConflictOnUdp(); + + console.log("Update Conflict"); + await this.RunUpdateConflictOnUdp(); + + console.log("Delete Conflict"); + await this.RunDeleteConflictsOnUdp(); + } + + private async RunInsertConflictonManual() { + do { + let p = logger( + `Performing conflicting insert across ${this.clients.size} regions on ${this.manualContainerName}` + ).start(); + try { + const insertTask: Array> = []; + const itemBase = { id: guid() }; + + let index = 0; + for (const [clientRegion, client] of this.clients.entries()) { + const container = client.database(this.databaseName).container(this.manualContainerName); + const newDef = { regionId: index++, regionEndpoint: clientRegion, ...itemBase }; // TODO: ReadEndpoint? + insertTask.push(this.tryInsertItem(container.items, newDef)); + } + + const items = await Promise.all(insertTask); + p.succeed(); + + const numberOfConflicts = items.reduce((prev, curr) => (curr !== null ? ++prev : prev), 0); + + if (numberOfConflicts > 1) { + p = logger(`Caused ${numberOfConflicts}, verifying conflict resolution`).succeed(); + + for (const item of items) { + if (item !== null) { + await this.validateAllManualConflict(item); + } + } + break; + } else { + console.log("Retrying insert to induce conflicts"); + } + } catch (err) { + p.fail(); + throw err; + } + } while (true); + } + + private async RunUpdateConflictOnManual() { + let retryCount = 5; + do { + const itemBase = { id: guid() }; + + const [initialRegionName, initialClient] = this.clients.entries().next().value; + const container = initialClient.database(this.databaseName).container(this.manualContainerName); + const item = { regionId: 0, regionEndpoint: initialRegionName, ...itemBase }; // TODO: ReadEndpoint? + const { body: newItemDef } = await container.items.create(item); + + await this.sleep(1000); // 1 second for the write to sync + + console.log( + `1) Performing conflicting update across ${this.clients.size} regions on ${this.manualContainerName}` + ); + + const updates: Array> = []; + let index = 0; + for (const [regionName, client] of this.clients.entries()) { + const newDef = { + regionId: index++, + regionName, + ...itemBase, + _etag: newItemDef._etag + }; + updates.push( + this.tryUpdateItem( + client + .database(this.databaseName) + .container(this.manualContainerName) + .item(itemBase.id), + newDef + ) + ); + } + + const updatedItems = await Promise.all(updates); + const numberOfConflicts = updatedItems.reduce((p: number, c: ItemDefinition) => (c !== null ? ++p : p), -1); + if (numberOfConflicts > 0) { + console.log(`2) Caused ${numberOfConflicts} update conflicts, verifying conflict resolution`); + + for (const updatedItem of updatedItems) { + if (updatedItem) { + await this.validateAllManualConflict(updatedItem); + } + } + return; + } else { + console.log(`Found ${numberOfConflicts} - retrying to create more conflicts`); + } + } while (retryCount--); + console.error("Could not enduce an update conflict for manual conflict resolution"); + } + + private async RunDeleteConflictOnManual() { + do { + const itemBase = { id: guid() }; + + const [initialRegionName, initialClient] = this.clients.entries().next().value; + const container = initialClient.database(this.databaseName).container(this.manualContainerName); + const item = { regionId: 0, regionEndpoint: initialRegionName, ...itemBase }; // TODO: ReadEndpoint? + const { body: newItemDef } = await container.items.create(item); + + await this.sleep(1000); // 1 second for the write to sync + + console.log( + `1) Performing conflicting delete across ${this.clients.size} regions on ${this.manualContainerName}` + ); + + const deletes: Array> = []; + let index = 0; + for (const [regionName, client] of this.clients.entries()) { + const newDef = { regionId: index++, regionName, ...itemBase, _etag: newItemDef._etag, _rid: newItemDef._rid }; + deletes.push( + this.tryDeleteItem( + client + .database(this.databaseName) + .container(this.manualContainerName) + .item(itemBase.id), + newDef + ) + ); + } + + const deletedItems = await Promise.all(deletes); + const numberOfConflicts = deletedItems.reduce((p: number, c: ItemDefinition) => (c !== null ? ++p : p), -1); + if (numberOfConflicts > 1) { + console.log(`2) Caused ${numberOfConflicts} delete conflicts, verifying conflict resolution`); + + await this.validateLWW(deletedItems, true); // LWW deletes and manual deletes are handled the same + + break; + } else { + console.warn("Retrying update/delete to induce conflicts"); + } + } while (true); + } + + private async validateAllManualConflict(item: ItemDefinition) { + let conflictExists = false; + for (const [conflictRegion, client] of this.clients.entries()) { + conflictExists = await this.validateManualConflict(conflictRegion, client, item); + } + + if (conflictExists) { + await this.DeleteConflict(item); + } + } + + private async validateManualConflict(clientRegion: string, client: CosmosClient, item: ItemDefinition) { + while (true) { + const container = client.database(this.databaseName).container(this.manualContainerName); + + const { result: conflicts } = await container.conflicts.readAll().toArray(); + + for (const conflict of conflicts) { + if (conflict.operationType !== Constants.OperationTypes.Delete) { + const content = JSON.parse(conflict.content as any); + if (item.id !== content.id) { + continue; + } + + if (item._rid === content._rid && item._etag === content._etag) { + console.log(`Document from region ${item.regionId} lost conflict @ ${clientRegion}`); + return true; + } else { + try { + const winner = client.database(this.databaseName).container(this.manualContainerName); + console.log(`Document from region ${item.regionId} won the conflict @ ${clientRegion}`); + return false; + } catch (err) { + if (err.code && err.code === StatusCodes.NotFound) { + console.log(`Item from region ${item.regionId} not found @ ${clientRegion}`); + } + } + } + } else { + if (conflict.resourceId === item._rid) { + console.log(`Delete conflict found @ ${clientRegion}`); + return false; + } + } + } + + console.warn(`Document ${item.id} is not found in conflict feed @ ${clientRegion}, retrying`); + await this.sleep(500); + } + } + + private async RunInsertConflictOnLWW() { + do { + console.log(`1) Performing conflicting insert across ${this.clients.size} regions on ${this.lwwContainerName}`); + + const inserts: Array> = []; + const itemBase = { id: guid() }; + + let index = 0; + for (const [clientRegion, client] of this.clients.entries()) { + const container = client.database(this.databaseName).container(this.lwwContainerName); + const newDef = { regionId: index++, regionEndpoint: clientRegion, ...itemBase }; // TODO: ReadEndpoint? + inserts.push(this.tryInsertItem(container.items, newDef)); + } + + const items = (await Promise.all(inserts)).filter(v => v !== null); + + if (items.length > 1) { + console.log(`2) Caused ${items.length} insert conflicts, verifying conflict resolution`); + + await this.validateLWW(items); + break; + } else { + console.warn("Retrying insert to induce conflicts"); + } + } while (true); + } + + private async RunUpdateConflictOnLWW() { + let retry = 5; + do { + const itemBase = { id: guid() }; + + const [initialRegionName, initialClient] = this.clients.entries().next().value; + const container = initialClient.database(this.databaseName).container(this.lwwContainerName); + const item = { regionId: 0, regionEndpoint: initialRegionName, ...itemBase }; // TODO: ReadEndpoint? + const { body: newItemDef } = await container.items.create(item); + + await this.sleep(1000); // 1 second for the write to sync + + console.log(`1) Performing conflicting update across ${this.clients.size} regions on ${this.lwwContainerName}`); + + const updates: Array> = []; + let index = 0; + for (const [regionName, client] of this.clients.entries()) { + const newDef = { regionId: index++, regionName, ...itemBase, _etag: newItemDef._etag }; + updates.push( + this.tryUpdateItem( + client + .database(this.databaseName) + .container(this.lwwContainerName) + .item(itemBase.id), + newDef + ) + ); + } + + const items = (await Promise.all(updates)).filter(v => v !== null); + + if (items.length > 1) { + console.log(`2) Caused ${items.length} update conflicts, verifying conflict resolution`); + + await this.validateLWW(items); + return; + } else { + console.warn("Retrying update to induce conflicts"); + } + } while (retry--); + console.error("Could not induce update conflict on LWW"); + } + + private async RunDeleteConflictOnLWW() { + do { + const itemBase = { id: guid() }; + + const [initialRegionName, initialClient] = this.clients.entries().next().value; + const container = initialClient.database(this.databaseName).container(this.lwwContainerName); + const item = { regionId: 0, regionEndpoint: initialRegionName, ...itemBase }; // TODO: ReadEndpoint? + const { body: newItemDef } = await container.items.create(item); + + await this.sleep(1000); // 1 second for the write to sync + + console.log(`1) Performing conflicting delete across ${this.clients.size} regions on ${this.lwwContainerName}`); + + const deletes: Array> = []; + let index = 0; + for (const [regionName, client] of this.clients.entries()) { + const newDef = { regionId: index++, regionName, ...itemBase, _etag: newItemDef._etag }; + if (index % 2 === 1) { + deletes.push( + this.tryDeleteItem( + client + .database(this.databaseName) + .container(this.lwwContainerName) + .item(itemBase.id), + newDef + ) + ); + } else { + deletes.push( + this.tryUpdateItem( + client + .database(this.databaseName) + .container(this.lwwContainerName) + .item(itemBase.id), + newDef + ) + ); + } + } + + const items = (await Promise.all(deletes)).filter(v => v !== null); + if (items.length > 2) { + console.log(`2) Caused ${items.length} delete conflicts, verifying conflict resolution`); + + await this.validateLWW(items, true); + break; + } else { + console.warn("Retrying update/delete to induce conflicts"); + } + } while (true); + } + + private async validateLWW(items: ItemDefinition[], hasDeleteConflict: boolean = false) { + for (const [regionName, client] of this.clients.entries()) { + await this.validateLWWPerClient(regionName, client, items, hasDeleteConflict); + } + } + + private async validateLWWPerClient( + regionName: string, + client: CosmosClient, + items: ItemDefinition[], + hasDeleteConflict: boolean + ) { + const container = client.database(this.databaseName).container(this.lwwContainerName); + + const { result: conflicts } = await container.conflicts.readAll().toArray(); + + if (conflicts.length !== 0) { + console.error(`Found ${conflicts.length} conflicts in the lww container`); + return; + } + + if (hasDeleteConflict) { + do { + try { + await container.item(items[0].id).read(); + } catch (err) { + if (err.code === StatusCodes.NotFound) { + console.log(`Delete conflict won @ ${regionName}`); + return; + } + } + console.error(`Delete conflict for item ${items[0].id} didn't win @ ${regionName}`); + await this.sleep(500); + } while (true); + } + + const winner = items.reduce((p, c) => (p.regionId <= c.regionId ? c : p), items[0]); + + console.log(`Document from region ${winner.regionId} should be the winner`); + + while (true) { + try { + const { body: currentItem } = await container.item(winner.id).read(); + + if (currentItem.regionId === winner.regionId) { + console.log(`Winner document from region ${currentItem.regionId} found at ${regionName}`); + break; + } + } catch (err) { + /* No op */ + } + + console.error( + `Winning document version from region ${winner.regionId} is not found @ ${regionName}, retrying...` + ); + await this.sleep(500); + } + } + + public async RunInsertConflictOnUdp() { + do { + console.log(`1) Performing conflicting insert across ${this.clients.size} regions on ${this.udpContainerName}`); + + const inserts: Array> = []; + const itemBase = { id: guid() }; + + let index = 0; + for (const [clientRegion, client] of this.clients.entries()) { + const container = client.database(this.databaseName).container(this.udpContainerName); + const newDef = { regionId: index++, regionEndpoint: clientRegion, ...itemBase }; // TODO: ReadEndpoint? + inserts.push(this.tryInsertItem(container.items, newDef)); + } + + const items = (await Promise.all(inserts)).filter(v => v !== null); + + if (items.length > 1) { + console.log(`2) Caused ${items.length} insert conflicts, verifying conflict resolution`); + + await this.validateUDP(items); + break; + } else { + console.warn("Retrying insert to induce conflicts"); + } + } while (true); + } + + public async RunUpdateConflictOnUdp() { + do { + const itemBase = { id: guid() }; + + const [initialRegionName, initialClient] = this.clients.entries().next().value; + const container = initialClient.database(this.databaseName).container(this.udpContainerName); + const item = { regionId: 0, regionEndpoint: initialRegionName, ...itemBase }; // TODO: ReadEndpoint? + const { body: newItemDef } = await container.items.create(item); + + await this.sleep(1000); // 1 second for the write to sync + + console.log(`1) Performing conflicting update across ${this.clients.size} regions on ${this.udpContainerName}`); + + const updates: Array> = []; + let index = 0; + for (const [regionName, client] of this.clients.entries()) { + const newDef = { regionId: index++, regionName, ...itemBase, _etag: newItemDef._etag }; + updates.push( + this.tryUpdateItem( + client + .database(this.databaseName) + .container(this.udpContainerName) + .item(itemBase.id), + newDef + ) + ); + } + + const items = (await Promise.all(updates)).filter(v => v !== null); + + if (items.length > 1) { + console.log(`2) Caused ${items.length} update conflicts, verifying conflict resolution`); + + await this.validateUDP(items); + break; + } else { + console.warn("Retrying update to induce conflicts"); + } + } while (true); + } + + public async RunDeleteConflictsOnUdp() { + do { + const itemBase = { id: guid() }; + + const [initialRegionName, initialClient] = this.clients.entries().next().value; + const container = initialClient.database(this.databaseName).container(this.udpContainerName); + const item = { regionId: 0, regionEndpoint: initialRegionName, ...itemBase }; // TODO: ReadEndpoint? + const { body: newItemDef } = await container.items.create(item); + + await this.sleep(1000); // 1 second for the write to sync + + console.log(`1) Performing conflicting delete across ${this.clients.size} regions on ${this.udpContainerName}`); + + const deletes: Array> = []; + let index = 0; + for (const [regionName, client] of this.clients.entries()) { + const newDef = { regionId: index++, regionName, ...itemBase, _etag: newItemDef._etag }; + if (index % 2 === 1) { + deletes.push( + this.tryDeleteItem( + client + .database(this.databaseName) + .container(this.udpContainerName) + .item(itemBase.id), + newDef + ) + ); + } else { + deletes.push( + this.tryUpdateItem( + client + .database(this.databaseName) + .container(this.udpContainerName) + .item(itemBase.id), + newDef + ) + ); + } + } + + const items = (await Promise.all(deletes)).filter(v => v !== null); + if (items.length > 2) { + console.log(`2) Caused ${items.length} delete conflicts, verifying conflict resolution`); + + await this.validateUDP(items, true); + break; + } else { + console.warn("Retrying update/delete to induce conflicts"); + } + } while (true); + } + + private async validateUDP(items: ItemDefinition[], hasDeleteConflict: boolean = false) { + for (const [regionName, client] of this.clients.entries()) { + await this.validateUDPPerClient(regionName, client, items, hasDeleteConflict); + } + } + + private async validateUDPPerClient( + regionName: string, + client: CosmosClient, + items: ItemDefinition, + hasDeleteConflict: boolean + ) { + const container = client.database(this.databaseName).container(this.udpContainerName); + + const { result: conflicts } = await container.conflicts.readAll().toArray(); + + if (conflicts.length !== 0) { + console.error(`Found ${conflicts.length} conflicts in the udp container`); + return; + } + + if (hasDeleteConflict) { + do { + try { + const { body: shouldNotExist } = await container.item(items[0].id).read(); + } catch (err) { + if (err.code === StatusCodes.NotFound) { + console.log(`Delete conflict won @ ${regionName}`); + return; + } + } + console.error(`Delete conflict for item ${items[0].id} didn't win @ ${regionName}`); + await this.sleep(500); + } while (true); + } + + const winner = items.reduce((p: ItemDefinition, c: ItemDefinition) => (p.regionId <= c.regionId ? c : p), items[0]); + + console.log(`Document from region ${winner.regionId} should be the winner`); + + while (true) { + try { + const { body: currentItem } = await container.item(winner.id).read(); + + if (currentItem.regionId === winner.regionId) { + console.log(`Winner document from region ${currentItem.regionId} found at ${regionName}`); + break; + } + } catch (err) { + /* No op */ + } + + console.error( + `Winning document version from region ${winner.regionId} is not found @ ${regionName}, retrying...` + ); + await this.sleep(500); + } + } + + private async tryInsertItem(items: Items, newDef: ItemDefinition): Promise { + try { + return (await items.create(newDef)).body; + } catch (err) { + // Handle conflict error silently + if (err.code === StatusCodes.Conflict) { + return null; + } + throw err; + } + } + + private async tryUpdateItem(item: Item, newDef: ItemDefinition): Promise { + const time = Date.now(); + try { + return (await item.replace(newDef, { + accessCondition: { + type: "IfMatch", + condition: newDef._etag + } + })).body; + } catch (err) { + if (err.code === StatusCodes.PreconditionFailed || err.code === StatusCodes.NotFound) { + console.log(`${await item.container.database.client.getWriteEndpoint()} hit ${err.code} at ${time}`); + return null; // Lost synchronously or not document yet. No conflict is induced. + } else { + console.log("tryUpdateItem hit unexpected error"); + throw new Error(JSON.stringify(err)); + } + } + } + + private async tryDeleteItem(item: Item, newDef: ItemDefinition): Promise { + try { + const { body: deletedItem } = await item.delete({ + accessCondition: { + type: "IfMatch", + condition: newDef._etag + } + }); + return newDef; + } catch (err) { + if (err.code === StatusCodes.PreconditionFailed || err.code === StatusCodes.NotFound) { + return null; // Lost synchronously or not document yet. No conflict is induced. + } else { + throw new Error(err); + } + } + } + + private async DeleteConflict(item: ItemDefinition) { + const client = this.clients.values().next().value; + const container = client.database(this.databaseName).container(this.manualContainerName); + const conflicts = await container.conflicts.readAll().toArray(); + + for (const conflict of conflicts.result) { + if (conflict.operationType !== Constants.OperationTypes.Delete) { + const content = JSON.parse(conflict.content); + if (content._rid === item._rid && content._etag === item._etag && content.regionId === item.regionId) { + console.log(`Deleting manual conflict ${conflict.resourceId} from region ${item.regionId}`); + await container.conflict(conflict.id).delete(); + } + } else if (conflict.resourceId === item._rid) { + console.log(`Deleting manual conflict ${conflict.resourceId} from region ${item.regionId}`); + await container.conflict(conflict.id).delete(); + } + } + } + + private sleep(timeinMS: number): Promise { + return new Promise((res, rej) => { + setTimeout(() => { + res(); + }, timeinMS); + }); + } +} diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/MultiRegionWriteScenario.ts b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/MultiRegionWriteScenario.ts new file mode 100644 index 000000000000..4622b1c50104 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/MultiRegionWriteScenario.ts @@ -0,0 +1,85 @@ +import { ConnectionPolicy, ConsistencyLevel, CosmosClient } from "../../lib"; +import config from "./config"; +import { ConflictWorker } from "./ConflictWorker"; +import { Worker } from "./Worker"; +// tslint:disable:no-console +export class MultiRegionWriteScenario { + private basicWorkers: Worker[] = []; + private conflictWorker: ConflictWorker; + constructor() { + this.conflictWorker = new ConflictWorker( + config.databaseName, + config.basicCollectionName, + config.manualCollectionName, + config.lwwCollectionName, + config.udpCollectionName + ); + for (const region of config.regions) { + const connectionPolicy: ConnectionPolicy = new ConnectionPolicy(); + connectionPolicy.UseMultipleWriteLocations = true; + connectionPolicy.PreferredLocations = [region]; + const client = new CosmosClient({ + endpoint: config.endpoint, + auth: { masterKey: config.key }, + connectionPolicy, + consistencyLevel: ConsistencyLevel.Eventual + }); + this.conflictWorker.addClient(region, client); + this.basicWorkers.push( + new Worker(region, client.database(config.databaseName).container(config.basicCollectionName)) + ); + } + } + + public async init() { + await this.conflictWorker.init(); + console.log("Initialized containers"); + } + + public async runBasic() { + console.log("################################################"); + console.log("Basic Active-Active"); + console.log("################################################"); + + console.log("1) Starting insert loops across multiple regions"); + + await Promise.all(this.basicWorkers.map(worker => worker.RunLoop(100))); + + console.log("2) Reading from every region..."); + + await Promise.all(this.basicWorkers.map(worker => worker.ReadAll(100 * this.basicWorkers.length))); + + console.log("3) Deleting all the documents"); + + await this.basicWorkers[0].DeleteAll(); + + console.log("################################################"); + } + + public async runManualConflict() { + console.log("################################################"); + console.log("Manual Conflict Resolution"); + console.log("################################################"); + + await this.conflictWorker.RunManualConflict(); + console.log("################################################"); + } + + public async runLWW() { + console.log("################################################"); + console.log("LWW Conflict Resolution"); + console.log("################################################"); + + await this.conflictWorker.RunLWWConflict(); + console.log("################################################"); + } + + public async runUDP() { + console.log("################################################"); + console.log("UDP Conflict Resolution"); + console.log("################################################"); + + await this.conflictWorker.RunUDP(); + console.log("################################################"); + } +} diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/Worker.ts b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/Worker.ts new file mode 100644 index 000000000000..8d598cfb5817 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/Worker.ts @@ -0,0 +1,57 @@ +import { v4 as guid } from "uuid"; +import { Container, CosmosClient } from "../../lib"; + +// tslint:disable:no-console +export class Worker { + constructor(private readonly regionName: string, private readonly container: Container) {} + + public async RunLoop(itemsToInsert: number) { + let iterationCount = 0; + + let latency: number[] = []; + while (iterationCount++ < itemsToInsert) { + const start = Date.now(); + await this.container.items.create({ id: guid() }); + const end = Date.now(); + latency.push(end - start); + } + latency = latency.sort(); + const p50Index = Math.floor(latency.length / 2); + + console.log(`Inserted ${latency.length} documents at ${this.regionName} with p50 ${latency[p50Index]}`); + } + + public async ReadAll(expectedNumberOfItems: number) { + while (true) { + const { result: items } = await this.container.items.readAll().toArray(); + if (items.length < expectedNumberOfItems) { + console.log( + `Total item read ${items.length} from ${ + this.regionName + } is less than ${expectedNumberOfItems}, retrying reads` + ); + + await this.sleep(1000); + } else { + console.log(`Read ${items.length} items from ${this.regionName}`); + return; + } + } + } + + public async DeleteAll() { + const { result: items } = await this.container.items.readAll().toArray(); + for (const item of items) { + await this.container.item(item.id).delete(); + } + console.log(`Deleted all documents from region ${this.regionName}`); + } + + private sleep(timeinMS: number) { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, timeinMS); + }); + } +} diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/app.ts b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/app.ts new file mode 100644 index 000000000000..54ab5101d470 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/app.ts @@ -0,0 +1,22 @@ +import { MultiRegionWriteScenario } from "./MultiRegionWriteScenario"; + +// tslint:disable:no-console +async function run() { + const scenarios = new MultiRegionWriteScenario(); + await scenarios.init(); + + await scenarios.runBasic(); + await scenarios.runManualConflict(); + await scenarios.runLWW(); + await scenarios.runUDP(); +} + +run() + .catch(err => { + console.error(err); + process.exit(1); + }) + .then(() => { + console.log("Complete!"); + process.exit(0); + }); diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/config.ts b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/config.ts new file mode 100644 index 000000000000..cc34928ea30a --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/config.ts @@ -0,0 +1,27 @@ +const endpoint = process.env["endpoint"]; +const key = process.env["key"]; + +if (!endpoint || !key) { + // tslint:disable-next-line:no-console + console.error("Missing endpoint and key environment variables. Exiting..."); + process.exit(1); +} + +const regions = process.env["regions"].split(";"); + +const databaseName = process.env["databaseName"] || "js-mww-test"; +const manualCollectionName = process.env["manualCollectionName"] || "manualCollection"; +const lwwCollectionName = process.env["lwwCollectionName"] || "lwwCollection"; +const udpCollectionName = process.env["udpCollectionName"] || "udpCollection"; +const basicCollectionName = process.env["basicCollectionName"] || "basicCollection"; + +export default { + endpoint, + key, + regions, + databaseName, + manualCollectionName, + lwwCollectionName, + udpCollectionName, + basicCollectionName +}; diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/logger.ts b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/logger.ts new file mode 100644 index 000000000000..7d3e4b87004b --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/logger.ts @@ -0,0 +1,8 @@ +import * as Ora from "ora"; + +export default (text: string) => { + return new Ora({ + spinner: "clock", + text + }); +}; diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/lwwSprocDef.ts b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/lwwSprocDef.ts new file mode 100644 index 000000000000..b2d97b33de8c --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/lwwSprocDef.ts @@ -0,0 +1,59 @@ +import { StoredProcedureDefinition } from "../../lib"; + +const lwwSprocDef: StoredProcedureDefinition = { + id: "resolver", + body: `function resolver(incomingRecord, existingRecord, isTombstone, conflictingRecords) { + var collection = getContext().getCollection(); + + if (!incomingRecord) { + if (existingRecord) { + + collection.deleteDocument(existingRecord._self, {}, function(err, responseOptions) { + if (err) throw err; + }); + } + } else if (isTombstone) { + // delete always wins. + } else { + var documentToUse = incomingRecord; + + if (existingRecord) { + if (documentToUse.regionId < existingRecord.regionId) { + documentToUse = existingRecord; + } + } + + var i; + for (i = 0; i < conflictingRecords.length; i++) { + if (documentToUse.regionId < conflictingRecords[i].regionId) { + documentToUse = conflictingRecords[i]; + } + } + + tryDelete(conflictingRecords, incomingRecord, existingRecord, documentToUse); + } + + function tryDelete(documents, incoming, existing, documentToInsert) { + if (documents.length > 0) { + collection.deleteDocument(documents[0]._self, {}, function(err, responseOptions) { + if (err) throw err; + + documents.shift(); + tryDelete(documents, incoming, existing, documentToInsert); + }); + } else if (existing) { + collection.replaceDocument(existing._self, documentToInsert, + function(err, documentCreated) { + if (err) throw err; + }); + } else { + collection.createDocument(collection.getSelfLink(), documentToInsert, + function(err, documentCreated) { + if (err) throw err; + }); + } + } +}` +}; + +export default lwwSprocDef; diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/package-lock.json b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/package-lock.json new file mode 100644 index 000000000000..000f0be850c6 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/package-lock.json @@ -0,0 +1,272 @@ +{ + "name": "multiregionwrite", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.7.1.tgz", + "integrity": "sha512-EGoI4ylB/lPOaqXqtzAyL8HcgOuCtH2hkEaLmkueOYufsTFWBn4VCvlCDC2HW8Q+9iF+QVC3sxjDKQYjHQeZ9w==", + "dev": true + }, + "@types/ora": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/ora/-/ora-1.3.4.tgz", + "integrity": "sha512-DrHOHEdYzRjL65n2v+NwTdhC8tACaiCDnUU1wAAbibfZOaRj3KjUb3unnAAWFZuny43qPAvB6ka+Iyj2R2XPxw==", + "dev": true, + "requires": { + "@types/node": "10.7.1" + } + }, + "@types/uuid": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", + "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, + "requires": { + "@types/node": "10.7.1" + } + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.2" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-spinners": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", + "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==" + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "1.0.4" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "2.4.1" + } + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "1.2.0" + } + }, + "ora": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.0.0.tgz", + "integrity": "sha512-LBS97LFe2RV6GJmXBi6OKcETKyklHNMV0xw7BtsVn2MlsgsydyZetSCbCANr+PFLmDyv4KV88nn0eCKza665Mg==", + "requires": { + "chalk": "2.4.1", + "cli-cursor": "2.1.0", + "cli-spinners": "1.3.1", + "log-symbols": "2.2.0", + "strip-ansi": "4.0.0", + "wcwidth": "1.0.1" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "dev": true, + "requires": { + "buffer-from": "1.1.1", + "source-map": "0.6.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "3.0.0" + } + }, + "ts-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", + "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "buffer-from": "1.1.1", + "diff": "3.5.0", + "make-error": "1.3.4", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.5.9", + "yn": "2.0.0" + } + }, + "typescript": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz", + "integrity": "sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "1.0.3" + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } +} diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/package.json b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/package.json new file mode 100644 index 000000000000..03d9fa084a0e --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/package.json @@ -0,0 +1,21 @@ +{ + "name": "multiregionwrite", + "version": "0.0.1", + "description": "Demonstrates the ability to read between multiple regions and handling conflicts", + "main": "app.ts", + "scripts": { + "start": "ts-node app.ts" + }, + "author": "", + "license": "ISC", + "dependencies": { + "ora": "^3.0.0", + "uuid": "^3.3.2" + }, + "devDependencies": { + "@types/ora": "^1.3.4", + "@types/uuid": "^3.4.3", + "ts-node": "^7.0.1", + "typescript": "^3.0.1" + } +} diff --git a/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/readme.md b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/readme.md new file mode 100644 index 000000000000..96d6ae3e6388 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/MultiRegionWrite/readme.md @@ -0,0 +1,17 @@ +# Multi-Region Write + +This demo shows off writing to multiple regions at the same time. It also demos different conflict handling scenarios. + +## Quick start + +1. Install packages: `npm i` +2. Set environment variables + 1. endpoint - the endpoint url + 2. key - the masterkey for the account + 3. regions - a semicolon deliminated list of regions (aka westus;eastus) + 4. There are additional config options in the config.ts file, but they are not required. +3. Start: `npm start` + +## Debugging with VS Code + +There is a launch.json config named "MultiRegionWrite Debug" which you can use to attach via VS Code. diff --git a/sdk/cosmosdb/cosmos/samples/ServerSideScripts/JS/README.md b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/JS/README.md new file mode 100644 index 000000000000..f5492d8b3d45 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/JS/README.md @@ -0,0 +1,3 @@ +Our server-side javascript samples have moved to: +https://github.com/Azure/azure-documentdb-js-server/tree/master/samples + diff --git a/sdk/cosmosdb/cosmos/samples/ServerSideScripts/JS/upsert.js b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/JS/upsert.js new file mode 100644 index 000000000000..71522f219624 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/JS/upsert.js @@ -0,0 +1,74 @@ +'use strict'; +/** + * An Azure Cosmos DB stored procedure that upserts a given document (insert new or update if present) using its id property.
+ * This implementation tries to create, and if the create fails then query for the document with the specified document's id, then replace it. + * Use this sproc if creates are more common than replaces, otherwise use "upsertOptimizedForReplace" + * + * @function + * @param {Object} document - A document that should be upserted into this collection. + * @returns {Object.} Returns an object with the property:
+ * op - created (or) replaced. + */ +var upsert = { + id: "upsert", + body: function (document) { + var context = getContext(); + var collection = context.getCollection(); + var collectionLink = collection.getSelfLink(); + var response = context.getResponse(); + var errorCodes = { CONFLICT: 409 }; + + // Not checking for existence of document.id for compatibility with createDocument. + if (!document) throw new Error("The document is undefined or null."); + + tryCreate(document, callback); + + function tryCreate(doc, callback) { + var isAccepted = collection.createDocument(collectionLink, doc, callback); + if (!isAccepted) throw new Error("Unable to schedule create document"); + response.setBody({"op": "created"}); + } + + // To replace the document, first issue a query to find it and then call replace. + function tryReplace(doc, callback) { + retrieveDoc(doc, null, function(retrievedDocs){ + var isAccepted = collection.replaceDocument(retrievedDocs[0]._self, doc, callback); + if (!isAccepted) throw new Error("Unable to schedule replace document"); + response.setBody({"op": "replaced"}); + }); + } + + function retrieveDoc(doc, continuation, callback) { + var query = { query: "select * from root r where r.id = @id", parameters: [ {name: "@id", value: doc.id}]}; + var requestOptions = { continuation : continuation }; + var isAccepted = collection.queryDocuments(collectionLink, query, requestOptions, function(err, retrievedDocs, responseOptions) { + if (err) throw err; + + if (retrievedDocs.length > 0) { + callback(retrievedDocs); + } else if (responseOptions.continuation) { + // Conservative check for continuation. Not expected to hit in practice for the "id query" + retrieveDoc(doc, responseOptions.continuation, callback); + } else { + throw new Error("Error in retrieving document: " + doc.id); + } + }); + if (!isAccepted) throw new Error("Unable to query documents"); + } + + // This is called when collection.createDocument is done in order to + // process the result. + function callback(err, doc, options) { + if (err) { + // Replace the document if status code is 409 and upsert is enabled + if(err.number == errorCodes.CONFLICT) { + return tryReplace(document, callback); + } else { + throw err; + } + } + } + } +} + +module.exports = upsert; \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/ServerSideScripts/README.md b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/README.md new file mode 100644 index 000000000000..4590ad2f1cfd --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/README.md @@ -0,0 +1 @@ +Samples for creating and executing ServerSide Scripts such as Stored Procedures, Triggers and User Defined Functions \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/ServerSideScripts/app.js b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/app.js new file mode 100644 index 000000000000..2054fba556d7 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/app.js @@ -0,0 +1,66 @@ +// @ts-check +console.log(); +console.log("Azure Cosmos DB Node.js Samples"); +console.log("================================"); +console.log(); +console.log("SERVER SIDE SCRIPTS"); +console.log("==================="); +console.log(); + +/*jshint node:true */ +("use strict"); + +const cosmos = require("../../lib/src"); +const CosmosClient = cosmos.CosmosClient; +const config = require("../Shared/config"); +const fs = require("fs"); +const databaseId = config.names.database; +const containerId = config.names.container; + +const endpoint = config.connection.endpoint; +const masterKey = config.connection.authKey; + +// Establish a new instance of the DocumentDBClient to be used throughout this demo +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +// Path to stored procedure definition +const sprocDefinition = require("./JS/upsert"); + +// Execute the stored procedure with the following parameters. +const sprocParams = [ + { + id: "myDocument", + foo: "bar" + } +]; + +async function run() { + const { database } = await client.databases.create({ id: databaseId }); + const { container } = await database.containers.create({ id: containerId }); + + console.log("Upserting the sproc: '" + sprocDefinition.id + "'"); + + // Query for the stored procedure. + const { sproc, body: sprocDef } = await container.storedProcedures.upsert(sprocDefinition); + + console.log("Executing the sproc: '" + sproc.id + "'"); + console.log("Sproc parameters: " + JSON.stringify(sprocParams)); + + const { body: results, headers } = await sproc.execute(sprocParams); + console.log("//////////////////////////////////"); + if (headers) { + console.log("// responseHeaders"); + console.log(headers); + } + if (results) { + console.log("// results"); + console.log(results); + } + console.log("//////////////////////////////////"); + + await database.delete(); + console.log("Database and Collection DELETED"); + console.log("Demo finished"); +} + +run().catch(console.error); diff --git a/sdk/cosmosdb/cosmos/samples/ServerSideScripts/package.json b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/package.json new file mode 100644 index 000000000000..220bb71ed436 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/ServerSideScripts/package.json @@ -0,0 +1,10 @@ +{ + "name": "cosmos-serversidescripts-sample", + "private": true, + "version": "0.0.0", + "description": "A sample showing server side scripts with Azure Cosmos DB", + "scripts": { + "start": "node app.js" + }, + "dependencies": {} +} diff --git a/sdk/cosmosdb/cosmos/samples/Shared/Data/Families.json b/sdk/cosmosdb/cosmos/samples/Shared/Data/Families.json new file mode 100644 index 000000000000..73a534f6b771 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/Shared/Data/Families.json @@ -0,0 +1,188 @@ +{ + "Families": [ + { + "id": "AndersenFamily", + "lastName": "Andersen", + "parents": [ + { + "firstName": "Thomas" + }, + { + "firstName": "Mary Kay" + } + ], + "children": [ + { + "firstName": "Henriette Thaulow", + "gender": "female", + "grade": 5, + "pets": [ + { + "givenName": "Fluffy" + } + ] + } + ], + "address": { + "state": "CA", + "county": "Orange", + "city": "Los Angeles", + "zip" : 90210 + }, + "isRegistered": true + }, + { + "id": "KinDocument", + "familyName": "Kin", + "parents": [ + { + "givenName": "Tatsunari" + } + ], + "address": { + "state": "WA", + "county": "King", + "city": "Redmond", + "zip": 98052 + } + }, + { + "id": "MeyerAndFamily", + "lastName": "Meyer", + "parents": [ + { + "firstName": "April", + "pets": [ + { + "givenName": "Wheeler" + } + ] + } + ], + "children": [ + { + "firstName": "Chris", + "gender": "male" + } + ], + "address": { + "state": "WA", + "county": "King", + "city": "Kirkland", + "zip": 98033 + } + }, + { + "id": "TheAlexanders", + "familyName": "Alexander", + "parents": [ + { + "givenName": "David" + } + ], + "children": [ + { + "givenName": "Michael", + "gender": "male", + "grade": 6 + } + ], + "address": { + "state": "WA", + "county": "King", + "city": "Kirkland", + "zip": 98033 + } + }, + { + "id": "TheSmiths", + "familyName": "Smith", + "parents": [ + { + "givenName": "Tony" + }, + { + "givenName": "Denise", + "pets": [ + { + "givenName": "Chewy" + } + ] + } + ], + "children": [ + { + "givenName": "Jeff", + "gender": "male", + "grade": 2 + }, + { + "givenName": "Ben", + "gender": "male", + "grade": 5 + }, + { + "givenName": "Samantha", + "gender": "female", + "grade": 9 + } + ], + "address": { + "state": "NY", + "county": "Bronx", + "city": "The Bronx", + "zip": 10453 + } + }, + { + "id": "WakefieldFamily", + "parents": [ + { + "familyName": "Wakefield", + "givenName": "Robin" + }, + { + "familyName": "Miller", + "givenName": "Ben" + } + ], + "children": [ + { + "familyName": "Merriam", + "givenName": "Jesse", + "gender": "female", + "grade": 1, + "pets": [ + { + "givenName": "Goofy" + }, + { + "givenName": "Shadow" + } + ] + }, + { + "familyName": "Miller", + "givenName": "Lisa", + "gender": "female", + "grade": 8 + } + ], + "address": { + "state": "FL", + "city": "Miami", + "zip": 33011 + }, + "isRegistered": false + }, + { + "id": "AdamsFamily", + "address": { + "state": "FL", + "city": "Miami", + "zip": 33002 + } + } + ] +} + diff --git a/sdk/cosmosdb/cosmos/samples/Shared/config.js b/sdk/cosmosdb/cosmos/samples/Shared/config.js new file mode 100644 index 000000000000..51ef2b78fee1 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/Shared/config.js @@ -0,0 +1,15 @@ +exports.connection = { + endpoint: process.env.COSMOS_SAMPLE_ENDPOINT || "https://localhost:8081/", + authKey: + process.env.COSMOS_SAMPLE_ENDPOINT || + "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" +}; + +if (exports.connection.endpoint.includes("https://localhost")) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +} + +exports.names = { + database: "NodeSamples", + container: "Data" +}; diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/.gitignore b/sdk/cosmosdb/cosmos/samples/TodoApp/.gitignore new file mode 100644 index 000000000000..09f767e769ca --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/.gitignore @@ -0,0 +1,2 @@ +!config.js +!bin \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/.vscode/launch.json b/sdk/cosmosdb/cosmos/samples/TodoApp/.vscode/launch.json new file mode 100644 index 000000000000..f4816c4c729a --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/bin/www", + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/../../lib/**" + ], + "env": { + "NODE_TLS_REJECT_UNAUTHORIZED": "0" + } + } + ] +} \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/app.js b/sdk/cosmosdb/cosmos/samples/TodoApp/app.js new file mode 100644 index 000000000000..fef8252f544e --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/app.js @@ -0,0 +1,72 @@ +const CosmosClient = require("../../").CosmosClient; +const config = require("./config"); +const TaskList = require("./routes/tasklist"); +const TaskDao = require("./models/taskDao"); + +const express = require("express"); +const path = require("path"); +const favicon = require("serve-favicon"); +const logger = require("morgan"); +const cookieParser = require("cookie-parser"); +const bodyParser = require("body-parser"); + +const index = require("./routes/index"); +const users = require("./routes/users"); + +const app = express(); + +// view engine setup +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "jade"); + +// uncomment after placing your favicon in /public +//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); +app.use(logger("dev")); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, "public"))); + +//Todo App: +const docDbClient = new CosmosClient({ + endpoint: config.host, + auth: { + masterKey: config.authKey + } +}); +const taskDao = new TaskDao(docDbClient, config.databaseId, config.collectionId); +const taskList = new TaskList(taskDao); +taskDao + .init(err => { + console.error(err); + }) + .catch(err => { + console.error(err); + console.error("Shutting down"); + process.exit(1); + }); + +app.get("/", (req, res, next) => taskList.showTasks(req, res).catch(next)); +app.post("/addtask", (req, res, next) => taskList.addTask(req, res).catch(next)); +app.post("/completetask", (req, res, next) => taskList.completeTask(req, res).catch(next)); +app.set("view engine", "jade"); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + const err = new Error("Not Found"); + err.status = 404; + next(err); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get("env") === "development" ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render("error"); +}); + +module.exports = app; diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/bin/www b/sdk/cosmosdb/cosmos/samples/TodoApp/bin/www new file mode 100644 index 000000000000..1c58b2944cd8 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +const app = require('../app'); +const debug = require('debug')('todo:server'); +const http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +const port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +const server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + const port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + const addr = server.address(); + const bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/config.js b/sdk/cosmosdb/cosmos/samples/TodoApp/config.js new file mode 100644 index 000000000000..463a379dec0c --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/config.js @@ -0,0 +1,14 @@ +const config = {}; + +config.host = process.env.HOST || "https://localhost:8081/"; +config.authKey = + process.env.AUTH_KEY || "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; +config.databaseId = "ToDoList"; +config.collectionId = "Items"; + +if (config.host.includes("https://localhost:")) { + console.log("WARNING: Disabled checking of self-signed certs. Do not have this code in production."); + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +} + +module.exports = config; diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/models/taskDao.js b/sdk/cosmosdb/cosmos/samples/TodoApp/models/taskDao.js new file mode 100644 index 000000000000..1c7716859443 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/models/taskDao.js @@ -0,0 +1,77 @@ +// @ts-check +const CosmosClient = require("../../../").CosmosClient; + +class TaskDao { + /** + * + * @param {CosmosClient} cosmosClient + * @param {*} databaseId + * @param {*} containerId + */ + constructor(cosmosClient, databaseId, containerId) { + this.client = cosmosClient; + this.databaseId = databaseId; + this.collectionId = containerId; + + this.database = null; + this.container = null; + } + + async init() { + try { + const dbResponse = await this.client.databases.createIfNotExists({ id: this.databaseId }); + this.database = dbResponse.database; + const coResponse = await this.database.containers.create({ id: this.collectionId }); + this.container = coResponse.container; + } catch (err) { + throw err; + } + } + + async find(querySpec) { + if (!this.container) { + throw new Error("Collection is not initialized."); + } + try { + const { result: results } = await this.container.items.query(querySpec).toArray(); + return results; + } catch (err) { + throw err; + } + } + + async addItem(item) { + item.date = Date.now(); + item.completed = false; + try { + const { body: doc } = await this.container.items.create(item); + return doc; + } catch (err) { + throw err; + } + } + + async updateItem(itemId) { + try { + const doc = await this.getItem(itemId); + doc.completed = true; + + const { body: replaced } = await this.container.item(itemId).replace(doc); + return replaced; + } catch (err) { + throw err; + } + } + + async getItem(itemId) { + try { + const { body } = await this.container.item(itemId).read(); + + return body; + } catch (err) { + throw err; + } + } +} + +module.exports = TaskDao; diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/package.json b/sdk/cosmosdb/cosmos/samples/TodoApp/package.json new file mode 100644 index 000000000000..539fb0e4046c --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/package.json @@ -0,0 +1,18 @@ +{ + "name": "todo", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "set NODE_TLS_REJECT_UNAUTHORIZED=0 && node ./bin/www" + }, + "dependencies": { + "async": "^2.1.2", + "body-parser": "~1.15.2", + "cookie-parser": "~1.4.3", + "debug": "~2.2.0", + "express": "~4.14.0", + "jade": "~1.11.0", + "morgan": "~1.7.0", + "serve-favicon": "~2.3.0" + } +} diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/public/stylesheets/style.css b/sdk/cosmosdb/cosmos/samples/TodoApp/public/stylesheets/style.css new file mode 100644 index 000000000000..2c0c235ac1c9 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/public/stylesheets/style.css @@ -0,0 +1,17 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} +a { + color: #00B7FF; +} +.well label { + display: block; +} +.well input { + margin-bottom: 5px; +} +.btn { + margin-top: 5px; + border: outset 1px #C8C8C8; +} \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/readme.md b/sdk/cosmosdb/cosmos/samples/TodoApp/readme.md new file mode 100644 index 000000000000..99b87ed39e6c --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/readme.md @@ -0,0 +1,26 @@ +# Todo App + +Sample Todo app + +## Prereqs + +- Build the SDK (see [dev.md](../../dev.md)) +- Node 8 (uses async/await) + +## Config + +If you're using the local emulator with default config, it should work without setting any additionanl config + +**Environment Variables** +- `host` - url for the Cosmos DB (default is https://localhost:8081) +- `AUTH_KEY` - master key for the Cosmos DB (default is the well known key for emulator) +- `PORT` - port for the web app (default is 3000) + +## Run + +```bash +npm i +npm start +``` + +open browser to http://localhost:3000 \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/routes/index.js b/sdk/cosmosdb/cosmos/samples/TodoApp/routes/index.js new file mode 100644 index 000000000000..6e84977257a7 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/routes/index.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +module.exports = router; diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/routes/tasklist.js b/sdk/cosmosdb/cosmos/samples/TodoApp/routes/tasklist.js new file mode 100644 index 000000000000..51dfe0aa24ed --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/routes/tasklist.js @@ -0,0 +1,63 @@ +const CosmosClient = require('../../../').DocumentClient; +const TaskDao = require('../models/TaskDao'); +const async = require('async'); + +class TaskList { + /** + * + * @param {TaskDao} taskDao + */ + constructor(taskDao) { + this.taskDao = taskDao; + } + async showTasks(req, res) { + const querySpec = { + query: 'SELECT * FROM root r WHERE r.completed=@completed', + parameters: [{ + name: '@completed', + value: false + }] + }; + + try { + const items = await this.taskDao.find() + res.render('index', { + title: 'My ToDo List ', + tasks: items + }); + + } catch (err) { + throw err; + } + } + + async addTask(req, res) { + const item = req.body; + + try { + await this.taskDao.addItem(item); + res.redirect('/'); + } catch (err) { + throw err; + } + } + + async completeTask(req, res) { + const completedTasks = Object.keys(req.body); + const tasks = []; + + try { + completedTasks.forEach((task) => { + tasks.push(this.taskDao.updateItem(task)); + }); + + await Promise.all(tasks); + + res.redirect('/'); + } catch (err) { + throw err; + } + } +} + +module.exports = TaskList; \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/routes/users.js b/sdk/cosmosdb/cosmos/samples/TodoApp/routes/users.js new file mode 100644 index 000000000000..f15a20da957e --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/routes/users.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); + +/* GET users listing. */ +router.get('/', function(req, res, next) { + res.send('respond with a resource'); +}); + +module.exports = router; diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/views/error.jade b/sdk/cosmosdb/cosmos/samples/TodoApp/views/error.jade new file mode 100644 index 000000000000..51ec12c6a263 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/views/error.jade @@ -0,0 +1,6 @@ +extends layout + +block content + h1= message + h2= error.status + pre #{error.stack} diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/views/index.jade b/sdk/cosmosdb/cosmos/samples/TodoApp/views/index.jade new file mode 100644 index 000000000000..672292385b09 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/views/index.jade @@ -0,0 +1,40 @@ +extends layout + +block content + h1 #{title} + br + + form(action="/completetask", method="post") + table.table.table-striped.table-bordered + tr + td Name + td Category + td Date + td Complete + if (typeof tasks === "undefined") + tr + td + else + each task in tasks + tr + td #{task.name} + td #{task.category} + - var date = new Date(task.date); + - var day = date.getDate(); + - var month = date.getMonth() + 1; + - var year = date.getFullYear(); + td #{month + "/" + day + "/" + year} + td + if(task.completed) + input(type="checkbox", disabled, name="#{task.id}", value="#{!task.completed}", checked=task.completed) + else + input(type="checkbox", name="#{task.id}", value="#{!task.completed}", checked=task.completed) + button.btn(type="submit") Update tasks + hr + form.well(action="/addtask", method="post") + label Item Name: + input(name="name", type="textbox") + label Item Category: + input(name="category", type="textbox") + br + button.btn(type="submit") Add item \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/TodoApp/views/layout.jade b/sdk/cosmosdb/cosmos/samples/TodoApp/views/layout.jade new file mode 100644 index 000000000000..4c6ad80fd843 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/TodoApp/views/layout.jade @@ -0,0 +1,13 @@ +doctype html +html + head + title= title + link(rel='stylesheet', href='//ajax.aspnetcdn.com/ajax/bootstrap/3.3.2/css/bootstrap.min.css') + link(rel='stylesheet', href='/stylesheets/style.css') + body + nav.navbar.navbar-inverse.navbar-fixed-top + div.navbar-header + a.navbar-brand(href='#') My Tasks + block content + script(src='//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.2.min.js') + script(src='//ajax.aspnetcdn.com/ajax/bootstrap/3.3.2/bootstrap.min.js') \ No newline at end of file diff --git a/sdk/cosmosdb/cosmos/samples/UserManagement/README.md b/sdk/cosmosdb/cosmos/samples/UserManagement/README.md new file mode 100644 index 000000000000..74ceb86a2d39 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/UserManagement/README.md @@ -0,0 +1,3 @@ +# UserManagement + + diff --git a/sdk/cosmosdb/cosmos/samples/UserManagement/app.js b/sdk/cosmosdb/cosmos/samples/UserManagement/app.js new file mode 100644 index 000000000000..865f5df351a4 --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/UserManagement/app.js @@ -0,0 +1,259 @@ +// @ts-check +console.log(); +console.log("Azure Cosmos DB Node.js Samples"); +console.log("================================"); +console.log(); +console.log("USER MANAGEMENT"); +console.log("================"); +console.log(); + +const cosmos = require("../../lib/src"); +const CosmosClient = cosmos.CosmosClient; +const config = require("../Shared/config"); +const databaseId = config.names.database; +const containerId = config.names.container; + +const endpoint = config.connection.endpoint; +const masterKey = config.connection.authKey; + +const container1Name = "COL1"; +const container2Name = "COL2"; +const user1Name = "Thomas Andersen"; +const user2Name = "Robin Wakefield"; +const item1Name = "item1"; +const item2Name = "item2"; +const item3Name = "item3"; + +// Establish a new instance of the DocumentDBClient to be used throughout this demo +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +async function run() { + const resources = await init(); + await attemptAdminOperations(resources.container1, resources.user1, resources.permission1); + await attemptWriteWithReadPermissionAsync(resources.container1, resources.user1, resources.permission1); + await attemptReadFromTwoCollections( + resources.container1, + resources.container2, + resources.user1, + resources.permission1, + resources.permission3 + ); + await finish(); +} + +async function init() { + //-------------------------------------------------------------------------------------------------- + // We need a database, two containers, two users, and some permissions for this sample, + // So let's go ahead and set these up initially + //-------------------------------------------------------------------------------------------------- + const { database } = await client.databases.createIfNotExists({ id: databaseId }); + const { container: container1 } = await database.containers.createIfNotExists({ id: container1Name }); + const { container: container2 } = await database.containers.createIfNotExists({ id: container2Name }); + + let itemSpec = { id: item1Name }; + + let userDef = { id: user1Name }; + + let permissionDef; + + const { body: itemDef, item: item1 } = await container1.items.create(itemSpec); + console.log(`${item1Name}Created in ${container1Name} !`); + + itemSpec = { id: item2Name }; + + const { item: item2 } = await container1.items.create(itemSpec); + console.log(`${item2Name}Created in ${container1Name} !`); + + itemSpec = { id: item3Name }; + + const { item: item3 } = await container2.items.create(itemSpec); + console.log(`${item3Name} Created in ${container2Name} !`); + + const { user: user1 } = await database.users.create(userDef); + console.log(`${user1Name} created!`); + + userDef = { id: user2Name }; + + const { user: user2 } = await database.users.create(userDef); + console.log(`${user2Name} created!`); + + // Read Permission on container 1 for user1 + permissionDef = { id: "p1", permissionMode: cosmos.DocumentBase.PermissionMode.Read, resource: container1.url }; + + const { ref: permission1 } = await user1.permissions.create(permissionDef); + console.log(`Read only permission assigned to Thomas Andersen on container 1!`); + + permissionDef = { id: "p2", permissionMode: cosmos.DocumentBase.PermissionMode.All, resource: item1.url }; + + // All Permissions on Doc1 for user1 + const { ref: permission2 } = await user1.permissions.create(permissionDef); + console.log("All permission assigned to Thomas Andersen on item 1!"); + + permissionDef = { id: "p3", permissionMode: cosmos.DocumentBase.PermissionMode.Read, resource: container2.url }; + + // Read Permissions on Col2 for user1 + const { ref: permission3 } = await user1.permissions.create(permissionDef); + console.log("Read permission assigned to Thomas Andersen on container 2!"); + + permissionDef = { id: "p4", permissionMode: cosmos.DocumentBase.PermissionMode.All, resource: container2.url }; + + const { ref: permission4 } = await user2.permissions.create(permissionDef); + console.log("All permission assigned to Robin Wakefield on container 2!"); + + const { result: permissions } = await user1.permissions.readAll().toArray(); + console.log(`Fetched permission for Thomas Andersen. Count is : ${permissions.length}`); + + return { user1, user2, container1, container2, permission1, permission2, permission3, permission4 }; +} + +//handle error +async function handleError(error) { + console.log(); + console.log(`An error with code '${error.code}' has occurred:`); + console.log(`\t${error.body || error}`); + if (error.headers) { + console.log(`\t${JSON.stringify(error.headers)}`); + } + console.log(); + try { + await finish(); + } catch (err) { + console.log("Database might not have cleaned itself up properly..."); + } +} + +async function finish() { + await client.database(databaseId).delete(); + console.log(); + console.log("End of demo."); +} + +/** + * + * @param {cosmos.Permission} permission + */ +async function getResourceToken(container, permission) { + const { body: permDef } = await permission.read(); + const resourceToken = {}; + resourceToken[container.url] = permDef._token; + return resourceToken; +} + +/** + * Attempt to do admin operations when user only has Read on a container + * @param {cosmos.Container} container + * @param {cosmos.User} user + * @param {cosmos.Permission} permission + */ +async function attemptAdminOperations(container, user, permission) { + /** @type any */ + const resourceTokens = await getResourceToken(container, permission); + const client = new CosmosClient({ + endpoint, + auth: { + resourceTokens + } + }); + + await client + .database(databaseId) + .container(container.id) + .items.readAll() + .toArray(); + console.log(`${user.id} able to perform read operation on container 1`); + + try { + await client.databases.readAll().toArray(); + } catch (err) { + console.log( + `Expected error occurred as ${user.id} does not have access to get the list of databases. Error code : ${ + err.code + }` + ); + } +} + +/** + * attempts to write in container 1 with user 1 permission. It fails as the user1 has read only permission on container 1 + * @param {cosmos.Container} container + * @param {cosmos.User} user + * @param {cosmos.Permission} permission + */ +async function attemptWriteWithReadPermissionAsync(container, user, permission) { + /** @type any */ + const resourceTokens = await getResourceToken(container, permission); + const client = new CosmosClient({ + endpoint, + auth: { + resourceTokens + } + }); + + const itemDef = { id: "not allowed" }; + try { + await client + .database(databaseId) + .container(container.id) + .items.upsert(itemDef); + } catch (err) { + console.log( + `Expected error occurred as ${ + user.id + } does not have access to insert an item in the first container. Error code : ${err.code}` + ); + } +} + +//attempts to read from both the containers as the user has read permission +/** + * + * @param {cosmos.Container} container1 + * @param {cosmos.Container} container2 + * @param {cosmos.User} user1 + * @param {cosmos.Permission} permission1 + * @param {cosmos.Permission} permission2 + */ +async function attemptReadFromTwoCollections(container1, container2, user1, permission1, permission2) { + const token1 = await getResourceToken(container1, permission1); + const token2 = await getResourceToken(container2, permission2); + const resourceTokens = { ...token1, ...token2 }; + + const client = new CosmosClient({ + endpoint, + auth: { + resourceTokens + } + }); + + const { result: items1 } = await client + .database(databaseId) + .container(container1.id) + .items.readAll() + .toArray(); + console.log(`${user1.id} able to read items from container 1. Document count is ${items1.length}`); + + const { result: items2 } = await client + .database(databaseId) + .container(container2.id) + .items.readAll() + .toArray(); + + console.log(`${user1.id} able to read items from container 2. Document count is ${items2.length}`); + + const itemDef = { id: "not allowed" }; + + try { + await client + .database(databaseId) + .container(container2.id) + .items.upsert(itemDef); + } catch (err) { + console.log( + `Expected error occurred as ${user1.id} does not have access to insert an item in container 2. Error code : ${ + err.code + }` + ); + } +} + +run().catch(handleError); diff --git a/sdk/cosmosdb/cosmos/samples/UserManagement/package.json b/sdk/cosmosdb/cosmos/samples/UserManagement/package.json new file mode 100644 index 000000000000..57f9302c985d --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/UserManagement/package.json @@ -0,0 +1,10 @@ +{ + "name": "user-management", + "version": "0.0.0", + "private": true, + "description": "UserManagement", + "scripts": { + "start": "node app.js" + }, + "dependencies": {} +} diff --git a/sdk/cosmosdb/cosmos/samples/readme.md b/sdk/cosmosdb/cosmos/samples/readme.md new file mode 100644 index 000000000000..d976430b2def --- /dev/null +++ b/sdk/cosmosdb/cosmos/samples/readme.md @@ -0,0 +1,51 @@ +## Introduction + +These samples demonstrate how to use the Node.js SDK to interact with the [Azure Cosmos DB](https://docs.microsoft.com/azure/cosmos-db/) service + +## Running the samples + +### Quick steps: + +1. Start the Cosmos DB emulator +2. Follow the steps in [../dev.md](../dev.md) to build the SDK. +3. `cd` into a given sample's directory +4. `npm start` + +### Debugging + +These samples were built using [VS Code](https://code.visualstudio.com) and includes a `.vscode/launch.json`. However, you do not _need_ anything other than Node.js to run these samples. Just run the app.js in your choice of editor or terminal. + +To debug in VS Code, just use the "Debug File" option, and start it in the sample's app.js of your choice. (For the TodoApp, you need to start from `bin/www`) + +### Cosmos Account + +Before you can run any of the samples you do need an active Azure Cosmos DB account or the emulator. +Head over to [How to create a Azure Cosmos DB database account](https://docs.microsoft.com/azure/cosmos-db/create-sql-api-nodejs#create-a-database-account) and see how to setup your account. Check out the emulator (windows only at the moment) [here](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator). + +## Description + +Azure Cosmos DB is a fully managed, scalable, query-able, schema free JSON document database service built for modern applications and delivered to you by Microsoft. + +These samples demonstrate how to use the Client SDKs to interact with the service. + +- **CollectionManagement** - CRUD operations on DocumentCollection resources. + +- **DatabaseManagent** - CRUD operations on Database resources. + +- **DocumentManagement** - CRUD operations on Document resources. + +- **IndexManagement** - shows samples on how to customize the Indexing Policy for a Collection should you need to. + +- **Partitioning** - shows samples on using the provided hashPartitionResolver and rangePartitionResolver classes, and how to implement custom resolvers. + +- **ServerSideScripts** - shows how to create, and execute, server-side stored procedures, triggers and user-defined functions. + +- **TodoApp** - Quick and simple todo app. + +After walking through these samples you should have a good idea of how to get going and how to make use of the various Azure Cosmos DB APIs. + +There are step-by-step tutorials and more documentation on the [Azure Cosmos DB documentation](https://docs.microsoft.com/azure/cosmos-db/) page so head over about this NoSQL document database. + +## More information + +For more information on this database service, please refer to the [Azure Cosmos DB](https://azure.microsoft.com/services/cosmos-db/) service page. diff --git a/sdk/cosmosdb/cosmos/src/ChangeFeedIterator.ts b/sdk/cosmosdb/cosmos/src/ChangeFeedIterator.ts new file mode 100644 index 000000000000..62163d591194 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/ChangeFeedIterator.ts @@ -0,0 +1,142 @@ +/// +import { ChangeFeedOptions } from "./ChangeFeedOptions"; +import { ChangeFeedResponse } from "./ChangeFeedResponse"; +import { Resource } from "./client"; +import { ClientContext } from "./ClientContext"; +import { Constants, ResourceType, StatusCodes } from "./common"; +import { FeedOptions } from "./request"; +import { Response } from "./request"; + +/** + * Provides iterator for change feed. + * + * Use `Items.readChangeFeed()` to get an instance of the iterator. + */ +export class ChangeFeedIterator { + private static readonly IfNoneMatchAllHeaderValue = "*"; + private nextIfNoneMatch: string; + private ifModifiedSince: string; + private lastStatusCode: number; + private isPartitionSpecified: boolean; + + /** + * @internal + * @hidden + * + * @param clientContext + * @param resourceId + * @param resourceLink + * @param isPartitionedContainer + * @param changeFeedOptions + */ + constructor( + private clientContext: ClientContext, + private resourceId: string, + private resourceLink: string, + private partitionKey: string | number | boolean, + private isPartitionedContainer: () => Promise, + private changeFeedOptions: ChangeFeedOptions + ) { + // partition key XOR partition key range id + const partitionKeyValid = partitionKey !== undefined; + this.isPartitionSpecified = partitionKeyValid; + + let canUseStartFromBeginning = true; + if (changeFeedOptions.continuation) { + this.nextIfNoneMatch = changeFeedOptions.continuation; + canUseStartFromBeginning = false; + } + + if (changeFeedOptions.startTime) { + // .toUTCString() is platform specific, but most platforms use RFC 1123. + // In ECMAScript 2018, this was standardized to RFC 1123. + // See for more info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toUTCString + this.ifModifiedSince = changeFeedOptions.startTime.toUTCString(); + canUseStartFromBeginning = false; + } + + if (canUseStartFromBeginning && !changeFeedOptions.startFromBeginning) { + this.nextIfNoneMatch = ChangeFeedIterator.IfNoneMatchAllHeaderValue; + } + } + + /** + * Gets a value indicating whether there are potentially additional results that can be retrieved. + * + * Initially returns true. This value is set based on whether the last execution returned a continuation token. + * + * @returns Boolean value representing if whether there are potentially additional results that can be retrieved. + */ + get hasMoreResults(): boolean { + return this.lastStatusCode !== StatusCodes.NotModified; + } + + /** + * Gets an async iterator which will yield pages of results from Azure Cosmos DB. + */ + public async *getAsyncIterator(): AsyncIterable>> { + do { + const result = await this.executeNext(); + if (result.count > 0) { + yield result; + } + } while (this.hasMoreResults); + } + + /** + * Read feed and retrieves the next page of results in Azure Cosmos DB. + */ + public async executeNext(): Promise>> { + const response = await this.getFeedResponse(); + this.lastStatusCode = response.statusCode; + this.nextIfNoneMatch = response.headers[Constants.HttpHeaders.ETag]; + return response; + } + + private async getFeedResponse(): Promise>> { + const isParittionedContainer = await this.isPartitionedContainer(); + if (!this.isPartitionSpecified && isParittionedContainer) { + throw new Error("Container is partitioned, but no partition key or partition key range id was specified."); + } + const feedOptions: FeedOptions = { initialHeaders: {}, a_im: "Incremental feed" }; + + if (typeof this.changeFeedOptions.maxItemCount === "number") { + feedOptions.maxItemCount = this.changeFeedOptions.maxItemCount; + } + + if (this.changeFeedOptions.sessionToken) { + feedOptions.sessionToken = this.changeFeedOptions.sessionToken; + } + + if (this.nextIfNoneMatch) { + feedOptions.accessCondition = { + type: Constants.HttpHeaders.IfNoneMatch, + condition: this.nextIfNoneMatch + }; + } + + if (this.ifModifiedSince) { + feedOptions.initialHeaders[Constants.HttpHeaders.IfModifiedSince] = this.ifModifiedSince; + } + + if (this.partitionKey !== undefined) { + feedOptions.partitionKey = this.partitionKey as any; // TODO: our partition key is too restrictive on the main object + } + + const response: Response> = await (this.clientContext.queryFeed( + this.resourceLink, + ResourceType.item, + this.resourceId, + result => (result ? result.Documents : []), + undefined, + feedOptions + ) as Promise); // TODO: some funky issues with query feed. Probably need to change it up. + + return new ChangeFeedResponse( + response.result, + response.result ? response.result.length : 0, + response.statusCode, + response.headers + ); + } +} diff --git a/sdk/cosmosdb/cosmos/src/ChangeFeedOptions.ts b/sdk/cosmosdb/cosmos/src/ChangeFeedOptions.ts new file mode 100644 index 000000000000..8639a5151744 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/ChangeFeedOptions.ts @@ -0,0 +1,34 @@ +/** + * Specifies options for the change feed + * + * Some of these options control where and when to start reading from the change feed. The order of precedence is: + * - continuation + * - startTime + * - startFromBeginning + * + * If none of those options are set, it will start reading changes from the first `ChangeFeedIterator.executeNext()` call. + */ +export interface ChangeFeedOptions { + /** + * Max amount of items to return per page + */ + maxItemCount?: number; + /** + * The continuation token to start from. + * + * This is equivalent to the etag and continuation value from the `ChangeFeedResponse` + */ + continuation?: string; + /** + * The session token to use. If not specified, will use the most recent captured session token to start with. + */ + sessionToken?: string; + /** + * Signals whether to start from the beginning or not. + */ + startFromBeginning?: boolean; + /** + * Specified the start time to start reading changes from. + */ + startTime?: Date; +} diff --git a/sdk/cosmosdb/cosmos/src/ChangeFeedResponse.ts b/sdk/cosmosdb/cosmos/src/ChangeFeedResponse.ts new file mode 100644 index 000000000000..773f3b370fe6 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/ChangeFeedResponse.ts @@ -0,0 +1,83 @@ +import { Constants } from "./common"; +import { IHeaders } from "./queryExecutionContext"; + +/** + * A single response page from the Azure Cosmos DB Change Feed + */ +export class ChangeFeedResponse { + /** + * @internal + * @hidden + * + * @param result + * @param count + * @param statusCode + * @param headers + */ + constructor( + /** + * Gets the items returned in the response from Azure Cosmos DB + */ + public readonly result: T, + /** + * Gets the number of items returned in the response from Azure Cosmos DB + */ + public readonly count: number, + /** + * Gets the status code of the response from Azure Cosmos DB + */ + public readonly statusCode: number, + headers: IHeaders + ) { + this.headers = Object.freeze(headers); + } + + /** + * Gets the request charge for this request from the Azure Cosmos DB service. + */ + public get requestCharge(): number { + const rus = this.headers[Constants.HttpHeaders.RequestCharge]; + return rus ? parseInt(rus, 10) : null; + } + + /** + * Gets the activity ID for the request from the Azure Cosmos DB service. + */ + public get activityId(): string { + return this.headers[Constants.HttpHeaders.ActivityId]; + } + + /** + * Gets the continuation token to be used for continuing enumeration of the Azure Cosmos DB service. + * + * This is equivalent to the `etag` property. + */ + public get continuation(): string { + return this.etag; + } + + /** + * Gets the session token for use in session consistency reads from the Azure Cosmos DB service. + */ + public get sessionToken(): string { + return this.headers[Constants.HttpHeaders.SessionToken]; + } + + /** + * Gets the entity tag associated with last transaction in the Azure Cosmos DB service, + * which can be used as If-Non-Match Access condition for ReadFeed REST request or + * `continuation` property of `ChangeFeedOptions` parameter for + * `Items.readChangeFeed()` + * to get feed changes since the transaction specified by this entity tag. + * + * This is equivalent to the `continuation` property. + */ + public get etag(): string { + return this.headers[Constants.HttpHeaders.ETag]; + } + + /** + * Response headers of the response from Azure Cosmos DB + */ + public headers: IHeaders; +} diff --git a/sdk/cosmosdb/cosmos/src/ClientContext.ts b/sdk/cosmosdb/cosmos/src/ClientContext.ts new file mode 100644 index 000000000000..7723d7934961 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/ClientContext.ts @@ -0,0 +1,548 @@ +import { Constants, CosmosClientOptions, IHeaders, QueryIterator, RequestOptions, Response, SqlQuerySpec } from "."; +import { PartitionKeyRange } from "./client/Container/PartitionKeyRange"; +import { Resource } from "./client/Resource"; +import { Helper, StatusCodes, SubStatusCodes } from "./common"; +import { ConnectionPolicy, ConsistencyLevel, DatabaseAccount, QueryCompatibilityMode } from "./documents"; +import { GlobalEndpointManager } from "./globalEndpointManager"; +import { FetchFunctionCallback } from "./queryExecutionContext"; +import { FeedOptions, RequestHandler } from "./request"; +import { ErrorResponse, getHeaders } from "./request/request"; +import { RequestContext } from "./request/RequestContext"; +import { SessionContainer } from "./session/sessionContainer"; +import { SessionContext } from "./session/SessionContext"; + +/** + * @hidden + * @ignore + */ +export class ClientContext { + private readonly sessionContainer: SessionContainer; + private connectionPolicy: ConnectionPolicy; + private requestHandler: RequestHandler; + + public partitionKeyDefinitionCache: { [containerUrl: string]: any }; // TODO: ParitionKeyDefinitionCache + public constructor( + private cosmosClientOptions: CosmosClientOptions, + private globalEndpointManager: GlobalEndpointManager + ) { + this.connectionPolicy = Helper.parseConnectionPolicy(cosmosClientOptions.connectionPolicy); + this.sessionContainer = new SessionContainer(); + this.requestHandler = new RequestHandler( + globalEndpointManager, + this.connectionPolicy, + this.cosmosClientOptions.agent + ); + this.partitionKeyDefinitionCache = {}; + } + /** @ignore */ + public async read( + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise> { + try { + const requestHeaders = await getHeaders( + this.cosmosClientOptions.auth, + { ...initialHeaders, ...this.cosmosClientOptions.defaultHeaders, ...(options && options.initialHeaders) }, + "get", + path, + id, + type, + options, + undefined, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + this.applySessionToken(path, requestHeaders); + + const request: any = { + // TODO: any + path, + operationType: Constants.OperationTypes.Read, + client: this, + endpointOverride: null + }; + // read will use ReadEndpoint since it uses GET operation + const endpoint = await this.globalEndpointManager.resolveServiceEndpoint(request); + const response = await this.requestHandler.get(endpoint, request, requestHeaders); + this.captureSessionToken(undefined, path, Constants.OperationTypes.Read, response.headers); + return response; + } catch (err) { + this.captureSessionToken(err, path, Constants.OperationTypes.Upsert, (err as ErrorResponse).headers); + throw err; + } + } + + public async queryFeed( + path: string, + type: string, // TODO: code smell: enum? + id: string, + resultFn: (result: { [key: string]: any }) => any[], // TODO: any + query: SqlQuerySpec | string, + options: FeedOptions, + partitionKeyRangeId?: string + ): Promise> { + // Query operations will use ReadEndpoint even though it uses + // GET(for queryFeed) and POST(for regular query operations) + + const request: any = { + // TODO: any request + path, + operationType: Constants.OperationTypes.Query, + client: this, + endpointOverride: null + }; + + const endpoint = await this.globalEndpointManager.resolveServiceEndpoint(request); + + const initialHeaders = { ...this.cosmosClientOptions.defaultHeaders, ...(options && options.initialHeaders) }; + if (query === undefined) { + const reqHeaders = await getHeaders( + this.cosmosClientOptions.auth, + initialHeaders, + "get", + path, + id, + type, + options, + partitionKeyRangeId, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + this.applySessionToken(path, reqHeaders); + + const response = await this.requestHandler.get(endpoint, request, reqHeaders); + this.captureSessionToken(undefined, path, Constants.OperationTypes.Query, response.headers); + return this.processQueryFeedResponse(response, !!query, resultFn); + } else { + initialHeaders[Constants.HttpHeaders.IsQuery] = "true"; + switch (this.cosmosClientOptions.queryCompatibilityMode) { + case QueryCompatibilityMode.SqlQuery: + initialHeaders[Constants.HttpHeaders.ContentType] = Constants.MediaTypes.SQL; + break; + case QueryCompatibilityMode.Query: + case QueryCompatibilityMode.Default: + default: + if (typeof query === "string") { + query = { query }; // Converts query text to query object. + } + initialHeaders[Constants.HttpHeaders.ContentType] = Constants.MediaTypes.QueryJson; + break; + } + + const reqHeaders = await getHeaders( + this.cosmosClientOptions.auth, + initialHeaders, + "post", + path, + id, + type, + options, + partitionKeyRangeId, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + this.applySessionToken(path, reqHeaders); + + const response = await this.requestHandler.post(endpoint, request, query, reqHeaders); + this.captureSessionToken(undefined, path, Constants.OperationTypes.Query, response.headers); + return this.processQueryFeedResponse(response, !!query, resultFn); + } + } + + public queryPartitionKeyRanges(collectionLink: string, query?: string | SqlQuerySpec, options?: FeedOptions) { + const path = Helper.getPathFromLink(collectionLink, "pkranges"); + const id = Helper.getIdFromLink(collectionLink); + const cb: FetchFunctionCallback = innerOptions => { + return this.queryFeed(path, "pkranges", id, result => result.PartitionKeyRanges, query, innerOptions); + }; + return new QueryIterator(this, query, options, cb); + } + + public async delete( + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise> { + try { + const reqHeaders = await getHeaders( + this.cosmosClientOptions.auth, + { ...initialHeaders, ...this.cosmosClientOptions.defaultHeaders, ...(options && options.initialHeaders) }, + "delete", + path, + id, + type, + options, + undefined, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + + const request: RequestContext = { + client: this, + operationType: Constants.OperationTypes.Delete, + path, + resourceType: type + }; + + this.applySessionToken(path, reqHeaders); + // deleteResource will use WriteEndpoint since it uses DELETE operation + const endpoint = await this.globalEndpointManager.resolveServiceEndpoint(request); + const response = await this.requestHandler.delete(endpoint, request, reqHeaders); + if (Helper.parseLink(path).type !== "colls") { + this.captureSessionToken(undefined, path, Constants.OperationTypes.Delete, response.headers); + } else { + this.clearSessionToken(path); + } + return response; + } catch (err) { + this.captureSessionToken(err, path, Constants.OperationTypes.Upsert, (err as ErrorResponse).headers); + throw err; + } + } + + // Most cases, things return the definition + the system resource props + public async create( + body: T, + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise>; + + // But a few cases, like permissions, there is additional junk added to the response that isn't in system resource props + public async create( + body: T, + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise>; + public async create( + body: T, + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise> { + try { + const requestHeaders = await getHeaders( + this.cosmosClientOptions.auth, + { ...initialHeaders, ...this.cosmosClientOptions.defaultHeaders, ...(options && options.initialHeaders) }, + "post", + path, + id, + type, + options, + undefined, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + + const request: RequestContext = { + client: this, + operationType: Constants.OperationTypes.Create, + path, + resourceType: type + }; + + // create will use WriteEndpoint since it uses POST operation + this.applySessionToken(path, requestHeaders); + + const endpoint = await this.globalEndpointManager.resolveServiceEndpoint(request); + const response = await this.requestHandler.post(endpoint, request, body, requestHeaders); + this.captureSessionToken(undefined, path, Constants.OperationTypes.Create, response.headers); + return response; + } catch (err) { + this.captureSessionToken(err, path, Constants.OperationTypes.Upsert, (err as ErrorResponse).headers); + throw err; + } + } + + private processQueryFeedResponse( + res: Response, + isQuery: boolean, + resultFn: (result: { [key: string]: any }) => any[] + ): Response { + if (isQuery) { + return { result: resultFn(res.result), headers: res.headers, statusCode: res.statusCode }; + } else { + const newResult = resultFn(res.result).map((body: any) => body); + return { result: newResult, headers: res.headers, statusCode: res.statusCode }; + } + } + + private applySessionToken(path: string, reqHeaders: IHeaders) { + const request = this.getSessionParams(path); + + if (reqHeaders && reqHeaders[Constants.HttpHeaders.SessionToken]) { + return; + } + + const sessionConsistency: ConsistencyLevel = reqHeaders[Constants.HttpHeaders.ConsistencyLevel]; + if (!sessionConsistency) { + return; + } + + if (sessionConsistency !== ConsistencyLevel.Session) { + return; + } + + if (request.resourceAddress) { + const sessionToken = this.sessionContainer.get(request); + if (sessionToken) { + reqHeaders[Constants.HttpHeaders.SessionToken] = sessionToken; + } + } + } + + public async replace( + resource: any, + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise> { + try { + const reqHeaders = await getHeaders( + this.cosmosClientOptions.auth, + { ...initialHeaders, ...this.cosmosClientOptions.defaultHeaders, ...(options && options.initialHeaders) }, + "put", + path, + id, + type, + options, + undefined, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + + const request: RequestContext = { + client: this, + operationType: Constants.OperationTypes.Replace, + path, + resourceType: type + }; + + this.applySessionToken(path, reqHeaders); + + // replace will use WriteEndpoint since it uses PUT operation + const endpoint = await this.globalEndpointManager.resolveServiceEndpoint(reqHeaders); + const response = await this.requestHandler.put(endpoint, request, resource, reqHeaders); + this.captureSessionToken(undefined, path, Constants.OperationTypes.Replace, response.headers); + return response; + } catch (err) { + this.captureSessionToken(err, path, Constants.OperationTypes.Upsert, (err as ErrorResponse).headers); + throw err; + } + } + + public async upsert( + body: T, + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise>; + public async upsert( + body: T, + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise>; + public async upsert( + body: T, + path: string, + type: string, + id: string, + initialHeaders: IHeaders, + options?: RequestOptions + ): Promise> { + try { + const requestHeaders = await getHeaders( + this.cosmosClientOptions.auth, + { ...initialHeaders, ...this.cosmosClientOptions.defaultHeaders, ...(options && options.initialHeaders) }, + "post", + path, + id, + type, + options, + undefined, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + + const request: RequestContext = { + client: this, + operationType: Constants.OperationTypes.Upsert, + path, + resourceType: type + }; + + Helper.setIsUpsertHeader(requestHeaders); + this.applySessionToken(path, requestHeaders); + + // upsert will use WriteEndpoint since it uses POST operation + const endpoint = await this.globalEndpointManager.resolveServiceEndpoint(request); + const response = await this.requestHandler.post(endpoint, request, body, requestHeaders); + this.captureSessionToken(undefined, path, Constants.OperationTypes.Upsert, response.headers); + return response; + } catch (err) { + this.captureSessionToken(err, path, Constants.OperationTypes.Upsert, (err as ErrorResponse).headers); + throw err; + } + } + + public async execute( + sprocLink: string, + params?: any[], // TODO: any + options?: RequestOptions + ): Promise> { + const initialHeaders = { ...this.cosmosClientOptions.defaultHeaders, ...(options && options.initialHeaders) }; + + // Accept a single parameter or an array of parameters. + // Didn't add type annotation for this because we should legacy this behavior + if (params !== null && params !== undefined && !Array.isArray(params)) { + params = [params]; + } + const path = Helper.getPathFromLink(sprocLink); + const id = Helper.getIdFromLink(sprocLink); + + const headers = await getHeaders( + this.cosmosClientOptions.auth, + initialHeaders, + "post", + path, + id, + "sprocs", + options, + undefined, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + + const request: RequestContext = { + client: this, + operationType: Constants.OperationTypes.Execute, + path, + resourceType: "sprocs" + }; + + // executeStoredProcedure will use WriteEndpoint since it uses POST operation + const endpoint = await this.globalEndpointManager.resolveServiceEndpoint(request); + return this.requestHandler.post(endpoint, request, params, headers); + } + + /** + * Gets the Database account information. + * @param {string} [options.urlConnection] - The endpoint url whose database account needs to be retrieved. \ + * If not present, current client's url will be used. + */ + public async getDatabaseAccount(options: RequestOptions = {}): Promise> { + const urlConnection = options.urlConnection || this.cosmosClientOptions.endpoint; + + const requestHeaders = await getHeaders( + this.cosmosClientOptions.auth, + this.cosmosClientOptions.defaultHeaders, + "get", + "", + "", + "", + {}, + undefined, + this.cosmosClientOptions.connectionPolicy.UseMultipleWriteLocations + ); + + const request: RequestContext = { + client: this, + operationType: Constants.OperationTypes.Read, + path: "", + resourceType: "DatabaseAccount" + }; + + const { result, headers } = await this.requestHandler.get(urlConnection, request, requestHeaders); + + const databaseAccount = new DatabaseAccount(result, headers); + + return { result: databaseAccount, headers }; + } + + public getWriteEndpoint(): Promise { + return this.globalEndpointManager.getWriteEndpoint(); + } + + public getReadEndpoint(): Promise { + return this.globalEndpointManager.getReadEndpoint(); + } + + private captureSessionToken(err: ErrorResponse, path: string, opType: string, resHeaders: IHeaders) { + const request = this.getSessionParams(path); // TODO: any request + request.operationType = opType; + if ( + !err || + (!this.isMasterResource(request.resourceType) && + (err.code === StatusCodes.PreconditionFailed || + err.code === StatusCodes.Conflict || + (err.code === StatusCodes.NotFound && err.substatus !== SubStatusCodes.ReadSessionNotAvailable))) + ) { + this.sessionContainer.set(request, resHeaders); + } + } + + // TODO: some session tests are using this, but I made them use type coercsion to call this method because I don't think it should be public. + private getSessionToken(collectionLink: string) { + if (!collectionLink) { + throw new Error("collectionLink cannot be null"); + } + + const paths = Helper.parseLink(collectionLink); + + if (paths === undefined) { + return ""; + } + + const request = this.getSessionParams(collectionLink); + return this.sessionContainer.get(request); + } + + public clearSessionToken(path: string) { + const request = this.getSessionParams(path); + this.sessionContainer.remove(request); + } + + private getSessionParams(resourceLink: string): SessionContext { + const resourceId: string = null; + let resourceAddress: string = null; + const parserOutput = Helper.parseLink(resourceLink); + + resourceAddress = parserOutput.objectBody.self; + + const resourceType = parserOutput.type; + return { + resourceId, + resourceAddress, + resourceType, + isNameBased: true + }; + } + + private isMasterResource(resourceType: string): boolean { + if ( + resourceType === Constants.Path.OffersPathSegment || + resourceType === Constants.Path.DatabasesPathSegment || + resourceType === Constants.Path.UsersPathSegment || + resourceType === Constants.Path.PermissionsPathSegment || + resourceType === Constants.Path.TopologyPathSegment || + resourceType === Constants.Path.DatabaseAccountPathSegment || + resourceType === Constants.Path.PartitionKeyRangesPathSegment || + resourceType === Constants.Path.CollectionsPathSegment + ) { + return true; + } + + return false; + } +} diff --git a/sdk/cosmosdb/cosmos/src/CosmosClient.ts b/sdk/cosmosdb/cosmos/src/CosmosClient.ts new file mode 100644 index 000000000000..b69e14000e14 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/CosmosClient.ts @@ -0,0 +1,168 @@ +import { Agent, AgentOptions } from "https"; +import * as tunnel from "tunnel"; +import * as url from "url"; +import { Constants, RequestOptions } from "."; +import { Database, Databases } from "./client/Database"; +import { Offer, Offers } from "./client/Offer"; +import { ClientContext } from "./ClientContext"; +import { Helper, Platform } from "./common"; +import { CosmosClientOptions } from "./CosmosClientOptions"; +import { DatabaseAccount } from "./documents"; +import { GlobalEndpointManager } from "./globalEndpointManager"; +import { CosmosResponse } from "./request"; + +/** + * Provides a client-side logical representation of the Azure Cosmos DB database account. + * This client is used to configure and execute requests in the Azure Cosmos DB database service. + * @example Instantiate a client and create a new database + * ```typescript + * const client = new CosmosClient({endpoint: "", auth: {masterKey: ""}}); + * await client.databases.create({id: ""}); + * ``` + * @example Instantiate a client with custom Connection Policy + * ```typescript + * const connectionPolicy = new ConnectionPolicy(); + * connectionPolicy.RequestTimeout = 10000; + * const client = new CosmosClient({ + * endpoint: "", + * auth: {masterKey: ""}, + * connectionPolicy + * }); + * ``` + */ +export class CosmosClient { + /** + * Used for creating new databases, or querying/reading all databases. + * + * Use `.database(id)` to read, replace, or delete a specific, existing database by id. + * + * @example Create a new database + * ```typescript + * const {body: databaseDefinition, database} = await client.databases.create({id: ""}); + * ``` + */ + public readonly databases: Databases; + /** + * Used for querying & reading all offers. + * + * Use `.offer(id)` to read, or replace existing offers. + */ + public readonly offers: Offers; + /** + * Creates a new {@link CosmosClient} object. See {@link CosmosClientOptions} for more details on what options you can use. + * @param options bag of options - require at least endpoint and auth to be configured + */ + + private clientContext: ClientContext; + constructor(private options: CosmosClientOptions) { + options.auth = options.auth || {}; + if (options.key) { + options.auth.key = options.key; + } + + options.connectionPolicy = Helper.parseConnectionPolicy(options.connectionPolicy); + + options.defaultHeaders = options.defaultHeaders || {}; + options.defaultHeaders[Constants.HttpHeaders.CacheControl] = "no-cache"; + options.defaultHeaders[Constants.HttpHeaders.Version] = Constants.CurrentVersion; + if (options.consistencyLevel !== undefined) { + options.defaultHeaders[Constants.HttpHeaders.ConsistencyLevel] = options.consistencyLevel; + } + + const platformDefaultHeaders = Platform.getPlatformDefaultHeaders() || {}; + for (const platformDefaultHeader of Object.keys(platformDefaultHeaders)) { + options.defaultHeaders[platformDefaultHeader] = platformDefaultHeaders[platformDefaultHeader]; + } + + options.defaultHeaders[Constants.HttpHeaders.UserAgent] = Platform.getUserAgent(); + + if (!this.options.agent) { + // Initialize request agent + const requestAgentOptions: AgentOptions & tunnel.HttpsOverHttpsOptions & tunnel.HttpsOverHttpOptions = { + keepAlive: true + }; + if (!!this.options.connectionPolicy.ProxyUrl) { + const proxyUrl = url.parse(this.options.connectionPolicy.ProxyUrl); + const port = parseInt(proxyUrl.port, 10); + requestAgentOptions.proxy = { + host: proxyUrl.hostname, + port, + headers: {} + }; + + if (!!proxyUrl.auth) { + requestAgentOptions.proxy.proxyAuth = proxyUrl.auth; + } + + this.options.agent = + proxyUrl.protocol.toLowerCase() === "https:" + ? tunnel.httpsOverHttps(requestAgentOptions) + : tunnel.httpsOverHttp(requestAgentOptions); // TODO: type coersion + } else { + this.options.agent = new Agent(requestAgentOptions); // TODO: Move to request? + } + } + + const globalEndpointManager = new GlobalEndpointManager(this.options, async (opts: RequestOptions) => + this.getDatabaseAccount(opts) + ); + this.clientContext = new ClientContext(options, globalEndpointManager); + + this.databases = new Databases(this, this.clientContext); + this.offers = new Offers(this, this.clientContext); + } + + /** + * Get information about the current {@link DatabaseAccount} (including which regions are supported, etc.) + */ + public async getDatabaseAccount(options?: RequestOptions): Promise> { + const response = await this.clientContext.getDatabaseAccount(options); + return { body: response.result, headers: response.headers, ref: this }; + } + + /** + * Gets the currently used write endpoint url. Useful for troubleshooting purposes. + * + * The url may contain a region suffix (e.g. "-eastus") if we're using location specific endpoints. + */ + public getWriteEndpoint(): Promise { + return this.clientContext.getWriteEndpoint(); + } + + /** + * Gets the currently used read endpoint. Useful for troubleshooting purposes. + * + * The url may contain a region suffix (e.g. "-eastus") if we're using location specific endpoints. + */ + public getReadEndpoint(): Promise { + return this.clientContext.getReadEndpoint(); + } + + /** + * Used for reading, updating, or deleting a existing database by id or accessing containers belonging to that database. + * + * This does not make a network call. Use `.read` to get info about the database after getting the {@link Database} object. + * + * @param id The id of the database. + * @example Create a new container off of an existing database + * ```typescript + * const container = client.database("").containers.create(""); + * ``` + * + * @example Delete an existing database + * ```typescript + * await client.database("").delete(); + * ``` + */ + public database(id: string): Database { + return new Database(this, id, this.clientContext); + } + + /** + * Used for reading, or updating a existing offer by id. + * @param id The id of the offer. + */ + public offer(id: string) { + return new Offer(this, id, this.clientContext); + } +} diff --git a/sdk/cosmosdb/cosmos/src/CosmosClientOptions.ts b/sdk/cosmosdb/cosmos/src/CosmosClientOptions.ts new file mode 100644 index 000000000000..f5e170cb0c79 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/CosmosClientOptions.ts @@ -0,0 +1,32 @@ +import { AuthOptions } from "./auth"; +import { ConnectionPolicy, ConsistencyLevel, QueryCompatibilityMode } from "./documents"; +import { IHeaders } from "./queryExecutionContext/IHeaders"; + +// We expose our own Agent interface to avoid taking a dependency on and leaking node types. This interface should mirror the node Agent interface +interface Agent { + maxFreeSockets: number; + maxSockets: number; + sockets: any; + requests: any; + destroy(): void; +} + +export interface CosmosClientOptions { + /** The service endpoint to use to create the client. */ + endpoint: string; + /** The account master or readonly key (alias of auth.key) */ + key?: string; + /** An object that is used for authenticating requests and must contains one of the options */ + auth?: AuthOptions; + /** An instance of {@link ConnectionPolicy} class. + * This parameter is optional and the default connectionPolicy will be used if omitted. + */ + connectionPolicy?: ConnectionPolicy | { [P in keyof ConnectionPolicy]?: ConnectionPolicy[P] }; + /** An optional parameter that represents the consistency level. + * It can take any value from {@link ConsistencyLevel}. + */ + consistencyLevel?: keyof typeof ConsistencyLevel; + defaultHeaders?: IHeaders; + agent?: Agent; + queryCompatibilityMode?: QueryCompatibilityMode; +} diff --git a/sdk/cosmosdb/cosmos/src/LocationCache.ts b/sdk/cosmosdb/cosmos/src/LocationCache.ts new file mode 100644 index 000000000000..09b96668a57d --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/LocationCache.ts @@ -0,0 +1,361 @@ +import { Constants, Helper, ResourceType } from "./common"; +import { CosmosClientOptions } from "./CosmosClientOptions"; +import { DatabaseAccount, Location } from "./documents"; +import { LocationInfo } from "./LocationInfo"; +import { LocationRouting } from "./request/LocationRouting"; +import { RequestContext } from "./request/RequestContext"; + +/** + * @private + * @hidden + */ +enum EndpointOperationType { + None = "None", + Read = "Read", + Write = "Write" +} + +/** + * @private + * @hidden + */ +interface LocationUnavailabilityInfo { + lastUnavailablityCheckTimeStamp: Date; + operationTypes: Set; +} + +/** + * Implements the abstraction to resolve target location for geo-replicated Database Account + * with multiple writable and readable locations. + * @private + * @hidden + */ +export class LocationCache { + private locationUnavailabilityInfoByEndpoint: Map = new Map(); + private locationInfo: LocationInfo; + private lastCacheUpdateTimestamp: Date = new Date(0); + private defaultEndpoint: string; + private enableMultipleWritableLocations: boolean; + + public constructor(private options: CosmosClientOptions) { + this.defaultEndpoint = options.endpoint; + this.locationInfo = new LocationInfo(options.connectionPolicy.PreferredLocations, options.endpoint); + } + + public get prefferredLocations(): string[] { + return this.options.connectionPolicy.PreferredLocations; + } + + public getWriteEndpoint(): string { + return this.getWriteEndpoints()[0]; + } + + public getReadEndpoint(): string { + return this.getReadEndpoints()[0]; + } + + /** + * Gets list of write endpoints ordered by + * 1. Preferred location + * 2. Endpoint availability + */ + public getWriteEndpoints(): ReadonlyArray { + if (this.locationUnavailabilityInfoByEndpoint.size > 0 && this.canUpdateCache(this.lastCacheUpdateTimestamp)) { + this.updateLocationCache(); + } + return this.locationInfo.writeEndpoints; + } + + /** + * Gets list of read endpoints ordered by + * 1. Preferred location + * 2. Endpoint availability + */ + public getReadEndpoints(): ReadonlyArray { + if (this.locationUnavailabilityInfoByEndpoint.size > 0 && this.canUpdateCache(this.lastCacheUpdateTimestamp)) { + this.updateLocationCache(); + } + return this.locationInfo.readEndpoints; + } + + public markCurrentLocationUnavailableForRead(endpoint: string) { + this.markEndpointUnavailable(endpoint, EndpointOperationType.Read); + } + + public markCurrentLocationUnavailableForWrite(endpoint: string) { + this.markEndpointUnavailable(endpoint, EndpointOperationType.Write); + } + + /** + * Invoked when {@link DatabaseAccount} is read + * @param databaseAccount The DatabaseAccount read + */ + public onDatabaseAccountRead(databaseAccount: DatabaseAccount) { + this.updateLocationCache( + databaseAccount.writableLocations, + databaseAccount.readableLocations, + databaseAccount.enableMultipleWritableLocations + ); + } + + public resolveServiceEndpoint(request: RequestContext): string { + request.locationRouting = request.locationRouting || new LocationRouting(); + + let locationIndex = request.locationRouting.locationIndexToRoute || 0; + + if (!this.options.connectionPolicy.EnableEndpointDiscovery) { + return this.defaultEndpoint; + } + + if (request.locationRouting.locationEndpointToRoute) { + return request.locationRouting.locationEndpointToRoute; + } + + // If we're ignoring preferred locations, or if it's a write request that can't use multiple locations + // then default to the first two write locations, alternating (or the default endpoint) + if ( + request.locationRouting.ignorePreferredLocation || + (!Helper.isReadRequest(request) && !this.canUseMultipleWriteLocations(request)) + ) { + const currentInfo = this.locationInfo; + if (currentInfo.orderedWriteLocations.length > 0) { + locationIndex = Math.min(locationIndex % 2, currentInfo.orderedWriteLocations.length - 1); + const writeLocation = currentInfo.orderedWriteLocations[locationIndex]; + return currentInfo.availableWriteEndpointByLocation.get(LocationCache.normalizeLocationName(writeLocation)); + } else { + return this.defaultEndpoint; + } + } else { + // If we're using preferred regions, then choose the correct endpoint based on the location index + const endpoints = Helper.isReadRequest(request) + ? this.locationInfo.readEndpoints + : this.locationInfo.writeEndpoints; + return endpoints[locationIndex % endpoints.length]; + } + } + + public shouldRefreshEndpoints(): { shouldRefresh: boolean; canRefreshInBackground: boolean } { + let canRefreshInBackground = true; + const currentInfo = this.locationInfo; + + const mostPreferredLocation: string = LocationCache.normalizeLocationName( + currentInfo.preferredLocations ? currentInfo.preferredLocations[0] : null + ); + + if (this.options.connectionPolicy.EnableEndpointDiscovery) { + // Refresh if client opts-in to use multiple write locations, but it's not enabled on the server. + const shouldRefresh = + this.options.connectionPolicy.UseMultipleWriteLocations && !this.enableMultipleWritableLocations; + + if (mostPreferredLocation) { + if (currentInfo.availableReadEndpointByLocation.size > 0) { + const mostPreferredReadEndpoint = currentInfo.availableReadEndpointByLocation.get(mostPreferredLocation); + if (mostPreferredReadEndpoint) { + if (mostPreferredReadEndpoint !== currentInfo.readEndpoints[0]) { + return { shouldRefresh: true, canRefreshInBackground }; + } + } else { + return { shouldRefresh: true, canRefreshInBackground }; + } + } + + if (!this.canUseMultipleWriteLocations()) { + if (this.isEndpointUnavailable(currentInfo.writeEndpoints[0], EndpointOperationType.Write)) { + canRefreshInBackground = currentInfo.writeEndpoints.length > 1; + return { shouldRefresh: true, canRefreshInBackground }; + } else { + return { shouldRefresh, canRefreshInBackground }; + } + } else if (mostPreferredLocation) { + const mostPreferredWriteEndpoint = currentInfo.availableWriteEndpointByLocation.get(mostPreferredLocation); + if (mostPreferredWriteEndpoint) { + return { + shouldRefresh: shouldRefresh || mostPreferredWriteEndpoint !== currentInfo.writeEndpoints[0], + canRefreshInBackground + }; + } else { + return { shouldRefresh, canRefreshInBackground }; + } + } + } + } + return { shouldRefresh: false, canRefreshInBackground }; + } + + public canUseMultipleWriteLocations(request?: RequestContext): boolean { + let canUse = this.options.connectionPolicy.UseMultipleWriteLocations && this.enableMultipleWritableLocations; + + if (request) { + canUse = + canUse && + (request.resourceType === ResourceType.item || + (request.resourceType === ResourceType.sproc && request.operationType === Constants.OperationTypes.Execute)); + } + + return canUse; + } + + private clearStaleEndpointUnavailabilityInfo() { + if (this.locationUnavailabilityInfoByEndpoint.size > 0) { + for (const [endpoint, info] of this.locationUnavailabilityInfoByEndpoint.entries()) { + if (info && this.canUpdateCache(info.lastUnavailablityCheckTimeStamp)) { + this.locationUnavailabilityInfoByEndpoint.delete(endpoint); + } + } + } + } + private isEndpointUnavailable(endpoint: string, expectedAvailableOperations: EndpointOperationType) { + const unavailabilityInfo = this.locationUnavailabilityInfoByEndpoint.get(endpoint); + + if ( + expectedAvailableOperations === EndpointOperationType.None || + unavailabilityInfo == null || + !unavailabilityInfo.operationTypes.has(expectedAvailableOperations) + ) { + return false; + } else { + if (this.canUpdateCache(unavailabilityInfo.lastUnavailablityCheckTimeStamp)) { + return false; + } else { + return true; + } + } + } + + private markEndpointUnavailable(unavailableEndpoint: string, unavailableOperationType: EndpointOperationType) { + const unavailabilityInfo = this.locationUnavailabilityInfoByEndpoint.get(unavailableEndpoint); + const now = new Date(Date.now()); + if (unavailabilityInfo == null) { + this.locationUnavailabilityInfoByEndpoint.set(unavailableEndpoint, { + lastUnavailablityCheckTimeStamp: now, + operationTypes: new Set([unavailableOperationType]) + }); + } else { + const unavailableOperations = new Set([unavailableOperationType]); + for (const op of unavailabilityInfo.operationTypes) { + unavailableOperations.add(op); + } + this.locationUnavailabilityInfoByEndpoint.set(unavailableEndpoint, { + lastUnavailablityCheckTimeStamp: now, + operationTypes: unavailableOperations + }); + } + + this.updateLocationCache(); + } + + private updateLocationCache( + writeLocations?: Location[], + readLocations?: Location[], + enableMultipleWritableLocations?: boolean + ) { + if (enableMultipleWritableLocations) { + this.enableMultipleWritableLocations = enableMultipleWritableLocations; + } + + this.clearStaleEndpointUnavailabilityInfo(); + + // TODO: To sstay consistent with .NET, grab a local copy of the locationInfo + + if (this.options.connectionPolicy.EnableEndpointDiscovery) { + if (readLocations) { + ({ + endpointsByLocation: this.locationInfo.availableReadEndpointByLocation, + orderedLocations: this.locationInfo.orderedReadLocations + } = this.getEndpointByLocation(readLocations)); + } + + if (writeLocations) { + ({ + endpointsByLocation: this.locationInfo.availableWriteEndpointByLocation, + orderedLocations: this.locationInfo.orderedWriteLocations + } = this.getEndpointByLocation(writeLocations)); + } + } + + this.locationInfo.writeEndpoints = this.getPreferredAvailableEndpoints( + this.locationInfo.availableWriteEndpointByLocation, + this.locationInfo.orderedWriteLocations, + EndpointOperationType.Write, + this.defaultEndpoint + ); + + this.locationInfo.readEndpoints = this.getPreferredAvailableEndpoints( + this.locationInfo.availableReadEndpointByLocation, + this.locationInfo.orderedReadLocations, + EndpointOperationType.Read, + this.defaultEndpoint + ); + + this.lastCacheUpdateTimestamp = new Date(); + } + + private getPreferredAvailableEndpoints( + endpointsByLocation: ReadonlyMap, + orderedLocations: ReadonlyArray, + expectedAvailableOperation: EndpointOperationType, + fallbackEndpoint: string + ): string[] { + const endpoints = []; + + if (this.options.connectionPolicy.EnableEndpointDiscovery && endpointsByLocation && endpointsByLocation.size > 0) { + if (this.canUseMultipleWriteLocations() || expectedAvailableOperation === EndpointOperationType.Read) { + const unavailableEndpoints: string[] = []; + if (this.options.connectionPolicy.PreferredLocations) { + for (const location of this.options.connectionPolicy.PreferredLocations) { + const endpoint = endpointsByLocation.get(LocationCache.normalizeLocationName(location)); + if (endpoint) { + if (this.isEndpointUnavailable(endpoint, expectedAvailableOperation)) { + unavailableEndpoints.push(endpoint); + } else { + endpoints.push(endpoint); + } + } + } + } + + if (endpoints.length === 0) { + endpoints.push(fallbackEndpoint); + } + } else { + for (const location of orderedLocations) { + const normalizedLocationName = LocationCache.normalizeLocationName(location); + if (endpointsByLocation.has(normalizedLocationName)) { + endpoints.push(endpointsByLocation.get(normalizedLocationName)); + } + } + } + } + + if (endpoints.length === 0) { + endpoints.push(fallbackEndpoint); + } + + return endpoints; + } + + private getEndpointByLocation( + locations: Location[] + ): { endpointsByLocation: Map; orderedLocations: string[] } { + const endpointsByLocation: Map = new Map(); + const orderedLocations: string[] = []; + + for (const location of locations) { + if (!location) { + continue; + } + const normalizedLocationName = LocationCache.normalizeLocationName(location.name); + endpointsByLocation.set(normalizedLocationName, location.databaseAccountEndpoint); + orderedLocations.push(normalizedLocationName); + } + return { endpointsByLocation, orderedLocations }; + } + + private canUpdateCache(timestamp: Date): boolean { + return new Date(Date.now() - Constants.DefaultUnavailableLocationExpirationTimeMS) > timestamp; + } + + private static normalizeLocationName(location: string): string { + return location ? location.toLowerCase().replace(/ /g, "") : null; + } +} diff --git a/sdk/cosmosdb/cosmos/src/LocationInfo.ts b/sdk/cosmosdb/cosmos/src/LocationInfo.ts new file mode 100644 index 000000000000..547bf9ad1a19 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/LocationInfo.ts @@ -0,0 +1,52 @@ +/** + * Used to store the location info in Location Cache + * @private + * @hidden + */ +export class LocationInfo { + public preferredLocations: ReadonlyArray; + public availableReadEndpointByLocation: ReadonlyMap; + public availableWriteEndpointByLocation: ReadonlyMap; + public orderedWriteLocations: ReadonlyArray; + public orderedReadLocations: ReadonlyArray; + public writeEndpoints: ReadonlyArray; + public readEndpoints: ReadonlyArray; + + public constructor(other: LocationInfo); + public constructor(preferredLocations: ReadonlyArray, defaultEndpoint: string); + public constructor( + preferredLocationsOrOtherLocationInfo: ReadonlyArray | LocationInfo, + defaultEndpoint?: string + ) { + let preferredLocations: ReadonlyArray = null; + let other: LocationInfo = null; + if (Array.isArray(preferredLocationsOrOtherLocationInfo)) { + preferredLocations = preferredLocationsOrOtherLocationInfo; + } else if (preferredLocationsOrOtherLocationInfo instanceof LocationInfo) { + other = preferredLocationsOrOtherLocationInfo; + } else { + throw new Error("Invalid type passed to LocationInfo"); + } + + if (preferredLocations && defaultEndpoint) { + this.preferredLocations = preferredLocations; + this.availableWriteEndpointByLocation = new Map(); + this.availableReadEndpointByLocation = new Map(); + this.orderedWriteLocations = []; + this.orderedReadLocations = []; + this.writeEndpoints = [defaultEndpoint]; + this.readEndpoints = [defaultEndpoint]; + } else if (other) { + this.preferredLocations = other.preferredLocations; + this.availableReadEndpointByLocation = other.availableReadEndpointByLocation; + this.availableWriteEndpointByLocation = other.availableWriteEndpointByLocation; + this.orderedReadLocations = other.orderedReadLocations; + this.orderedWriteLocations = other.orderedWriteLocations; + this.writeEndpoints = other.writeEndpoints; + this.readEndpoints = other.readEndpoints; + } else { + // This should never be called + throw new Error("Invalid arguments passed to LocationInfo"); + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/auth.ts b/sdk/cosmosdb/cosmos/src/auth.ts new file mode 100644 index 000000000000..c369a8c00313 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/auth.ts @@ -0,0 +1,173 @@ +import createHmac from "create-hmac"; +import { PermissionDefinition } from "./client"; +import { Helper } from "./common"; +import { IHeaders } from "./queryExecutionContext"; + +/** @hidden */ +export interface IRequestInfo { + [index: string]: any; + verb: string; + path: string; + resourceId: string; + resourceType: string; + headers: IHeaders; +} + +export interface ITokenProvider { + getToken: (requestInfo: IRequestInfo, callback?: (err: Error, token: string) => void) => Promise; +} + +export interface AuthOptions { + /** Account master key or read only key */ + key?: string; + /** The authorization master key to use to create the client. */ + masterKey?: string; + /** An object that contains resources tokens. + * Keys for the object are resource Ids and values are the resource tokens. + */ + resourceTokens?: { [resourcePath: string]: string }; + tokenProvider?: any; // TODO: any + /** An array of {@link Permission} objects. */ + permissionFeed?: PermissionDefinition[]; // TODO: any +} + +/** @hidden */ +export class AuthHandler { + public static async getAuthorizationHeader( + authOptions: AuthOptions, + verb: string, + path: string, + resourceId: string, + resourceType: string, + headers: IHeaders + ): Promise { + if (authOptions.permissionFeed) { + authOptions.resourceTokens = {}; + for (const permission of authOptions.permissionFeed) { + const id = Helper.getResourceIdFromPath(permission.resource); + if (!id) { + throw new Error(`authorization error: ${id} \ + is an invalid resourceId in permissionFeed`); + } + + authOptions.resourceTokens[id] = (permission as any)._token; // TODO: any + } + } + + if (authOptions.masterKey || authOptions.key) { + const key = authOptions.masterKey || authOptions.key; + return encodeURIComponent( + AuthHandler.getAuthorizationTokenUsingMasterKey(verb, resourceId, resourceType, headers, key) + ); + } else if (authOptions.resourceTokens) { + return encodeURIComponent( + AuthHandler.getAuthorizationTokenUsingResourceTokens(authOptions.resourceTokens, path, resourceId) + ); + } else if (authOptions.tokenProvider) { + return encodeURIComponent( + await AuthHandler.getAuthorizationTokenUsingTokenProvider(authOptions.tokenProvider, { + verb, + path, + resourceId, + resourceType, + headers + }) + ); + } + } + + private static getAuthorizationTokenUsingMasterKey( + verb: string, + resourceId: string, + resourceType: string, + headers: IHeaders, + masterKey: string + ) { + if (resourceType === "offers") { + resourceId = resourceId && resourceId.toLowerCase(); + } + const key = Buffer.from(masterKey, "base64"); + + const text = + (verb || "").toLowerCase() + + "\n" + + (resourceType || "").toLowerCase() + + "\n" + + (resourceId || "") + + "\n" + + ((headers["x-ms-date"] as string) || "").toLowerCase() + + "\n" + + ((headers["date"] as string) || "").toLowerCase() + + "\n"; + + const body = Buffer.from(text, "utf8"); + const signature = createHmac("sha256", key) + .update(body) + .digest("base64"); + const MasterToken = "master"; + const TokenVersion = "1.0"; + + return `type=${MasterToken}&ver=${TokenVersion}&sig=${signature}`; + } + + // TODO: Resource tokens + private static getAuthorizationTokenUsingResourceTokens( + resourceTokens: { [resourceId: string]: string }, + path: string, + resourceId: string + ) { + if (resourceTokens && Object.keys(resourceTokens).length > 0) { + // For database account access(through getDatabaseAccount API), path and resourceId are "", + // so in this case we return the first token to be used for creating the auth header as the + // service will accept any token in this case + if (!path && !resourceId) { + return resourceTokens[Object.keys(resourceTokens)[0]]; + } + + if (resourceId && resourceTokens[resourceId]) { + return resourceTokens[resourceId]; + } + + // minimum valid path /dbs + if (!path || path.length < 4) { + return null; + } + + // remove '/' from left and right of path + path = path[0] === "/" ? path.substring(1) : path; + path = path[path.length - 1] === "/" ? path.substring(0, path.length - 1) : path; + + const pathSegments = (path && path.split("/")) || []; + + // if it's an incomplete path like /dbs/db1/colls/, start from the paretn resource + let index = pathSegments.length % 2 === 0 ? pathSegments.length - 1 : pathSegments.length - 2; + for (; index > 0; index -= 2) { + const id = decodeURI(pathSegments[index]); + if (resourceTokens[id]) { + return resourceTokens[id]; + } + } + } + return null; + } + + private static getAuthorizationTokenUsingTokenProvider( + tokenProvider: ITokenProvider, + requestInfo: IRequestInfo + ): Promise { + requestInfo.getAuthorizationTokenUsingMasterKey = AuthHandler.getAuthorizationTokenUsingMasterKey; + return new Promise(async (resolve, reject) => { + const callback = (err: Error, token: string) => { + if (reject) { + return reject(err); + } + resolve(token); + }; + + const results = tokenProvider.getToken(requestInfo, callback); + if (results.then && typeof results.then === "function") { + resolve(await results); + } + }); + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Conflict/Conflict.ts b/sdk/cosmosdb/cosmos/src/client/Conflict/Conflict.ts new file mode 100644 index 000000000000..125fdfe2bf06 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Conflict/Conflict.ts @@ -0,0 +1,54 @@ +import { ClientContext } from "../../ClientContext"; +import { Constants, Helper } from "../../common"; +import { RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { ConflictDefinition } from "./ConflictDefinition"; +import { ConflictResponse } from "./ConflictResponse"; + +/** + * Use to read or delete a given {@link Conflict} by id. + * + * @see {@link Conflicts} to query or read all conflicts. + */ +export class Conflict { + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return `/${this.container.url}/${Constants.Path.ConflictsPathSegment}/${this.id}`; + } + /** + * @hidden + * @param container The parent {@link Container}. + * @param id The id of the given {@link Conflict}. + */ + constructor( + public readonly container: Container, + public readonly id: string, + private readonly clientContext: ClientContext + ) {} + + /** + * Read the {@link ConflictDefinition} for the given {@link Conflict}. + * @param options + */ + public async read(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url, "conflicts"); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.read(path, "users", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, conflict: this }; + } + + /** + * Delete the given {@link ConflictDefinition}. + * @param options + */ + public async delete(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.delete(path, "conflicts", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, conflict: this }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictDefinition.ts b/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictDefinition.ts new file mode 100644 index 000000000000..035bdd6ac429 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictDefinition.ts @@ -0,0 +1,11 @@ +import { ItemDefinition } from "../Item"; + +export interface ConflictDefinition { + /** The id of the conflict */ + id?: string; + /** Source resource id */ + resourceId?: string; + resourceType?: string; + operationType?: string; // TODO: enum + content?: string; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResolutionMode.ts b/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResolutionMode.ts new file mode 100644 index 000000000000..38743fb2e13a --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResolutionMode.ts @@ -0,0 +1,4 @@ +export enum ConflictResolutionMode { + Custom = "Custom", + LastWriterWins = "LastWriterWins" +} diff --git a/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResolutionPolicy.ts b/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResolutionPolicy.ts new file mode 100644 index 000000000000..17acb5c48df2 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResolutionPolicy.ts @@ -0,0 +1,38 @@ +import { ConflictResolutionMode } from "./ConflictResolutionMode"; + +/** + * Represents the conflict resolution policy configuration for specifying how to resolve conflicts + * in case writes from different regions result in conflicts on documents in the collection in the Azure Cosmos DB service. + */ +export interface ConflictResolutionPolicy { + /** + * Gets or sets the in the Azure Cosmos DB service. By default it is {@link ConflictResolutionMode.LastWriterWins}. + */ + mode?: keyof typeof ConflictResolutionMode; + /** + * Gets or sets the path which is present in each document in the Azure Cosmos DB service for last writer wins conflict-resolution. + * This path must be present in each document and must be an integer value. + * In case of a conflict occurring on a document, the document with the higher integer value in the specified path will be picked. + * If the path is unspecified, by default the timestamp path will be used. + * + * This value should only be set when using {@link ConflictResolutionMode.LastWriterWins}. + * + * ```typescript + * conflictResolutionPolicy.ConflictResolutionPath = "/name/first"; + * ``` + * + */ + conflictResolutionPath?: string; + /** + * Gets or sets the {@link StoredProcedure} which is used for conflict resolution in the Azure Cosmos DB service. + * This stored procedure may be created after the {@link Container} is created and can be changed as required. + * + * 1. This value should only be set when using {@link ConflictResolutionMode.Custom}. + * 2. In case the stored procedure fails or throws an exception, the conflict resolution will default to registering conflicts in the conflicts feed. + * + * ```typescript + * conflictResolutionPolicy.ConflictResolutionProcedure = "resolveConflict" + * ``` + */ + conflictResolutionProcedure?: string; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResponse.ts b/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResponse.ts new file mode 100644 index 000000000000..548f49902a40 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Conflict/ConflictResponse.ts @@ -0,0 +1,9 @@ +import { CosmosResponse } from "../../request"; +import { Resource } from "../Resource"; +import { Conflict } from "./Conflict"; +import { ConflictDefinition } from "./ConflictDefinition"; + +export interface ConflictResponse extends CosmosResponse { + /** A reference to the {@link Conflict} corresponding to the returned {@link ConflictDefinition}. */ + conflict: Conflict; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Conflict/Conflicts.ts b/sdk/cosmosdb/cosmos/src/client/Conflict/Conflicts.ts new file mode 100644 index 000000000000..5111ccbe3f86 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Conflict/Conflicts.ts @@ -0,0 +1,48 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper } from "../../common"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions } from "../../request"; +import { Container } from "../Container"; +import { Resource } from "../Resource"; +import { ConflictDefinition } from "./ConflictDefinition"; + +/** + * Use to query or read all conflicts. + * + * @see {@link Conflict} to read or delete a given {@link Conflict} by id. + */ +export class Conflicts { + constructor(public readonly container: Container, private readonly clientContext: ClientContext) {} + + /** + * Queries all conflicts. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options Use to set options like response page size, continuation tokens, etc. + * @returns {@link QueryIterator} Allows you to return results in an array or iterate over them one at a time. + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Queries all conflicts. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options Use to set options like response page size, continuation tokens, etc. + * @returns {@link QueryIterator} Allows you to return results in an array or iterate over them one at a time. + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator { + const path = Helper.getPathFromLink(this.container.url, "conflicts"); + const id = Helper.getIdFromLink(this.container.url); + + return new QueryIterator(this.clientContext, query, options, innerOptions => { + return this.clientContext.queryFeed(path, "conflicts", id, result => result.Conflicts, query, innerOptions); + }); + } + + /** + * Reads all conflicts + * @param options Use to set options like response page size, continuation tokens, etc. + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Conflict/index.ts b/sdk/cosmosdb/cosmos/src/client/Conflict/index.ts new file mode 100644 index 000000000000..128da8fc1186 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Conflict/index.ts @@ -0,0 +1,6 @@ +export { Conflict } from "./Conflict"; +export { Conflicts } from "./Conflicts"; +export { ConflictDefinition } from "./ConflictDefinition"; +export { ConflictResponse } from "./ConflictResponse"; +export { ConflictResolutionPolicy } from "./ConflictResolutionPolicy"; +export { ConflictResolutionMode } from "./ConflictResolutionMode"; diff --git a/sdk/cosmosdb/cosmos/src/client/Container/Container.ts b/sdk/cosmosdb/cosmos/src/client/Container/Container.ts new file mode 100644 index 000000000000..5bfc1ca599fc --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Container/Container.ts @@ -0,0 +1,241 @@ +import { PartitionKey } from "../.."; +import { ClientContext } from "../../ClientContext"; +import { Helper, UriFactory } from "../../common"; +import { PartitionKeyDefinition } from "../../documents"; +import { CosmosResponse, FeedOptions, RequestOptions } from "../../request"; +import { Conflict, Conflicts } from "../Conflict"; +import { Database } from "../Database"; +import { Item, Items } from "../Item"; +import { StoredProcedure, StoredProcedures } from "../StoredProcedure"; +import { Trigger, Triggers } from "../Trigger"; +import { UserDefinedFunction, UserDefinedFunctions } from "../UserDefinedFunction"; +import { ContainerDefinition } from "./ContainerDefinition"; +import { ContainerResponse } from "./ContainerResponse"; + +/** + * Operations for reading, replacing, or deleting a specific, existing container by id. + * + * @see {@link Containers} for creating new containers, and reading/querying all containers; use `.containers`. + * + * Note: all these operations make calls against a fixed budget. + * You should design your system such that these calls scale sublinearly with your application. + * For instance, do not call `container(id).read()` before every single `item.read()` call, to ensure the container exists; + * do this once on application start up. + */ +export class Container { + /** + * Operations for creating new items, and reading/querying all items + * + * For reading, replacing, or deleting an existing item, use `.item(id)`. + * + * @example Create a new item + * ```typescript + * const {body: createdItem} = await container.items.create({id: "", properties: {}}); + * ``` + */ + public readonly items: Items; + /** + * Operations for creating new stored procedures, and reading/querying all stored procedures. + * + * For reading, replacing, or deleting an existing stored procedure, use `.storedProcedure(id)`. + */ + public readonly storedProcedures: StoredProcedures; + /** + * Operations for creating new triggers, and reading/querying all triggers. + * + * For reading, replacing, or deleting an existing trigger, use `.trigger(id)`. + */ + public readonly triggers: Triggers; + /** + * Operations for creating new user defined functions, and reading/querying all user defined functions. + * + * For reading, replacing, or deleting an existing user defined function, use `.userDefinedFunction(id)`. + */ + public readonly userDefinedFunctions: UserDefinedFunctions; + + public readonly conflicts: Conflicts; + + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return UriFactory.createDocumentCollectionUri(this.database.id, this.id); + } + + /** + * Returns a container instance. Note: You should get this from `database.container(id)`, rather than creating your own object. + * @param database The parent {@link Database}. + * @param id The id of the given container. + * @hidden + */ + constructor( + public readonly database: Database, + public readonly id: string, + private readonly clientContext: ClientContext + ) { + this.items = new Items(this, this.clientContext); + this.storedProcedures = new StoredProcedures(this, this.clientContext); + this.triggers = new Triggers(this, this.clientContext); + this.userDefinedFunctions = new UserDefinedFunctions(this, this.clientContext); + this.conflicts = new Conflicts(this, this.clientContext); + } + + /** + * Used to read, replace, or delete a specific, existing {@link Item} by id. + * + * Use `.items` for creating new items, or querying/reading all items. + * + * @param id The id of the {@link Item}. + * @param partitionKey The partition key of the {@link Item}. (Required for partitioned containers). + * @example Replace an item + * const {body: replacedItem} = await container.item("").replace({id: "", title: "Updated post", authorID: 5}); + */ + public item(id: string, partitionKey?: string): Item { + return new Item(this, id, partitionKey, this.clientContext); + } + + /** + * Used to read, replace, or delete a specific, existing {@link UserDefinedFunction} by id. + * + * Use `.userDefinedFunctions` for creating new user defined functions, or querying/reading all user defined functions. + * @param id The id of the {@link UserDefinedFunction}. + */ + public userDefinedFunction(id: string): UserDefinedFunction { + return new UserDefinedFunction(this, id, this.clientContext); + } + + /** + * Used to read, replace, or delete a specific, existing {@link Conflict} by id. + * + * Use `.conflicts` for creating new conflicts, or querying/reading all conflicts. + * @param id The id of the {@link Conflict}. + */ + public conflict(id: string): Conflict { + return new Conflict(this, id, this.clientContext); + } + + /** + * Used to read, replace, or delete a specific, existing {@link StoredProcedure} by id. + * + * Use `.storedProcedures` for creating new stored procedures, or querying/reading all stored procedures. + * @param id The id of the {@link StoredProcedure}. + */ + public storedProcedure(id: string): StoredProcedure { + return new StoredProcedure(this, id, this.clientContext); + } + + /** + * Used to read, replace, or delete a specific, existing {@link Trigger} by id. + * + * Use `.triggers` for creating new triggers, or querying/reading all triggers. + * @param id The id of the {@link Trigger}. + */ + public trigger(id: string): Trigger { + return new Trigger(this, id, this.clientContext); + } + + /** Read the container's definition */ + public async read(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.read(path, "colls", id, undefined, options); + this.clientContext.partitionKeyDefinitionCache[this.url] = response.result.partitionKey; + return { + body: response.result, + headers: response.headers, + ref: this, + container: this + }; + } + + /** Replace the container's definition */ + public async replace(body: ContainerDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.replace(body, path, "colls", id, undefined, options); + return { + body: response.result, + headers: response.headers, + ref: this, + container: this + }; + } + + /** Delete the container */ + public async delete(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.delete(path, "colls", id, undefined, options); + return { + body: response.result, + headers: response.headers, + ref: this, + container: this + }; + } + + /** + * Gets the partition key definition first by looking into the cache otherwise by reading the collection. + * @ignore + * @param {string} collectionLink - Link to the collection whose partition key needs to be extracted. + * @param {function} callback - \ + * The arguments to the callback are(in order): error, partitionKeyDefinition, response object and response headers + */ + public async getPartitionKeyDefinition(): Promise> { + // $ISSUE-felixfan-2016-03-17: Make name based path and link based path use the same key + // $ISSUE-felixfan-2016-03-17: Refresh partitionKeyDefinitionCache when necessary + if (this.url in this.clientContext.partitionKeyDefinitionCache) { + return { + body: this.clientContext.partitionKeyDefinitionCache[this.url], + ref: this + }; + } + + const { headers } = await this.read(); + return { + body: this.clientContext.partitionKeyDefinitionCache[this.url], + headers, + ref: this + }; + } + + public readPartitionKeyRanges(feedOptions?: FeedOptions) { + feedOptions = feedOptions || {}; + return this.clientContext.queryPartitionKeyRanges(this.url, undefined, feedOptions); + } + + // TODO: The ParitionKey type is REALLY weird. Now that it's being exported, we should clean it up. + public extractPartitionKey(document: any, partitionKeyDefinition: PartitionKeyDefinition): PartitionKey[] { + // TODO: any + if (partitionKeyDefinition && partitionKeyDefinition.paths && partitionKeyDefinition.paths.length > 0) { + const partitionKey: PartitionKey[] = []; + partitionKeyDefinition.paths.forEach((path: string) => { + const pathParts = Helper.parsePath(path); + + let obj = document; + for (const part of pathParts) { + if (!(typeof obj === "object" && part in obj)) { + obj = {}; + break; + } + + obj = obj[part]; + } + + partitionKey.push(obj); + }); + + return partitionKey; + } + + return undefined; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Container/ContainerDefinition.ts b/sdk/cosmosdb/cosmos/src/client/Container/ContainerDefinition.ts new file mode 100644 index 000000000000..c664c0d433b7 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Container/ContainerDefinition.ts @@ -0,0 +1,18 @@ +import { IndexingPolicy, PartitionKey, PartitionKeyDefinition } from "../../documents"; +import { ConflictResolutionPolicy } from "../Conflict/ConflictResolutionPolicy"; +import { UniqueKeyPolicy } from "./UniqueKeyPolicy"; + +export interface ContainerDefinition { + /** The id of the container. */ + id?: string; + /** TODO */ + partitionKey?: PartitionKeyDefinition; + /** The indexing policy associated with the container. */ + indexingPolicy?: IndexingPolicy; + /** The default time to live in seconds for items in a container. */ + defaultTtl?: number; + /** The conflict resolution policy used to resolve conflicts in a container. */ + conflictResolutionPolicy?: ConflictResolutionPolicy; + /** Policy for additional keys that must be unique per partion key */ + uniqueKeyPolicy?: UniqueKeyPolicy; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Container/ContainerResponse.ts b/sdk/cosmosdb/cosmos/src/client/Container/ContainerResponse.ts new file mode 100644 index 000000000000..529d6bf0dec7 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Container/ContainerResponse.ts @@ -0,0 +1,10 @@ +import { Container } from "."; +import { CosmosResponse } from "../../request/CosmosResponse"; +import { Resource } from "../Resource"; +import { ContainerDefinition } from "./ContainerDefinition"; + +/** Response object for Container operations */ +export interface ContainerResponse extends CosmosResponse { + /** A reference to the {@link Container} that the returned {@link ContainerDefinition} corresponds to. */ + container: Container; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Container/Containers.ts b/sdk/cosmosdb/cosmos/src/client/Container/Containers.ts new file mode 100644 index 000000000000..d07723343c4f --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Container/Containers.ts @@ -0,0 +1,164 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, StatusCodes } from "../../common"; +import { HeaderUtils, SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions, RequestOptions } from "../../request"; +import { Database } from "../Database"; +import { Resource } from "../Resource"; +import { Container } from "./Container"; +import { ContainerDefinition } from "./ContainerDefinition"; +import { ContainerResponse } from "./ContainerResponse"; + +/** + * Operations for creating new containers, and reading/querying all containers + * + * @see {@link Container} for reading, replacing, or deleting an existing container; use `.container(id)`. + * + * Note: all these operations make calls against a fixed budget. + * You should design your system such that these calls scale sublinearly with your application. + * For instance, do not call `containers.readAll()` before every single `item.read()` call, to ensure the container exists; + * do this once on application start up. + */ +export class Containers { + constructor(public readonly database: Database, private readonly clientContext: ClientContext) {} + + /** + * Queries all containers. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options Use to set options like response page size, continuation tokens, etc. + * @returns {@link QueryIterator} Allows you to return specific contaienrs in an array or iterate over them one at a time. + * @example Read all containers to array. + * ```typescript + * const querySpec: SqlQuerySpec = { + * query: "SELECT * FROM root r WHERE r.id = @container", + * parameters: [ + * {name: "@container", value: "Todo"} + * ] + * }; + * const {body: containerList} = await client.database("").containers.query(querySpec).toArray(); + * ``` + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Queries all containers. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options Use to set options like response page size, continuation tokens, etc. + * @returns {@link QueryIterator} Allows you to return specific contaienrs in an array or iterate over them one at a time. + * @example Read all containers to array. + * ```typescript + * const querySpec: SqlQuerySpec = { + * query: "SELECT * FROM root r WHERE r.id = @container", + * parameters: [ + * {name: "@container", value: "Todo"} + * ] + * }; + * const {body: containerList} = await client.database("").containers.query(querySpec).toArray(); + * ``` + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator { + const path = Helper.getPathFromLink(this.database.url, "colls"); + const id = Helper.getIdFromLink(this.database.url); + + return new QueryIterator(this.clientContext, query, options, innerOptions => { + return this.clientContext.queryFeed( + path, + "colls", + id, + result => result.DocumentCollections, + query, + innerOptions + ); + }); + } + + /** + * Creates a container. + * + * A container is a named logical container for items. + * + * A database may contain zero or more named containers and each container consists of + * zero or more JSON items. + * + * Being schema-free, the items in a container do not need to share the same structure or fields. + * + * + * Since containers are application resources, they can be authorized using either the + * master key or resource keys. + * + * @param body Represents the body of the container. + * @param options Use to set options like response page size, continuation tokens, etc. + */ + public async create(body: ContainerDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + const path = Helper.getPathFromLink(this.database.url, "colls"); + const id = Helper.getIdFromLink(this.database.url); + + const response = await this.clientContext.create(body, path, "colls", id, undefined, options); + const ref = new Container(this.database, response.result.id, this.clientContext); + return { + body: response.result, + headers: response.headers, + ref, + container: ref + }; + } + + /** + * Checks if a Container exists, and, if it doesn't, creates it. + * This will make a read operation based on the id in the `body`, then if it is not found, a create operation. + * You should confirm that the output matches the body you passed in for non-default properties (i.e. indexing policy/etc.) + * + * A container is a named logical container for items. + * + * A database may contain zero or more named containers and each container consists of + * zero or more JSON items. + * + * Being schema-free, the items in a container do not need to share the same structure or fields. + * + * + * Since containers are application resources, they can be authorized using either the + * master key or resource keys. + * + * @param body Represents the body of the container. + * @param options Use to set options like response page size, continuation tokens, etc. + */ + public async createIfNotExists(body: ContainerDefinition, options?: RequestOptions): Promise { + if (!body || body.id === null || body.id === undefined) { + throw new Error("body parameter must be an object with an id property"); + } + /* + 1. Attempt to read the Database (based on an assumption that most databases will already exist, so its faster) + 2. If it fails with NotFound error, attempt to create the db. Else, return the read results. + */ + try { + const readResponse = await this.database.container(body.id).read(options); + return readResponse; + } catch (err) { + if (err.code === StatusCodes.NotFound) { + const createResponse = await this.create(body, options); + // Must merge the headers to capture RU costskaty + HeaderUtils.mergeHeaders(createResponse.headers, err.headers); + return createResponse; + } else { + throw err; + } + } + } + + /** + * Read all containers. + * @param options Use to set options like response page size, continuation tokens, etc. + * @returns {@link QueryIterator} Allows you to return all containers in an array or iterate over them one at a time. + * @example Read all containers to array. + * ```typescript + * const {body: containerList} = await client.database("").containers.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Container/PartitionKeyRange.ts b/sdk/cosmosdb/cosmos/src/client/Container/PartitionKeyRange.ts new file mode 100644 index 000000000000..b52219135e67 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Container/PartitionKeyRange.ts @@ -0,0 +1,9 @@ +export interface PartitionKeyRange { + id: string; + minInclusive: string; + maxExclusive: string; + ridPrefix: number; + throughputFraction: number; + status: string; + parents: string[]; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Container/UniqueKeyPolicy.ts b/sdk/cosmosdb/cosmos/src/client/Container/UniqueKeyPolicy.ts new file mode 100644 index 000000000000..777ee811d729 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Container/UniqueKeyPolicy.ts @@ -0,0 +1,9 @@ +/** Interface for setting unique keys on container creation */ +export interface UniqueKeyPolicy { + uniqueKeys: UniqueKey[]; +} + +/** Interface for a single unique key passed as part of UniqueKeyPolicy */ +export interface UniqueKey { + paths: string[]; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Container/index.ts b/sdk/cosmosdb/cosmos/src/client/Container/index.ts new file mode 100644 index 000000000000..b33252018302 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Container/index.ts @@ -0,0 +1,5 @@ +export { Container } from "./Container"; +export { Containers } from "./Containers"; +export { ContainerDefinition } from "./ContainerDefinition"; +export { ContainerResponse } from "./ContainerResponse"; +export { PartitionKeyRange } from "./PartitionKeyRange"; diff --git a/sdk/cosmosdb/cosmos/src/client/Database/Database.ts b/sdk/cosmosdb/cosmos/src/client/Database/Database.ts new file mode 100644 index 000000000000..6e3c9d1b2916 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Database/Database.ts @@ -0,0 +1,103 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, UriFactory } from "../../common"; +import { CosmosClient } from "../../CosmosClient"; +import { RequestOptions } from "../../request"; +import { Container, Containers } from "../Container"; +import { User, Users } from "../User"; +import { DatabaseDefinition } from "./DatabaseDefinition"; +import { DatabaseResponse } from "./DatabaseResponse"; + +/** + * Operations for reading or deleting an existing database. + * + * @see {@link Databases} for creating new databases, and reading/querying all databases; use `client.databases`. + * + * Note: all these operations make calls against a fixed budget. + * You should design your system such that these calls scale sublinearly with your application. + * For instance, do not call `database.read()` before every single `item.read()` call, to ensure the database exists; + * do this once on application start up. + */ +export class Database { + /** + * Used for creating new containers, or querying/reading all containers. + * + * Use `.container(id)` to read, replace, or delete a specific, existing {@link Database} by id. + * + * @example Create a new container + * ```typescript + * const {body: containerDefinition, container} = await client.database("").containers.create({id: ""}); + * ``` + */ + public readonly containers: Containers; + /** + * Used for creating new users, or querying/reading all users. + * + * Use `.user(id)` to read, replace, or delete a specific, existing {@link User} by id. + */ + public readonly users: Users; + + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return UriFactory.createDatabaseUri(this.id); + } + + /** Returns a new {@link Database} instance. + * + * Note: the intention is to get this object from {@link CosmosClient} via `client.database(id)`, not to instantiate it yourself. + */ + constructor(public readonly client: CosmosClient, public readonly id: string, private clientContext: ClientContext) { + this.containers = new Containers(this, this.clientContext); + this.users = new Users(this, this.clientContext); + } + + /** + * Used to read, replace, or delete a specific, existing {@link Database} by id. + * + * Use `.containers` creating new containers, or querying/reading all containers. + * + * @example Delete a container + * ```typescript + * await client.database("").container("").delete(); + * ``` + */ + public container(id: string): Container { + return new Container(this, id, this.clientContext); + } + + /** + * Used to read, replace, or delete a specific, existing {@link User} by id. + * + * Use `.users` for creating new users, or querying/reading all users. + */ + public user(id: string): User { + return new User(this, id, this.clientContext); + } + + /** Read the definition of the given Database. */ + public async read(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + const response = await this.clientContext.read(path, "dbs", id, undefined, options); + return { + body: response.result, + headers: response.headers, + ref: this, + database: this + }; + } + + /** Delete the given Database. */ + public async delete(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + const response = await this.clientContext.delete(path, "dbs", id, undefined, options); + return { + body: response.result, + headers: response.headers, + ref: this, + database: this + }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Database/DatabaseDefinition.ts b/sdk/cosmosdb/cosmos/src/client/Database/DatabaseDefinition.ts new file mode 100644 index 000000000000..643cb2a62881 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Database/DatabaseDefinition.ts @@ -0,0 +1,4 @@ +export interface DatabaseDefinition { + /** The id of the database. */ + id?: string; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Database/DatabaseResponse.ts b/sdk/cosmosdb/cosmos/src/client/Database/DatabaseResponse.ts new file mode 100644 index 000000000000..69c8df153230 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Database/DatabaseResponse.ts @@ -0,0 +1,10 @@ +import { CosmosResponse } from "../../request/CosmosResponse"; +import { Resource } from "../Resource"; +import { Database } from "./Database"; +import { DatabaseDefinition } from "./DatabaseDefinition"; + +/** Response object for Database operations */ +export interface DatabaseResponse extends CosmosResponse { + /** A reference to the {@link Database} that the returned {@link DatabaseDefinition} corresponds to. */ + database: Database; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Database/Databases.ts b/sdk/cosmosdb/cosmos/src/client/Database/Databases.ts new file mode 100644 index 000000000000..a1d867cb4d51 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Database/Databases.ts @@ -0,0 +1,159 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, StatusCodes } from "../../common"; +import { CosmosClient } from "../../CosmosClient"; +import { FetchFunctionCallback, HeaderUtils, SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions, RequestOptions } from "../../request"; +import { Resource } from "../Resource"; +import { Database } from "./Database"; +import { DatabaseDefinition } from "./DatabaseDefinition"; +import { DatabaseResponse } from "./DatabaseResponse"; + +/** + * Operations for creating new databases, and reading/querying all databases + * + * @see {@link Database} for reading or deleting an existing database; use `client.database(id)`. + * + * Note: all these operations make calls against a fixed budget. + * You should design your system such that these calls scale sublinearly with your application. + * For instance, do not call `databases.readAll()` before every single `item.read()` call, to ensure the database exists; + * do this once on application start up. + */ +export class Databases { + /** + * @hidden + * @param client The parent {@link CosmosClient} for the Database. + */ + constructor(public readonly client: CosmosClient, private readonly clientContext: ClientContext) {} + + /** + * Queries all databases. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options Use to set options like response page size, continuation tokens, etc. + * @returns {@link QueryIterator} Allows you to return all databases in an array or iterate over them one at a time. + * @example Read all databases to array. + * ```typescript + * const querySpec: SqlQuerySpec = { + * query: "SELECT * FROM root r WHERE r.id = @db", + * parameters: [ + * {name: "@db", value: "Todo"} + * ] + * }; + * const {body: databaseList} = await client.databases.query(querySpec).toArray(); + * ``` + */ + public query(query: string | SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Queries all databases. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options Use to set options like response page size, continuation tokens, etc. + * @returns {@link QueryIterator} Allows you to return all databases in an array or iterate over them one at a time. + * @example Read all databases to array. + * ```typescript + * const querySpec: SqlQuerySpec = { + * query: "SELECT * FROM root r WHERE r.id = @db", + * parameters: [ + * {name: "@db", value: "Todo"} + * ] + * }; + * const {body: databaseList} = await client.databases.query(querySpec).toArray(); + * ``` + */ + public query(query: string | SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: string | SqlQuerySpec, options?: FeedOptions): QueryIterator { + const cb: FetchFunctionCallback = innerOptions => { + return this.clientContext.queryFeed("/dbs", "dbs", "", result => result.Databases, query, innerOptions); + }; + return new QueryIterator(this.clientContext, query, options, cb); + } + + /** + * Send a request for creating a database. + * + * A database manages users, permissions and a set of containers. + * Each Azure Cosmos DB Database Account is able to support multiple independent named databases, + * with the database being the logical container for data. + * + * Each Database consists of one or more containers, each of which in turn contain one or more + * documents. Since databases are an administrative resource, the Service Master Key will be + * required in order to access and successfully complete any action using the User APIs. + * + * @param body The {@link DatabaseDefinition} that represents the {@link Database} to be created. + * @param options Use to set options like response page size, continuation tokens, etc. + */ + public async create(body: DatabaseDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = "/dbs"; // TODO: constant + const response = await this.clientContext.create( + body, + path, + "dbs", + undefined, + undefined, + options + ); + const ref = new Database(this.client, body.id, this.clientContext); + return { + body: response.result, + headers: response.headers, + ref, + database: ref + }; + } + + /** + * Check if a database exists, and if it doesn't, create it. + * This will make a read operation based on the id in the `body`, then if it is not found, a create operation. + * + * A database manages users, permissions and a set of containers. + * Each Azure Cosmos DB Database Account is able to support multiple independent named databases, + * with the database being the logical container for data. + * + * Each Database consists of one or more containers, each of which in turn contain one or more + * documents. Since databases are an an administrative resource, the Service Master Key will be + * required in order to access and successfully complete any action using the User APIs. + * + * @param body The {@link DatabaseDefinition} that represents the {@link Database} to be created. + * @param options + */ + public async createIfNotExists(body: DatabaseDefinition, options?: RequestOptions): Promise { + if (!body || body.id === null || body.id === undefined) { + throw new Error("body parameter must be an object with an id property"); + } + /* + 1. Attempt to read the Database (based on an assumption that most databases will already exist, so its faster) + 2. If it fails with NotFound error, attempt to create the db. Else, return the read results. + */ + try { + const readResponse = await this.client.database(body.id).read(options); + return readResponse; + } catch (err) { + if (err.code === StatusCodes.NotFound) { + const createResponse = await this.create(body, options); + // Must merge the headers to capture RU costskaty + HeaderUtils.mergeHeaders(createResponse.headers, err.headers); + return createResponse; + } else { + throw err; + } + } + } + + // TODO: DatabaseResponse for QueryIterator? + /** + * Reads all databases. + * @param options Use to set options like response page size, continuation tokens, etc. + * @returns {@link QueryIterator} Allows you to return all databases in an array or iterate over them one at a time. + * @example Read all databases to array. + * ```typescript + * const {body: databaseList} = await client.databases.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Database/index.ts b/sdk/cosmosdb/cosmos/src/client/Database/index.ts new file mode 100644 index 000000000000..cc20795c666b --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Database/index.ts @@ -0,0 +1,4 @@ +export { Database } from "./Database"; +export { Databases } from "./Databases"; +export { DatabaseDefinition } from "./DatabaseDefinition"; +export { DatabaseResponse } from "./DatabaseResponse"; diff --git a/sdk/cosmosdb/cosmos/src/client/Item/Item.ts b/sdk/cosmosdb/cosmos/src/client/Item/Item.ts new file mode 100644 index 000000000000..2d11339c62e6 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Item/Item.ts @@ -0,0 +1,165 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, UriFactory } from "../../common"; +import { RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { Resource } from "../Resource"; +import { ItemDefinition } from "./ItemDefinition"; +import { ItemResponse } from "./ItemResponse"; + +/** + * Used to perform operations on a specific item. + * + * @see {@link Items} for operations on all items; see `container.items`. + */ +export class Item { + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return UriFactory.createDocumentUri(this.container.database.id, this.container.id, this.id); + } + + /** + * @hidden + * @param container The parent {@link Container}. + * @param id The id of the given {@link Item}. + * @param primaryKey The primary key of the given {@link Item} (only for partitioned containers). + */ + constructor( + public readonly container: Container, + public readonly id: string, + public readonly primaryKey: string, + private readonly clientContext: ClientContext + ) {} + + /** + * Read the item's definition. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param options Additional options for the request, such as the partition key. + * Note, if you provide a partition key on the options object, it will override the primary key on `this.primaryKey`. + */ + public read(options?: RequestOptions): Promise>; + /** + * Read the item's definition. + * + * Any provided type, T, is not necessarily enforced by the SDK. + * You may get more or less properties and it's up to your logic to enforce it. + * If the type, T, is a class, it won't pass `typeof` comparisons, because it won't have a match prototype. + * It's recommended to only use interfaces. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param options Additional options for the request, such as the partition key. + * Note, if you provide a partition key on the options object, it will override the primary key on `this.primaryKey`. + * + * @example Using custom type for response + * ```typescript + * interface TodoItem { + * title: string; + * done: bool; + * id: string; + * } + * + * let item: TodoItem; + * ({body: item} = await item.read()); + * ``` + */ + public read(options?: RequestOptions): Promise>; + public async read(options?: RequestOptions): Promise> { + options = options || {}; + if ((!options || !options.partitionKey) && this.primaryKey) { + options.partitionKey = this.primaryKey; + } + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + const response = await this.clientContext.read(path, "docs", id, undefined, options); + + return { + body: response.result, + headers: response.headers, + ref: this, + item: this + }; + } + + /** + * Replace the item's definition. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param body The definition to replace the existing {@link Item}'s definition with. + * @param options Additional options for the request, such as the partition key. + */ + public replace(body: ItemDefinition, options?: RequestOptions): Promise>; + /** + * Replace the item's definition. + * + * Any provided type, T, is not necessarily enforced by the SDK. + * You may get more or less properties and it's up to your logic to enforce it. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param body The definition to replace the existing {@link Item}'s definition with. + * @param options Additional options for the request, such as the partition key. + */ + public replace(body: T, options?: RequestOptions): Promise>; + public async replace(body: T, options?: RequestOptions): Promise> { + options = options || {}; + if ((!options || !options.partitionKey) && this.primaryKey) { + options.partitionKey = this.primaryKey; + } + if (options.partitionKey === undefined && options.skipGetPartitionKeyDefinition !== true) { + const { body: partitionKeyDefinition } = await this.container.getPartitionKeyDefinition(); + options.partitionKey = this.container.extractPartitionKey(body, partitionKeyDefinition); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.replace(body, path, "docs", id, undefined, options); + return { + body: response.result, + headers: response.headers, + ref: this, + item: this + }; + } + + /** + * Delete the item. + * @param options Additional options for the request, such as the partition key. + */ + public delete(options?: RequestOptions): Promise>; + /** + * Delete the item. + * + * Any provided type, T, is not necessarily enforced by the SDK. + * You may get more or less properties and it's up to your logic to enforce it. + * + * @param options Additional options for the request, such as the partition key. + */ + public delete(options?: RequestOptions): Promise>; + public async delete(options?: RequestOptions): Promise> { + options = options || {}; + if ((!options || !options.partitionKey) && this.primaryKey) { + options.partitionKey = this.primaryKey; + } + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.delete(path, "docs", id, undefined, options); + return { + body: response.result, + headers: response.headers, + ref: this, + item: this + }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Item/ItemDefinition.ts b/sdk/cosmosdb/cosmos/src/client/Item/ItemDefinition.ts new file mode 100644 index 000000000000..9bd90125516c --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Item/ItemDefinition.ts @@ -0,0 +1,11 @@ +/** + * Items in Cosmos DB are simply JSON objects. + * Most of the Item operations allow for your to provide your own type + * that extends the very simple ItemDefinition. + * + * You cannot use any reserved keys. You can see the reserved key list + * in {@link ItemBody} + */ +export interface ItemDefinition { + [key: string]: any; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Item/ItemResponse.ts b/sdk/cosmosdb/cosmos/src/client/Item/ItemResponse.ts new file mode 100644 index 000000000000..9685f4dd1ff3 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Item/ItemResponse.ts @@ -0,0 +1,9 @@ +import { CosmosResponse } from "../../request/CosmosResponse"; +import { Resource } from "../Resource"; +import { Item } from "./Item"; +import { ItemDefinition } from "./ItemDefinition"; + +export interface ItemResponse extends CosmosResponse { + /** Reference to the {@link Item} the response corresponds to. */ + item: Item; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Item/Items.ts b/sdk/cosmosdb/cosmos/src/client/Item/Items.ts new file mode 100644 index 000000000000..dbde4d4828d9 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Item/Items.ts @@ -0,0 +1,297 @@ +import { ChangeFeedIterator } from "../../ChangeFeedIterator"; +import { ChangeFeedOptions } from "../../ChangeFeedOptions"; +import { ClientContext } from "../../ClientContext"; +import { Helper } from "../../common"; +import { FetchFunctionCallback, SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions, RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { Resource } from "../Resource"; +import { Item } from "./Item"; +import { ItemDefinition } from "./ItemDefinition"; +import { ItemResponse } from "./ItemResponse"; + +function isChangeFeedOptions(options: unknown): options is ChangeFeedOptions { + const optionsType = typeof options; + return options && !(optionsType === "string" || optionsType === "boolean" || optionsType === "number"); +} + +/** + * Operations for creating new items, and reading/querying all items + * + * @see {@link Item} for reading, replacing, or deleting an existing container; use `.item(id)`. + */ +export class Items { + /** + * Create an instance of {@link Items} linked to the parent {@link Container}. + * @param container The parent container. + * @hidden + */ + constructor(public readonly container: Container, private readonly clientContext: ClientContext) {} + + /** + * Queries all items. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options Used for modifying the request (for instance, specifying the partition key). + * @example Read all items to array. + * ```typescript + * const querySpec: SqlQuerySpec = { + * query: "SELECT * FROM Families f WHERE f.lastName = @lastName", + * parameters: [ + * {name: "@lastName", value: "Hendricks"} + * ] + * }; + * const {result: items} = await items.query(querySpec).toArray(); + * ``` + */ + public query(query: string | SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Queries all items. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options Used for modifying the request (for instance, specifying the partition key). + * @example Read all items to array. + * ```typescript + * const querySpec: SqlQuerySpec = { + * query: "SELECT firstname FROM Families f WHERE f.lastName = @lastName", + * parameters: [ + * {name: "@lastName", value: "Hendricks"} + * ] + * }; + * const {result: items} = await items.query<{firstName: string}>(querySpec).toArray(); + * ``` + */ + public query(query: string | SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: string | SqlQuerySpec, options?: FeedOptions): QueryIterator { + const path = Helper.getPathFromLink(this.container.url, "docs"); + const id = Helper.getIdFromLink(this.container.url); + + const fetchFunction: FetchFunctionCallback = (innerOptions: FeedOptions) => { + return this.clientContext.queryFeed( + path, + "docs", + id, + result => (result ? result.Documents : []), + query, + innerOptions + ); + }; + + return new QueryIterator(this.clientContext, query, options, fetchFunction, this.container.url); + } + + /** + * Create a `ChangeFeedIterator` to iterate over pages of changes + * + * @param partitionKey + * @param changeFeedOptions + * + * @example Read from the beginning of the change feed. + * ```javascript + * const iterator = items.readChangeFeed({ startFromBeginning: true }); + * const firstPage = await iterator.executeNext(); + * const firstPageResults = firstPage.result + * const secondPage = await iterator.executeNext(); + * ``` + */ + public readChangeFeed( + partitionKey: string | number | boolean, + changeFeedOptions: ChangeFeedOptions + ): ChangeFeedIterator; + /** + * Create a `ChangeFeedIterator` to iterate over pages of changes + * + * @param changeFeedOptions + */ + public readChangeFeed(changeFeedOptions?: ChangeFeedOptions): ChangeFeedIterator; + /** + * Create a `ChangeFeedIterator` to iterate over pages of changes + * + * @param partitionKey + * @param changeFeedOptions + */ + public readChangeFeed( + partitionKey: string | number | boolean, + changeFeedOptions: ChangeFeedOptions + ): ChangeFeedIterator; + /** + * Create a `ChangeFeedIterator` to iterate over pages of changes + * + * @param changeFeedOptions + */ + public readChangeFeed(changeFeedOptions?: ChangeFeedOptions): ChangeFeedIterator; + public readChangeFeed( + partitionKeyOrChangeFeedOptions?: string | number | boolean | ChangeFeedOptions, + changeFeedOptions?: ChangeFeedOptions + ): ChangeFeedIterator { + let partitionKey: string | number | boolean; + if (!changeFeedOptions && isChangeFeedOptions(partitionKeyOrChangeFeedOptions)) { + partitionKey = undefined; + changeFeedOptions = partitionKeyOrChangeFeedOptions; + } else if (partitionKeyOrChangeFeedOptions !== undefined && !isChangeFeedOptions(partitionKeyOrChangeFeedOptions)) { + partitionKey = partitionKeyOrChangeFeedOptions; + } + + if (!changeFeedOptions) { + throw new Error("changeFeedOptions must be a valid object"); + } + + const path = Helper.getPathFromLink(this.container.url, "docs"); + const id = Helper.getIdFromLink(this.container.url); + return new ChangeFeedIterator( + this.clientContext, + id, + path, + partitionKey, + async () => { + const bodyWillBeTruthyIfPartitioned = (await this.container.getPartitionKeyDefinition()).body; + return !!bodyWillBeTruthyIfPartitioned; + }, + changeFeedOptions + ); + } + + /** + * Read all items. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param options Used for modifying the request (for instance, specifying the partition key). + * @example Read all items to array. + * ```typescript + * const {body: containerList} = await items.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator; + /** + * Read all items. + * + * Any provided type, T, is not necessarily enforced by the SDK. + * You may get more or less properties and it's up to your logic to enforce it. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param options Used for modifying the request (for instance, specifying the partition key). + * @example Read all items to array. + * ```typescript + * const {body: containerList} = await items.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator; + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } + + /** + * Create a item. + * + * There is no set schema for JSON items. They may contain any number of custom properties.. + * + * @param body Represents the body of the item. Can contain any number of user defined properties. + * @param options Used for modifying the request (for instance, specifying the partition key). + */ + public async create(body: any, options?: RequestOptions): Promise>; + /** + * Create a item. + * + * Any provided type, T, is not necessarily enforced by the SDK. + * You may get more or less properties and it's up to your logic to enforce it. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param body Represents the body of the item. Can contain any number of user defined properties. + * @param options Used for modifying the request (for instance, specifying the partition key). + */ + public async create(body: T, options?: RequestOptions): Promise>; + public async create(body: T, options: RequestOptions = {}): Promise> { + if (options.partitionKey === undefined && options.skipGetPartitionKeyDefinition !== true) { + const { body: partitionKeyDefinition } = await this.container.getPartitionKeyDefinition(); + options.partitionKey = this.container.extractPartitionKey(body, partitionKeyDefinition); + } + + // Generate random document id if the id is missing in the payload and + // options.disableAutomaticIdGeneration != true + if ((body.id === undefined || body.id === "") && !options.disableAutomaticIdGeneration) { + body.id = Helper.generateGuidId(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.container.url, "docs"); + const id = Helper.getIdFromLink(this.container.url); + + const response = await this.clientContext.create(body, path, "docs", id, undefined, options); + + const ref = new Item( + this.container, + (response.result as any).id, + (options && options.partitionKey) as string, + this.clientContext + ); + return { + body: response.result, + headers: response.headers, + ref, + item: ref + }; + } + + /** + * Upsert an item. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param body Represents the body of the item. Can contain any number of user defined properties. + * @param options Used for modifying the request (for instance, specifying the partition key). + */ + public async upsert(body: any, options?: RequestOptions): Promise>; + /** + * Upsert an item. + * + * Any provided type, T, is not necessarily enforced by the SDK. + * You may get more or less properties and it's up to your logic to enforce it. + * + * There is no set schema for JSON items. They may contain any number of custom properties. + * + * @param body Represents the body of the item. Can contain any number of user defined properties. + * @param options Used for modifying the request (for instance, specifying the partition key). + */ + public async upsert(body: T, options?: RequestOptions): Promise>; + public async upsert(body: T, options: RequestOptions = {}): Promise> { + if (options.partitionKey === undefined && options.skipGetPartitionKeyDefinition !== true) { + const { body: partitionKeyDefinition } = await this.container.getPartitionKeyDefinition(); + options.partitionKey = this.container.extractPartitionKey(body, partitionKeyDefinition); + } + + // Generate random document id if the id is missing in the payload and + // options.disableAutomaticIdGeneration != true + if ((body.id === undefined || body.id === "") && !options.disableAutomaticIdGeneration) { + body.id = Helper.generateGuidId(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.container.url, "docs"); + const id = Helper.getIdFromLink(this.container.url); + + const response = (await this.clientContext.upsert(body, path, "docs", id, undefined, options)) as T & Resource; + + const ref = new Item( + this.container, + (response.result as any).id, + (options && options.partitionKey) as string, + this.clientContext + ); + return { + body: response.result, + headers: response.headers, + ref, + item: ref + }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Item/index.ts b/sdk/cosmosdb/cosmos/src/client/Item/index.ts new file mode 100644 index 000000000000..1f37b9feab76 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Item/index.ts @@ -0,0 +1,4 @@ +export { Item } from "./Item"; +export { Items } from "./Items"; +export { ItemResponse } from "./ItemResponse"; +export { ItemDefinition } from "./ItemDefinition"; diff --git a/sdk/cosmosdb/cosmos/src/client/Offer/Offer.ts b/sdk/cosmosdb/cosmos/src/client/Offer/Offer.ts new file mode 100644 index 000000000000..fcac4ca72476 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Offer/Offer.ts @@ -0,0 +1,60 @@ +import { ClientContext } from "../../ClientContext"; +import { Constants, Helper } from "../../common"; +import { CosmosClient } from "../../CosmosClient"; +import { RequestOptions } from "../../request"; +import { OfferDefinition } from "./OfferDefinition"; +import { OfferResponse } from "./OfferResponse"; + +/** + * Use to read or replace an existing {@link Offer} by id. + * + * @see {@link Offers} to query or read all offers. + */ +export class Offer { + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return `/${Constants.Path.OffersPathSegment}/${this.id}`; + } + /** + * @hidden + * @param client The parent {@link CosmosClient} for the Database Account. + * @param id The id of the given {@link Offer}. + */ + constructor( + public readonly client: CosmosClient, + public readonly id: string, + private readonly clientContext: ClientContext + ) {} + + /** + * Read the {@link OfferDefinition} for the given {@link Offer}. + * @param options + */ + public async read(options?: RequestOptions): Promise { + const response = await this.clientContext.read(this.url, "offers", this.id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, offer: this }; + } + + /** + * Replace the given {@link Offer} with the specified {@link OfferDefinition}. + * @param body The specified {@link OfferDefinition} + * @param options + */ + public async replace(body: OfferDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + const response = await this.clientContext.replace( + body, + this.url, + "offers", + this.id, + undefined, + options + ); + return { body: response.result, headers: response.headers, ref: this, offer: this }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Offer/OfferDefinition.ts b/sdk/cosmosdb/cosmos/src/client/Offer/OfferDefinition.ts new file mode 100644 index 000000000000..42084dfac993 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Offer/OfferDefinition.ts @@ -0,0 +1,11 @@ +export interface OfferDefinition { + id?: string; + offerType?: string; // TODO: enum? + offerVersion?: string; // TODO: enum? + resource?: string; + offerResourceId?: string; + content?: { + offerThroughput: number; + offerIsRUPerMinuteThroughputEnabled: boolean; + }; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Offer/OfferResponse.ts b/sdk/cosmosdb/cosmos/src/client/Offer/OfferResponse.ts new file mode 100644 index 000000000000..8d1095ad004a --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Offer/OfferResponse.ts @@ -0,0 +1,9 @@ +import { CosmosResponse } from "../../request"; +import { Resource } from "../Resource"; +import { Offer } from "./Offer"; +import { OfferDefinition } from "./OfferDefinition"; + +export interface OfferResponse extends CosmosResponse { + /** A reference to the {@link Offer} corresponding to the returned {@link OfferDefinition}. */ + offer: Offer; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Offer/Offers.ts b/sdk/cosmosdb/cosmos/src/client/Offer/Offers.ts new file mode 100644 index 000000000000..52da61d0b5ef --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Offer/Offers.ts @@ -0,0 +1,50 @@ +import { ClientContext } from "../../ClientContext"; +import { CosmosClient } from "../../CosmosClient"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions } from "../../request"; +import { Resource } from "../Resource"; +import { OfferDefinition } from "./OfferDefinition"; + +/** + * Use to query or read all Offers. + * + * @see {@link Offer} to read or replace an existing {@link Offer} by id. + */ +export class Offers { + /** + * @hidden + * @param client The parent {@link CosmosClient} for the offers. + */ + constructor(public readonly client: CosmosClient, private readonly clientContext: ClientContext) {} + + /** + * Query all offers. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Query all offers. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator { + return new QueryIterator(this.clientContext, query, options, innerOptions => { + return this.clientContext.queryFeed("/offers", "offers", "", result => result.Offers, query, innerOptions); + }); + } + + /** + * Read all offers. + * @param options + * @example Read all offers to array. + * ```typescript + * const {body: offerList} = await client.offers.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Offer/index.ts b/sdk/cosmosdb/cosmos/src/client/Offer/index.ts new file mode 100644 index 000000000000..fe384dee31f5 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Offer/index.ts @@ -0,0 +1,4 @@ +export { Offer } from "./Offer"; +export { Offers } from "./Offers"; +export { OfferDefinition } from "./OfferDefinition"; +export { OfferResponse } from "./OfferResponse"; diff --git a/sdk/cosmosdb/cosmos/src/client/Permission/Permission.ts b/sdk/cosmosdb/cosmos/src/client/Permission/Permission.ts new file mode 100644 index 000000000000..9998c23d8499 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Permission/Permission.ts @@ -0,0 +1,103 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, UriFactory } from "../../common"; +import { RequestOptions } from "../../request/RequestOptions"; +import { User } from "../User"; +import { PermissionBody } from "./PermissionBody"; +import { PermissionDefinition } from "./PermissionDefinition"; +import { PermissionResponse } from "./PermissionResponse"; + +/** + * Use to read, replace, or delete a given {@link Permission} by id. + * + * @see {@link Permissions} to create, upsert, query, or read all Permissions. + */ +export class Permission { + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return UriFactory.createPermissionUri(this.user.database.id, this.user.id, this.id); + } + /** + * @hidden + * @param user The parent {@link User}. + * @param id The id of the given {@link Permission}. + */ + constructor(public readonly user: User, public readonly id: string, private readonly clientContext: ClientContext) {} + + /** + * Read the {@link PermissionDefinition} of the given {@link Permission}. + * @param options + */ + public async read(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.read( + path, + "permissions", + id, + undefined, + options + ); + return { + body: response.result, + headers: response.headers, + ref: this, + permission: this + }; + } + + /** + * Replace the given {@link Permission} with the specified {@link PermissionDefinition}. + * @param body The specified {@link PermissionDefinition}. + * @param options + */ + public async replace(body: PermissionDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.replace( + body, + path, + "permissions", + id, + undefined, + options + ); + return { + body: response.result, + headers: response.headers, + ref: this, + permission: this + }; + } + + /** + * Delete the given {@link Permission}. + * @param options + */ + public async delete(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.delete( + path, + "permissions", + id, + undefined, + options + ); + return { + body: response.result, + headers: response.headers, + ref: this, + permission: this + }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Permission/PermissionBody.ts b/sdk/cosmosdb/cosmos/src/client/Permission/PermissionBody.ts new file mode 100644 index 000000000000..028046fda93e --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Permission/PermissionBody.ts @@ -0,0 +1,6 @@ +import { Resource } from "../Resource"; + +export interface PermissionBody { + /** System generated resource token for the particular resource and user */ + _token: string; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Permission/PermissionDefinition.ts b/sdk/cosmosdb/cosmos/src/client/Permission/PermissionDefinition.ts new file mode 100644 index 000000000000..b625065fccfa --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Permission/PermissionDefinition.ts @@ -0,0 +1,11 @@ +import { PermissionMode } from "../../documents"; + +export interface PermissionDefinition { + /** The id of the permission */ + id: string; + /** The mode of the permission, must be a value of {@link PermissionMode} */ + permissionMode: PermissionMode; + /** The link of the resource that the permission will be applied to. */ + resource: string; + resourcePartitionKey?: string | any[]; // TODO: what's allowed here? +} diff --git a/sdk/cosmosdb/cosmos/src/client/Permission/PermissionResponse.ts b/sdk/cosmosdb/cosmos/src/client/Permission/PermissionResponse.ts new file mode 100644 index 000000000000..1708c4d8f8ad --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Permission/PermissionResponse.ts @@ -0,0 +1,11 @@ +import { CosmosResponse } from "../../request"; +import { Resource } from "../Resource"; +import { Permission } from "./Permission"; +import { PermissionBody } from "./PermissionBody"; +import { PermissionDefinition } from "./PermissionDefinition"; + +export interface PermissionResponse + extends CosmosResponse { + /** A reference to the {@link Permission} corresponding to the returned {@link PermissionDefinition}. */ + permission: Permission; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Permission/Permissions.ts b/sdk/cosmosdb/cosmos/src/client/Permission/Permissions.ts new file mode 100644 index 000000000000..86e28657a0cd --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Permission/Permissions.ts @@ -0,0 +1,122 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper } from "../../common"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions, RequestOptions } from "../../request"; +import { Resource } from "../Resource"; +import { User } from "../User"; +import { Permission } from "./Permission"; +import { PermissionBody } from "./PermissionBody"; +import { PermissionDefinition } from "./PermissionDefinition"; +import { PermissionResponse } from "./PermissionResponse"; + +/** + * Use to create, replace, query, and read all Permissions. + * + * @see {@link Permission} to read, replace, or delete a specific permission by id. + */ +export class Permissions { + /** + * @hidden + * @param user The parent {@link User}. + */ + constructor(public readonly user: User, private readonly clientContext: ClientContext) {} + + /** + * Query all permissions. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Query all permissions. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator { + const path = Helper.getPathFromLink(this.user.url, "permissions"); + const id = Helper.getIdFromLink(this.user.url); + + return new QueryIterator(this.clientContext, query, options, innerOptions => { + return this.clientContext.queryFeed(path, "permissions", id, result => result.Permissions, query, innerOptions); + }); + } + + /** + * Read all permissions. + * @param options + * @example Read all permissions to array. + * ```typescript + * const {body: permissionList} = await user.permissions.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } + + /** + * Create a permission. + * + * A permission represents a per-User Permission to access a specific resource + * e.g. Item or Container. + * @param body Represents the body of the permission. + */ + public async create(body: PermissionDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.user.url, "permissions"); + const id = Helper.getIdFromLink(this.user.url); + + const response = await this.clientContext.create( + body, + path, + "permissions", + id, + undefined, + options + ); + const ref = new Permission(this.user, response.result.id, this.clientContext); + return { + body: response.result, + headers: response.headers, + ref, + permission: ref + }; + } + + /** + * Upsert a permission. + * + * A permission represents a per-User Permission to access a + * specific resource e.g. Item or Container. + */ + public async upsert(body: PermissionDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.user.url, "permissions"); + const id = Helper.getIdFromLink(this.user.url); + + const response = await this.clientContext.upsert( + body, + path, + "permissions", + id, + undefined, + options + ); + const ref = new Permission(this.user, response.result.id, this.clientContext); + return { + body: response.result, + headers: response.headers, + ref, + permission: ref + }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Permission/index.ts b/sdk/cosmosdb/cosmos/src/client/Permission/index.ts new file mode 100644 index 000000000000..4193883635de --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Permission/index.ts @@ -0,0 +1,4 @@ +export { Permission } from "./Permission"; +export { Permissions } from "./Permissions"; +export { PermissionDefinition } from "./PermissionDefinition"; +export { PermissionResponse } from "./PermissionResponse"; diff --git a/sdk/cosmosdb/cosmos/src/client/Resource.ts b/sdk/cosmosdb/cosmos/src/client/Resource.ts new file mode 100644 index 000000000000..88894835a3a5 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Resource.ts @@ -0,0 +1,12 @@ +export interface Resource { + /** Required. User settable property. Unique name that identifies the item, that is, no two items share the same ID within a database. The id must not exceed 255 characters. */ + id: string; + /** System generated property. The resource ID (_rid) is a unique identifier that is also hierarchical per the resource stack on the resource model. It is used internally for placement and navigation of the item resource. */ + _rid: string; + /** System generated property. Specifies the last updated timestamp of the resource. The value is a timestamp. */ + _ts: number; + /** System generated property. The unique addressable URI for the resource. */ + _self: string; + /** System generated property. Represents the resource etag required for optimistic concurrency control. */ + _etag: string; +} diff --git a/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedure.ts b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedure.ts new file mode 100644 index 000000000000..eab8410fb9d1 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedure.ts @@ -0,0 +1,106 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, UriFactory } from "../../common"; +import { CosmosResponse, RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { StoredProcedureDefinition } from "./StoredProcedureDefinition"; +import { StoredProcedureResponse } from "./StoredProcedureResponse"; + +/** + * Operations for reading, replacing, deleting, or executing a specific, existing stored procedure by id. + * + * For operations to create, upsert, read all, or query Stored Procedures, + */ +export class StoredProcedure { + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return UriFactory.createStoredProcedureUri(this.container.database.id, this.container.id, this.id); + } + /** + * Creates a new instance of {@link StoredProcedure} linked to the parent {@link Container}. + * @param container The parent {@link Container}. + * @param id The id of the given {@link StoredProcedure}. + * @hidden + */ + constructor( + public readonly container: Container, + public readonly id: string, + private readonly clientContext: ClientContext + ) {} + + /** + * Read the {@link StoredProcedureDefinition} for the given {@link StoredProcedure}. + * @param options + */ + public async read(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + const response = await this.clientContext.read(path, "sprocs", id, undefined, options); + + return { body: response.result, headers: response.headers, ref: this, storedProcedure: this, sproc: this }; + } + + /** + * Replace the given {@link StoredProcedure} with the specified {@link StoredProcedureDefinition}. + * @param body The specified {@link StoredProcedureDefinition} to replace the existing definition. + * @param options + */ + public async replace(body: StoredProcedureDefinition, options?: RequestOptions): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.replace( + body, + path, + "sprocs", + id, + undefined, + options + ); + + return { body: response.result, headers: response.headers, ref: this, storedProcedure: this, sproc: this }; + } + + /** + * Delete the given {@link StoredProcedure}. + * @param options + */ + public async delete(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.delete(path, "sprocs", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, storedProcedure: this, sproc: this }; + } + + /** + * Execute the given {@link StoredProcedure}. + * @param params Array of parameters to pass as arguments to the given {@link StoredProcedure}. + * @param options Additional options, such as the partition key to invoke the {@link StoredProcedure} on. + */ + public async execute(params?: any[], options?: RequestOptions): Promise>; + /** + * Execute the given {@link StoredProcedure}. + * + * The specified type, T, is not enforced by the client. + * Be sure to validate the response from the stored procedure matches the type, T, you provide. + * + * @param params Array of parameters to pass as arguments to the given {@link StoredProcedure}. + * @param options Additional options, such as the partition key to invoke the {@link StoredProcedure} on. + */ + public async execute(params?: any[], options?: RequestOptions): Promise>; + public async execute(params?: any[], options?: RequestOptions): Promise> { + const response = await this.clientContext.execute(this.url, params, options); + return { body: response.result, headers: response.headers, ref: this }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedureDefinition.ts b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedureDefinition.ts new file mode 100644 index 000000000000..ad5586edd881 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedureDefinition.ts @@ -0,0 +1,10 @@ +export interface StoredProcedureDefinition { + /** + * The id of the {@link StoredProcedure}. + */ + id?: string; + /** + * The body of the {@link StoredProcedure}. This is a JavaScript function. + */ + body?: string | ((...inputs: any[]) => void); +} diff --git a/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedureResponse.ts b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedureResponse.ts new file mode 100644 index 000000000000..7e840cf6143c --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedureResponse.ts @@ -0,0 +1,18 @@ +import { CosmosResponse } from "../../request"; +import { Resource } from "../Resource"; +import { StoredProcedure } from "./StoredProcedure"; +import { StoredProcedureDefinition } from "./StoredProcedureDefinition"; + +export interface StoredProcedureResponse extends CosmosResponse { + /** + * A reference to the {@link StoredProcedure} which the {@link StoredProcedureDefinition} corresponds to. + */ + storedProcedure: StoredProcedure; + + /** + * Alias for storedProcedure. + * + * A reference to the {@link StoredProcedure} which the {@link StoredProcedureDefinition} corresponds to. + */ + sproc: StoredProcedure; +} diff --git a/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedures.ts b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedures.ts new file mode 100644 index 000000000000..d687111f77b9 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/StoredProcedures.ts @@ -0,0 +1,145 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper } from "../../common"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions, RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { Resource } from "../Resource"; +import { StoredProcedure } from "./StoredProcedure"; +import { StoredProcedureDefinition } from "./StoredProcedureDefinition"; +import { StoredProcedureResponse } from "./StoredProcedureResponse"; + +/** + * Operations for creating, upserting, or reading/querying all Stored Procedures. + * + * For operations to read, replace, delete, or execute a specific, existing stored procedure by id, see `container.storedProcedure()`. + */ +export class StoredProcedures { + /** + * @param container The parent {@link Container}. + * @hidden + */ + constructor(public readonly container: Container, private readonly clientContext: ClientContext) {} + + /** + * Query all Stored Procedures. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + * @example Read all stored procedures to array. + * ```typescript + * const querySpec: SqlQuerySpec = { + * query: "SELECT * FROM root r WHERE r.id = @sproc", + * parameters: [ + * {name: "@sproc", value: "Todo"} + * ] + * }; + * const {body: sprocList} = await containers.storedProcedures.query(querySpec).toArray(); + * ``` + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Query all Stored Procedures. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + * @example Read all stored procedures to array. + * ```typescript + * const querySpec: SqlQuerySpec = { + * query: "SELECT * FROM root r WHERE r.id = @sproc", + * parameters: [ + * {name: "@sproc", value: "Todo"} + * ] + * }; + * const {body: sprocList} = await containers.storedProcedures.query(querySpec).toArray(); + * ``` + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator { + const path = Helper.getPathFromLink(this.container.url, "sprocs"); + const id = Helper.getIdFromLink(this.container.url); + + return new QueryIterator(this.clientContext, query, options, innerOptions => { + return this.clientContext.queryFeed(path, "sprocs", id, result => result.StoredProcedures, query, innerOptions); + }); + } + + /** + * Read all stored procedures. + * @param options + * @example Read all stored procedures to array. + * ```typescript + * const {body: sprocList} = await containers.storedProcedures.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } + + /** + * Create a StoredProcedure. + * + * Azure Cosmos DB allows stored procedures to be executed in the storage tier, + * directly against an item container. The script + * gets executed under ACID transactions on the primary storage partition of the + * specified container. For additional details, + * refer to the server-side JavaScript API documentation. + */ + public async create(body: StoredProcedureDefinition, options?: RequestOptions): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.container.url, "sprocs"); + const id = Helper.getIdFromLink(this.container.url); + + const response = await this.clientContext.create( + body, + path, + "sprocs", + id, + undefined, + options + ); + const ref = new StoredProcedure(this.container, response.result.id, this.clientContext); + return { body: response.result, headers: response.headers, ref, storedProcedure: ref, sproc: ref }; + } + + /** + * Upsert a StoredProcedure. + * + * Azure Cosmos DB allows stored procedures to be executed in the storage tier, + * directly against a document container. The script + * gets executed under ACID transactions on the primary storage partition of the + * specified container. For additional details, + * refer to the server-side JavaScript API documentation. + * + */ + public async upsert(body: StoredProcedureDefinition, options?: RequestOptions): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.container.url, "sprocs"); + const id = Helper.getIdFromLink(this.container.url); + + const response = await this.clientContext.upsert( + body, + path, + "sprocs", + id, + undefined, + options + ); + const ref = new StoredProcedure(this.container, response.result.id, this.clientContext); + return { body: response.result, headers: response.headers, ref, storedProcedure: ref, sproc: ref }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/StoredProcedure/index.ts b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/index.ts new file mode 100644 index 000000000000..07c3a4e83744 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/StoredProcedure/index.ts @@ -0,0 +1,4 @@ +export { StoredProcedure } from "./StoredProcedure"; +export { StoredProcedures } from "./StoredProcedures"; +export { StoredProcedureDefinition } from "./StoredProcedureDefinition"; +export { StoredProcedureResponse } from "./StoredProcedureResponse"; diff --git a/sdk/cosmosdb/cosmos/src/client/Trigger/Trigger.ts b/sdk/cosmosdb/cosmos/src/client/Trigger/Trigger.ts new file mode 100644 index 000000000000..fbf527867098 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Trigger/Trigger.ts @@ -0,0 +1,91 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, UriFactory } from "../../common"; +import { CosmosClient } from "../../CosmosClient"; +import { RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { TriggerDefinition } from "./TriggerDefinition"; +import { TriggerResponse } from "./TriggerResponse"; + +/** + * Operations to read, replace, or delete a {@link Trigger}. + * + * Use `container.triggers` to create, upsert, query, or read all. + */ +export class Trigger { + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return UriFactory.createTriggerUri(this.container.database.id, this.container.id, this.id); + } + + private client: CosmosClient; + + /** + * @hidden + * @param container The parent {@link Container}. + * @param id The id of the given {@link Trigger}. + */ + constructor( + public readonly container: Container, + public readonly id: string, + private readonly clientContext: ClientContext + ) { + this.client = this.container.database.client; + } + + /** + * Read the {@link TriggerDefinition} for the given {@link Trigger}. + * @param options + */ + public async read(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.read(path, "triggers", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, trigger: this }; + } + + /** + * Replace the given {@link Trigger} with the specified {@link TriggerDefinition}. + * @param body The specified {@link TriggerDefinition} to replace the existing definition with. + * @param options + */ + public async replace(body: TriggerDefinition, options?: RequestOptions): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.replace( + body, + path, + "triggers", + id, + undefined, + options + ); + + return { body: response.result, headers: response.headers, ref: this, trigger: this }; + } + + /** + * Delete the given {@link Trigger}. + * @param options + */ + public async delete(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.delete(path, "triggers", id, undefined, options); + + return { body: response.result, headers: response.headers, ref: this, trigger: this }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Trigger/TriggerDefinition.ts b/sdk/cosmosdb/cosmos/src/client/Trigger/TriggerDefinition.ts new file mode 100644 index 000000000000..861153476fa5 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Trigger/TriggerDefinition.ts @@ -0,0 +1,12 @@ +import { TriggerOperation, TriggerType } from "../../documents"; + +export interface TriggerDefinition { + /** The id of the trigger. */ + id?: string; + /** The body of the trigger, it can also be passed as a stringifed function */ + body: (() => void) | string; + /** The type of the trigger, should be one of the values of {@link TriggerType}. */ + triggerType: TriggerType; + /** The trigger operation, should be one of the values of {@link TriggerOperation}. */ + triggerOperation: TriggerOperation; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Trigger/TriggerResponse.ts b/sdk/cosmosdb/cosmos/src/client/Trigger/TriggerResponse.ts new file mode 100644 index 000000000000..23c1b9b89431 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Trigger/TriggerResponse.ts @@ -0,0 +1,9 @@ +import { Trigger } from "."; +import { CosmosResponse } from "../../request"; +import { Resource } from "../Resource"; +import { TriggerDefinition } from "./TriggerDefinition"; + +export interface TriggerResponse extends CosmosResponse { + /** A reference to the {@link Trigger} corresponding to the returned {@link TriggerDefinition}. */ + trigger: Trigger; +} diff --git a/sdk/cosmosdb/cosmos/src/client/Trigger/Triggers.ts b/sdk/cosmosdb/cosmos/src/client/Trigger/Triggers.ts new file mode 100644 index 000000000000..d1effc3abeac --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Trigger/Triggers.ts @@ -0,0 +1,111 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper } from "../../common"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions, RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { Resource } from "../Resource"; +import { Trigger } from "./Trigger"; +import { TriggerDefinition } from "./TriggerDefinition"; +import { TriggerResponse } from "./TriggerResponse"; + +/** + * Operations to create, upsert, query, and read all triggers. + * + * Use `container.triggers` to read, replace, or delete a {@link Trigger}. + */ +export class Triggers { + /** + * @hidden + * @param container The parent {@link Container}. + */ + constructor(public readonly container: Container, private readonly clientContext: ClientContext) {} + + /** + * Query all Triggers. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Query all Triggers. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator { + const path = Helper.getPathFromLink(this.container.url, "triggers"); + const id = Helper.getIdFromLink(this.container.url); + + return new QueryIterator(this.clientContext, query, options, innerOptions => { + return this.clientContext.queryFeed(path, "triggers", id, result => result.Triggers, query, innerOptions); + }); + } + + /** + * Read all Triggers. + * @param options + * @example Read all trigger to array. + * ```typescript + * const {body: triggerList} = await container.triggers.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } + /** + * Create a trigger. + * + * Azure Cosmos DB supports pre and post triggers defined in JavaScript to be executed + * on creates, updates and deletes. + * + * For additional details, refer to the server-side JavaScript API documentation. + * @param body + * @param options + */ + public async create(body: TriggerDefinition, options?: RequestOptions): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.container.url, "triggers"); + const id = Helper.getIdFromLink(this.container.url); + + const response = await this.clientContext.create(body, path, "triggers", id, undefined, options); + const ref = new Trigger(this.container, response.result.id, this.clientContext); + return { body: response.result, headers: response.headers, ref, trigger: ref }; + } + + /** + * Upsert a trigger. + * + * Azure Cosmos DB supports pre and post triggers defined in JavaScript to be + * executed on creates, updates and deletes. + * + * For additional details, refer to the server-side JavaScript API documentation. + * @param body + * @param options + */ + public async upsert(body: TriggerDefinition, options?: RequestOptions): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.container.url, "triggers"); + const id = Helper.getIdFromLink(this.container.url); + + const response = await this.clientContext.upsert(body, path, "triggers", id, undefined, options); + const ref = new Trigger(this.container, response.result.id, this.clientContext); + return { body: response.result, headers: response.headers, ref, trigger: ref }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/Trigger/index.ts b/sdk/cosmosdb/cosmos/src/client/Trigger/index.ts new file mode 100644 index 000000000000..226828b27417 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/Trigger/index.ts @@ -0,0 +1,4 @@ +export { Trigger } from "./Trigger"; +export { Triggers } from "./Triggers"; +export { TriggerDefinition } from "./TriggerDefinition"; +export { TriggerResponse } from "./TriggerResponse"; diff --git a/sdk/cosmosdb/cosmos/src/client/User/User.ts b/sdk/cosmosdb/cosmos/src/client/User/User.ts new file mode 100644 index 000000000000..e3aca0b129ff --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/User/User.ts @@ -0,0 +1,92 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, UriFactory } from "../../common"; +import { RequestOptions } from "../../request"; +import { Database } from "../Database"; +import { Permission, Permissions } from "../Permission"; +import { UserDefinition } from "./UserDefinition"; +import { UserResponse } from "./UserResponse"; + +/** + * Used to read, replace, and delete Users. + * + * Additionally, you can access the permissions for a given user via `user.permission` and `user.permissions`. + * + * @see {@link Users} to create, upsert, query, or read all. + */ +export class User { + /** + * Operations for creating, upserting, querying, or reading all operations. + * + * See `client.permission(id)` to read, replace, or delete a specific Permission by id. + */ + public readonly permissions: Permissions; + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return UriFactory.createUserUri(this.database.id, this.id); + } + /** + * @hidden + * @param database The parent {@link Database}. + * @param id + */ + constructor( + public readonly database: Database, + public readonly id: string, + private readonly clientContext: ClientContext + ) { + this.permissions = new Permissions(this, this.clientContext); + } + + /** + * Operations to read, replace, or delete a specific Permission by id. + * + * See `client.permissions` for creating, upserting, querying, or reading all operations. + * @param id + */ + public permission(id: string): Permission { + return new Permission(this, id, this.clientContext); + } + + /** + * Read the {@link UserDefinition} for the given {@link User}. + * @param options + */ + public async read(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + const response = await this.clientContext.read(path, "users", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, user: this }; + } + + /** + * Replace the given {@link User}'s definition with the specified {@link UserDefinition}. + * @param body The specified {@link UserDefinition} to replace the definition. + * @param options + */ + public async replace(body: UserDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.replace(body, path, "users", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, user: this }; + } + + /** + * Delete the given {@link User}. + * @param options + */ + public async delete(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.delete(path, "users", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, user: this }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/User/UserDefinition.ts b/sdk/cosmosdb/cosmos/src/client/User/UserDefinition.ts new file mode 100644 index 000000000000..a533497a843d --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/User/UserDefinition.ts @@ -0,0 +1,4 @@ +export interface UserDefinition { + /** The id of the user. */ + id?: string; +} diff --git a/sdk/cosmosdb/cosmos/src/client/User/UserResponse.ts b/sdk/cosmosdb/cosmos/src/client/User/UserResponse.ts new file mode 100644 index 000000000000..08960cd11e81 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/User/UserResponse.ts @@ -0,0 +1,9 @@ +import { CosmosResponse } from "../../request"; +import { Resource } from "../Resource"; +import { User } from "./User"; +import { UserDefinition } from "./UserDefinition"; + +export interface UserResponse extends CosmosResponse { + /** A reference to the {@link User} corresponding to the returned {@link UserDefinition}. */ + user: User; +} diff --git a/sdk/cosmosdb/cosmos/src/client/User/Users.ts b/sdk/cosmosdb/cosmos/src/client/User/Users.ts new file mode 100644 index 000000000000..14add0008c7f --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/User/Users.ts @@ -0,0 +1,93 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper } from "../../common"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions, RequestOptions } from "../../request"; +import { Database } from "../Database"; +import { Resource } from "../Resource"; +import { User } from "./User"; +import { UserDefinition } from "./UserDefinition"; +import { UserResponse } from "./UserResponse"; + +/** + * Used to create, upsert, query, and read all users. + * + * @see {@link User} to read, replace, or delete a specific User by id. + */ +export class Users { + /** + * @hidden + * @param database The parent {@link Database}. + */ + constructor(public readonly database: Database, private readonly clientContext: ClientContext) {} + + /** + * Query all users. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Query all users. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator { + const path = Helper.getPathFromLink(this.database.url, "users"); + const id = Helper.getIdFromLink(this.database.url); + + return new QueryIterator(this.clientContext, query, options, innerOptions => { + return this.clientContext.queryFeed(path, "users", id, result => result.Users, query, innerOptions); + }); + } + + /** + * Read all users. + * @param options + * @example Read all users to array. + * ```typescript + * const {body: usersList} = await database.users.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } + + /** + * Create a database user with the specified {@link UserDefinition}. + * @param body The specified {@link UserDefinition}. + * @param options + */ + public async create(body: UserDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.database.url, "users"); + const id = Helper.getIdFromLink(this.database.url); + const response = await this.clientContext.create(body, path, "users", id, undefined, options); + const ref = new User(this.database, response.result.id, this.clientContext); + return { body: response.result, headers: response.headers, ref, user: ref }; + } + + /** + * Upsert a database user with a specified {@link UserDefinition}. + * @param body The specified {@link UserDefinition}. + * @param options + */ + public async upsert(body: UserDefinition, options?: RequestOptions): Promise { + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.database.url, "users"); + const id = Helper.getIdFromLink(this.database.url); + + const response = await this.clientContext.upsert(body, path, "users", id, undefined, options); + const ref = new User(this.database, response.result.id, this.clientContext); + return { body: response.result, headers: response.headers, ref, user: ref }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/User/index.ts b/sdk/cosmosdb/cosmos/src/client/User/index.ts new file mode 100644 index 000000000000..1936bbed8b79 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/User/index.ts @@ -0,0 +1,4 @@ +export { User } from "./User"; +export { Users } from "./Users"; +export { UserDefinition } from "./UserDefinition"; +export { UserResponse } from "./UserResponse"; diff --git a/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunction.ts b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunction.ts new file mode 100644 index 000000000000..a2545b246cf7 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunction.ts @@ -0,0 +1,86 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper, UriFactory } from "../../common"; +import { RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { UserDefinedFunctionDefinition } from "./UserDefinedFunctionDefinition"; +import { UserDefinedFunctionResponse } from "./UserDefinedFunctionResponse"; + +/** + * Used to read, replace, or delete a specified User Definied Function by id. + * + * @see {@link UserDefinedFunction} to create, upsert, query, read all User Defined Functions. + */ +export class UserDefinedFunction { + /** + * Returns a reference URL to the resource. Used for linking in Permissions. + */ + public get url() { + return UriFactory.createUserDefinedFunctionUri(this.container.database.id, this.container.id, this.id); + } + /** + * @hidden + * @param container The parent {@link Container}. + * @param id The id of the given {@link UserDefinedFunction}. + */ + constructor( + public readonly container: Container, + public readonly id: string, + private readonly clientContext: ClientContext + ) {} + + /** + * Read the {@link UserDefinedFunctionDefinition} for the given {@link UserDefinedFunction}. + * @param options + */ + public async read(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.read(path, "udfs", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, userDefinedFunction: this, udf: this }; + } + + /** + * Replace the given {@link UserDefinedFunction} with the specified {@link UserDefinedFunctionDefinition}. + * @param body The specified {@link UserDefinedFunctionDefinition}. + * @param options + */ + public async replace( + body: UserDefinedFunctionDefinition, + options?: RequestOptions + ): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.replace( + body, + path, + "udfs", + id, + undefined, + options + ); + return { body: response.result, headers: response.headers, ref: this, userDefinedFunction: this, udf: this }; + } + + /** + * Delete the given {@link UserDefined}. + * @param options + */ + public async delete(options?: RequestOptions): Promise { + const path = Helper.getPathFromLink(this.url); + const id = Helper.getIdFromLink(this.url); + + const response = await this.clientContext.delete(path, "udfs", id, undefined, options); + return { body: response.result, headers: response.headers, ref: this, userDefinedFunction: this, udf: this }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctionDefinition.ts b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctionDefinition.ts new file mode 100644 index 000000000000..565552958a50 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctionDefinition.ts @@ -0,0 +1,6 @@ +export interface UserDefinedFunctionDefinition { + /** The id of the {@link UserDefinedFunction} */ + id?: string; + /** The body of the user defined function, it can also be passed as a stringifed function */ + body?: string | (() => void); +} diff --git a/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctionResponse.ts b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctionResponse.ts new file mode 100644 index 000000000000..182724973a9b --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctionResponse.ts @@ -0,0 +1,16 @@ +import { CosmosResponse } from "../../request"; +import { Resource } from "../Resource"; +import { UserDefinedFunction } from "./UserDefinedFunction"; +import { UserDefinedFunctionDefinition } from "./UserDefinedFunctionDefinition"; + +export interface UserDefinedFunctionResponse + extends CosmosResponse { + /** A reference to the {@link UserDefinedFunction} corresponding to the returned {@link UserDefinedFunctionDefinition}. */ + userDefinedFunction: UserDefinedFunction; + /** + * Alias for `userDefinedFunction(id). + * + * A reference to the {@link UserDefinedFunction} corresponding to the returned {@link UserDefinedFunctionDefinition}. + */ + udf: UserDefinedFunction; +} diff --git a/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctions.ts b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctions.ts new file mode 100644 index 000000000000..2e3e4ea6c76c --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/UserDefinedFunctions.ts @@ -0,0 +1,128 @@ +import { ClientContext } from "../../ClientContext"; +import { Helper } from "../../common"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { FeedOptions, RequestOptions } from "../../request"; +import { Container } from "../Container"; +import { Resource } from "../Resource"; +import { UserDefinedFunction } from "./UserDefinedFunction"; +import { UserDefinedFunctionDefinition } from "./UserDefinedFunctionDefinition"; +import { UserDefinedFunctionResponse } from "./UserDefinedFunctionResponse"; + +/** + * Used to create, upsert, query, or read all User Defined Functions. + * + * @see {@link UserDefinedFunction} to read, replace, or delete a given User Defined Function by id. + */ +export class UserDefinedFunctions { + /** + * @hidden + * @param container The parent {@link Container}. + */ + constructor(public readonly container: Container, private readonly clientContext: ClientContext) {} + + /** + * Query all User Defined Functions. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + /** + * Query all User Defined Functions. + * @param query Query configuration for the operation. See {@link SqlQuerySpec} for more info on how to configure a query. + * @param options + */ + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator; + public query(query: SqlQuerySpec, options?: FeedOptions): QueryIterator { + const path = Helper.getPathFromLink(this.container.url, "udfs"); + const id = Helper.getIdFromLink(this.container.url); + + return new QueryIterator(this.clientContext, query, options, innerOptions => { + return this.clientContext.queryFeed(path, "udfs", id, result => result.UserDefinedFunctions, query, innerOptions); + }); + } + + /** + * Read all User Defined Functions. + * @param options + * @example Read all User Defined Functions to array. + * ```typescript + * const {body: udfList} = await container.userDefinedFunctions.readAll().toArray(); + * ``` + */ + public readAll(options?: FeedOptions): QueryIterator { + return this.query(undefined, options); + } + + /** + * Create a UserDefinedFunction. + * + * Azure Cosmos DB supports JavaScript UDFs which can be used inside queries, stored procedures and triggers. + * + * For additional details, refer to the server-side JavaScript API documentation. + * + */ + public async create( + body: UserDefinedFunctionDefinition, + options?: RequestOptions + ): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.container.url, "udfs"); + const id = Helper.getIdFromLink(this.container.url); + + const response = await this.clientContext.create( + body, + path, + "udfs", + id, + undefined, + options + ); + const ref = new UserDefinedFunction(this.container, response.result.id, this.clientContext); + return { body: response.result, headers: response.headers, ref, userDefinedFunction: ref, udf: ref }; + } + + /** + * Upsert a UserDefinedFunction. + * + * Azure Cosmos DB supports JavaScript UDFs which can be used inside queries, stored procedures and triggers. + * + * For additional details, refer to the server-side JavaScript API documentation. + * + */ + public async upsert( + body: UserDefinedFunctionDefinition, + options?: RequestOptions + ): Promise { + if (body.body) { + body.body = body.body.toString(); + } + + const err = {}; + if (!Helper.isResourceValid(body, err)) { + throw err; + } + + const path = Helper.getPathFromLink(this.container.url, "udfs"); + const id = Helper.getIdFromLink(this.container.url); + + const response = await this.clientContext.upsert( + body, + path, + "udfs", + id, + undefined, + options + ); + const ref = new UserDefinedFunction(this.container, response.result.id, this.clientContext); + return { body: response.result, headers: response.headers, ref, userDefinedFunction: ref, udf: ref }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/index.ts b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/index.ts new file mode 100644 index 000000000000..120edaba4afa --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/UserDefinedFunction/index.ts @@ -0,0 +1,4 @@ +export { UserDefinedFunction } from "./UserDefinedFunction"; +export { UserDefinedFunctions } from "./UserDefinedFunctions"; +export { UserDefinedFunctionDefinition } from "./UserDefinedFunctionDefinition"; +export { UserDefinedFunctionResponse } from "./UserDefinedFunctionResponse"; diff --git a/sdk/cosmosdb/cosmos/src/client/index.ts b/sdk/cosmosdb/cosmos/src/client/index.ts new file mode 100644 index 000000000000..6559a2c2e851 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/client/index.ts @@ -0,0 +1,11 @@ +export * from "./Conflict"; +export * from "./Container"; +export * from "./Database"; +export * from "./Item"; +export * from "./Offer"; +export * from "./Permission"; +export * from "./StoredProcedure"; +export * from "./Trigger"; +export * from "./User"; +export * from "./UserDefinedFunction"; +export * from "./Resource"; diff --git a/sdk/cosmosdb/cosmos/src/common/constants.ts b/sdk/cosmosdb/cosmos/src/common/constants.ts new file mode 100644 index 000000000000..33a82ad016bd --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/common/constants.ts @@ -0,0 +1,263 @@ +import { version } from "../../package.json"; + +export const Constants = { + MediaTypes: { + Any: "*/*", + ImageJpeg: "image/jpeg", + ImagePng: "image/png", + Javascript: "application/x-javascript", + Json: "application/json", + OctetStream: "application/octet-stream", + QueryJson: "application/query+json", + SQL: "application/sql", + TextHtml: "text/html", + TextPlain: "text/plain", + Xml: "application/xml" + }, + + HttpMethods: { + Get: "GET", + Post: "POST", + Put: "PUT", + Delete: "DELETE", + Head: "HEAD", + Options: "OPTIONS" + }, + + HttpHeaders: { + Authorization: "authorization", + ETag: "etag", + MethodOverride: "X-HTTP-Method", + Slug: "Slug", + ContentType: "Content-Type", + LastModified: "Last-Modified", + ContentEncoding: "Content-Encoding", + CharacterSet: "CharacterSet", + UserAgent: "User-Agent", + IfModifiedSince: "If-Modified-Since", + IfMatch: "If-Match", + IfNoneMatch: "If-None-Match", + ContentLength: "Content-Length", + AcceptEncoding: "Accept-Encoding", + KeepAlive: "Keep-Alive", + CacheControl: "Cache-Control", + TransferEncoding: "Transfer-Encoding", + ContentLanguage: "Content-Language", + ContentLocation: "Content-Location", + ContentMd5: "Content-Md5", + ContentRange: "Content-Range", + Accept: "Accept", + AcceptCharset: "Accept-Charset", + AcceptLanguage: "Accept-Language", + IfRange: "If-Range", + IfUnmodifiedSince: "If-Unmodified-Since", + MaxForwards: "Max-Forwards", + ProxyAuthorization: "Proxy-Authorization", + AcceptRanges: "Accept-Ranges", + ProxyAuthenticate: "Proxy-Authenticate", + RetryAfter: "Retry-After", + SetCookie: "Set-Cookie", + WwwAuthenticate: "Www-Authenticate", + Origin: "Origin", + Host: "Host", + AccessControlAllowOrigin: "Access-Control-Allow-Origin", + AccessControlAllowHeaders: "Access-Control-Allow-Headers", + KeyValueEncodingFormat: "application/x-www-form-urlencoded", + WrapAssertionFormat: "wrap_assertion_format", + WrapAssertion: "wrap_assertion", + WrapScope: "wrap_scope", + SimpleToken: "SWT", + HttpDate: "date", + Prefer: "Prefer", + Location: "Location", + Referer: "referer", + A_IM: "A-IM", + + // Query + Query: "x-ms-documentdb-query", + IsQuery: "x-ms-documentdb-isquery", + + // Our custom Azure Cosmos DB headers + Continuation: "x-ms-continuation", + PageSize: "x-ms-max-item-count", + ItemCount: "x-ms-item-count", + + // Request sender generated. Simply echoed by backend. + ActivityId: "x-ms-activity-id", + PreTriggerInclude: "x-ms-documentdb-pre-trigger-include", + PreTriggerExclude: "x-ms-documentdb-pre-trigger-exclude", + PostTriggerInclude: "x-ms-documentdb-post-trigger-include", + PostTriggerExclude: "x-ms-documentdb-post-trigger-exclude", + IndexingDirective: "x-ms-indexing-directive", + SessionToken: "x-ms-session-token", + ConsistencyLevel: "x-ms-consistency-level", + XDate: "x-ms-date", + CollectionPartitionInfo: "x-ms-collection-partition-info", + CollectionServiceInfo: "x-ms-collection-service-info", + RetryAfterInMilliseconds: "x-ms-retry-after-ms", + IsFeedUnfiltered: "x-ms-is-feed-unfiltered", + ResourceTokenExpiry: "x-ms-documentdb-expiry-seconds", + EnableScanInQuery: "x-ms-documentdb-query-enable-scan", + EmitVerboseTracesInQuery: "x-ms-documentdb-query-emit-traces", + EnableCrossPartitionQuery: "x-ms-documentdb-query-enablecrosspartition", + ParallelizeCrossPartitionQuery: "x-ms-documentdb-query-parallelizecrosspartitionquery", + + // QueryMetrics + // Request header to tell backend to give you query metrics. + PopulateQueryMetrics: "x-ms-documentdb-populatequerymetrics", + // Response header that holds the serialized version of query metrics. + QueryMetrics: "x-ms-documentdb-query-metrics", + + // Version headers and values + Version: "x-ms-version", + + // Owner name + OwnerFullName: "x-ms-alt-content-path", + + // Owner ID used for name based request in session token. + OwnerId: "x-ms-content-path", + + // Partition Key + PartitionKey: "x-ms-documentdb-partitionkey", + PartitionKeyRangeID: "x-ms-documentdb-partitionkeyrangeid", + + // Quota Info + MaxEntityCount: "x-ms-root-entity-max-count", + CurrentEntityCount: "x-ms-root-entity-current-count", + CollectionQuotaInMb: "x-ms-collection-quota-mb", + CollectionCurrentUsageInMb: "x-ms-collection-usage-mb", + MaxMediaStorageUsageInMB: "x-ms-max-media-storage-usage-mb", + CurrentMediaStorageUsageInMB: "x-ms-media-storage-usage-mb", + RequestCharge: "x-ms-request-charge", + PopulateQuotaInfo: "x-ms-documentdb-populatequotainfo", + MaxResourceQuota: "x-ms-resource-quota", + + // Offer header + OfferType: "x-ms-offer-type", + OfferThroughput: "x-ms-offer-throughput", + + // Custom RUs/minute headers + DisableRUPerMinuteUsage: "x-ms-documentdb-disable-ru-per-minute-usage", + IsRUPerMinuteUsed: "x-ms-documentdb-is-ru-per-minute-used", + OfferIsRUPerMinuteThroughputEnabled: "x-ms-offer-is-ru-per-minute-throughput-enabled", + + // Index progress headers + IndexTransformationProgress: "x-ms-documentdb-collection-index-transformation-progress", + LazyIndexingProgress: "x-ms-documentdb-collection-lazy-indexing-progress", + + // Upsert header + IsUpsert: "x-ms-documentdb-is-upsert", + + // Sub status of the error + SubStatus: "x-ms-substatus", + + // StoredProcedure related headers + EnableScriptLogging: "x-ms-documentdb-script-enable-logging", + ScriptLogResults: "x-ms-documentdb-script-log-results", + + // Multi-Region Write + ALLOW_MULTIPLE_WRITES: "x-ms-cosmos-allow-tentative-writes" + }, + + // GlobalDB related constants + WritableLocations: "writableLocations", + ReadableLocations: "readableLocations", + Name: "name", + DatabaseAccountEndpoint: "databaseAccountEndpoint", + + // ServiceDocument Resource + ENABLE_MULTIPLE_WRITABLE_LOCATIONS: "enableMultipleWriteLocations", + + // Background refresh time + DefaultUnavailableLocationExpirationTimeMS: 5 * 60 * 1000, + + // Client generated retry count response header + ThrottleRetryCount: "x-ms-throttle-retry-count", + ThrottleRetryWaitTimeInMs: "x-ms-throttle-retry-wait-time-ms", + + CurrentVersion: "2018-06-18", + + SDKName: "azure-cosmos-js", + SDKVersion: version, + + DefaultPrecisions: { + DefaultNumberHashPrecision: 3, + DefaultNumberRangePrecision: -1, + DefaultStringHashPrecision: 3, + DefaultStringRangePrecision: -1 + }, + + ConsistentHashRing: { + DefaultVirtualNodesPerCollection: 128 + }, + + RegularExpressions: { + TrimLeftSlashes: new RegExp("^[/]+"), + TrimRightSlashes: new RegExp("[/]+$"), + IllegalResourceIdCharacters: new RegExp("[/\\\\?#]") + }, + + Quota: { + CollectionSize: "collectionSize" + }, + + Path: { + DatabasesPathSegment: "dbs", + CollectionsPathSegment: "colls", + UsersPathSegment: "users", + DocumentsPathSegment: "docs", + PermissionsPathSegment: "permissions", + StoredProceduresPathSegment: "sprocs", + TriggersPathSegment: "triggers", + UserDefinedFunctionsPathSegment: "udfs", + ConflictsPathSegment: "conflicts", + AttachmentsPathSegment: "attachments", + PartitionKeyRangesPathSegment: "pkranges", + SchemasPathSegment: "schemas", + OffersPathSegment: "offers", + TopologyPathSegment: "topology", + DatabaseAccountPathSegment: "databaseaccount" + }, + + OperationTypes: { + Create: "create", + Replace: "replace", + Upsert: "upsert", + Delete: "delete", + Read: "read", + Query: "query", + Execute: "execute" + }, + + PartitionKeyRange: { + // Partition Key Range Constants + MinInclusive: "minInclusive", + MaxExclusive: "maxExclusive", + Id: "id" + }, + + QueryRangeConstants: { + // Partition Key Range Constants + MinInclusive: "minInclusive", + MaxExclusive: "maxExclusive", + min: "min" + }, + + EffectiveParitionKeyConstants: { + MinimumInclusiveEffectivePartitionKey: "", + MaximumExclusiveEffectivePartitionKey: "FF" + } +}; + +export enum ResourceType { + database = "dbs", + offer = "offers", + user = "users", + permission = "permissions", + container = "colls", + conflicts = "conflicts", + sproc = "sprocs", + udf = "udfs", + trigger = "triggers", + item = "docs" +} diff --git a/sdk/cosmosdb/cosmos/src/common/helper.ts b/sdk/cosmosdb/cosmos/src/common/helper.ts new file mode 100644 index 000000000000..937e6c240774 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/common/helper.ts @@ -0,0 +1,328 @@ +import { Constants } from "."; +import { IHeaders } from ".."; +import { ConnectionPolicy } from "../documents"; +import { RequestContext } from "../request/RequestContext"; + +/** @hidden */ +const Regexes = Constants.RegularExpressions; + +/** @hidden */ +export class Helper { + public static jsonStringifyAndEscapeNonASCII(arg: any) { + // TODO: better way for this? Not sure. + // escapes non-ASCII characters as \uXXXX + return JSON.stringify(arg).replace(/[\u0080-\uFFFF]/g, m => { + return "\\u" + ("0000" + m.charCodeAt(0).toString(16)).slice(-4); + }); + } + + public static parseLink(resourcePath: string) { + if (resourcePath.length === 0) { + /* for DatabaseAccount case, both type and objectBody will be undefined. */ + return { + type: undefined, + objectBody: undefined + }; + } + + if (resourcePath[resourcePath.length - 1] !== "/") { + resourcePath = resourcePath + "/"; + } + + if (resourcePath[0] !== "/") { + resourcePath = "/" + resourcePath; + } + + /* + The path will be in the form of /[resourceType]/[resourceId]/ .... + /[resourceType]//[resourceType]/[resourceId]/ .... /[resourceType]/[resourceId]/ + or /[resourceType]/[resourceId]/ .... /[resourceType]/[resourceId]/[resourceType]/[resourceId]/ .... + /[resourceType]/[resourceId]/ + The result of split will be in the form of + [[[resourceType], [resourceId] ... ,[resourceType], [resourceId], ""] + In the first case, to extract the resourceId it will the element before last ( at length -2 ) + and the type will be before it ( at length -3 ) + In the second case, to extract the resource type it will the element before last ( at length -2 ) + */ + const pathParts = resourcePath.split("/"); + let id; + let type; + if (pathParts.length % 2 === 0) { + // request in form /[resourceType]/[resourceId]/ .... /[resourceType]/[resourceId]. + id = pathParts[pathParts.length - 2]; + type = pathParts[pathParts.length - 3]; + } else { + // request in form /[resourceType]/[resourceId]/ .... /[resourceType]/. + id = pathParts[pathParts.length - 3]; + type = pathParts[pathParts.length - 2]; + } + + const result = { + type, + objectBody: { + id, + self: resourcePath + } + }; + + return result; + } + + public static isReadRequest(request: RequestContext): boolean { + return ( + request.operationType === Constants.OperationTypes.Read || + request.operationType === Constants.OperationTypes.Query + ); + } + + public static sleep(time: number): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, time); + }); + } + + public static getContainerLink(link: string) { + return link + .split("/") + .slice(0, 4) + .join("/"); + } + + public static trimSlashes(source: string) { + return source + .replace(Constants.RegularExpressions.TrimLeftSlashes, "") + .replace(Constants.RegularExpressions.TrimRightSlashes, ""); + } + + public static getHexaDigit() { + return Math.floor(Math.random() * 16).toString(16); + } + + public static setIsUpsertHeader(headers: IHeaders) { + if (headers === undefined || headers === null) { + throw new Error('The "headers" parameter must not be null or undefined'); + } + + if (!(headers instanceof Object)) { + throw new Error(`The "headers" parameter must be an instance of "Object". Actual type is: "${typeof headers}".`); + } + + (headers as IHeaders)[Constants.HttpHeaders.IsUpsert] = true; + } + + // TODO: replace with well known library? + public static generateGuidId() { + let id = ""; + + for (let i = 0; i < 8; i++) { + id += Helper.getHexaDigit(); + } + + id += "-"; + + for (let i = 0; i < 4; i++) { + id += Helper.getHexaDigit(); + } + + id += "-"; + + for (let i = 0; i < 4; i++) { + id += Helper.getHexaDigit(); + } + + id += "-"; + + for (let i = 0; i < 4; i++) { + id += Helper.getHexaDigit(); + } + + id += "-"; + + for (let i = 0; i < 12; i++) { + id += Helper.getHexaDigit(); + } + + return id; + } + + public static parsePath(path: string) { + const pathParts = []; + let currentIndex = 0; + + const throwError = () => { + throw new Error("Path " + path + " is invalid at index " + currentIndex); + }; + + const getEscapedToken = () => { + const quote = path[currentIndex]; + let newIndex = ++currentIndex; + + while (true) { + newIndex = path.indexOf(quote, newIndex); + if (newIndex === -1) { + throwError(); + } + + if (path[newIndex - 1] !== "\\") { + break; + } + + ++newIndex; + } + + const token = path.substr(currentIndex, newIndex - currentIndex); + currentIndex = newIndex + 1; + return token; + }; + + const getToken = () => { + const newIndex = path.indexOf("/", currentIndex); + let token = null; + if (newIndex === -1) { + token = path.substr(currentIndex); + currentIndex = path.length; + } else { + token = path.substr(currentIndex, newIndex - currentIndex); + currentIndex = newIndex; + } + + token = token.trim(); + return token; + }; + + while (currentIndex < path.length) { + if (path[currentIndex] !== "/") { + throwError(); + } + + if (++currentIndex === path.length) { + break; + } + + if (path[currentIndex] === '"' || path[currentIndex] === "'") { + pathParts.push(getEscapedToken()); + } else { + pathParts.push(getToken()); + } + } + + return pathParts; + } + public static isResourceValid(resource: any, err: any) { + // TODO: any TODO: code smell + if (resource.id) { + if (typeof resource.id !== "string") { + err.message = "Id must be a string."; + return false; + } + + if ( + resource.id.indexOf("/") !== -1 || + resource.id.indexOf("\\") !== -1 || + resource.id.indexOf("?") !== -1 || + resource.id.indexOf("#") !== -1 + ) { + err.message = "Id contains illegal chars."; + return false; + } + if (resource.id[resource.id.length - 1] === " ") { + err.message = "Id ends with a space."; + return false; + } + } + return true; + } + + /** @ignore */ + public static getIdFromLink(resourceLink: string, isNameBased: boolean = true) { + if (isNameBased) { + resourceLink = Helper.trimSlashes(resourceLink); + return resourceLink; + } else { + return Helper.parseLink(resourceLink).objectBody.id.toLowerCase(); + } + } + + /** @ignore */ + public static getPathFromLink(resourceLink: string, resourceType?: string, isNameBased: boolean = true) { + if (isNameBased) { + resourceLink = Helper.trimSlashes(resourceLink); + if (resourceType) { + return "/" + encodeURI(resourceLink) + "/" + resourceType; + } else { + return "/" + encodeURI(resourceLink); + } + } else { + if (resourceType) { + return "/" + resourceLink + resourceType + "/"; + } else { + return "/" + resourceLink; + } + } + } + public static isStringNullOrEmpty(inputString: string) { + // checks whether string is null, undefined, empty or only contains space + return !inputString || /^\s*$/.test(inputString); + } + + public static trimSlashFromLeftAndRight(inputString: string) { + if (typeof inputString !== "string") { + throw new Error("invalid input: input is not string"); + } + + return inputString.replace(Regexes.TrimLeftSlashes, "").replace(Regexes.TrimRightSlashes, ""); + } + + public static validateResourceId(resourceId: string) { + // if resourceId is not a string or is empty throw an error + if (typeof resourceId !== "string" || this.isStringNullOrEmpty(resourceId)) { + throw new Error("Resource Id must be a string and cannot be undefined, null or empty"); + } + + // if resourceId starts or ends with space throw an error + if (resourceId[resourceId.length - 1] === " ") { + throw new Error("Resource Id cannot end with space"); + } + + // if resource id contains illegal characters throw an error + if (Regexes.IllegalResourceIdCharacters.test(resourceId)) { + throw new Error("Illegal characters ['/', '\\', '?', '#'] cannot be used in resourceId"); + } + + return true; + } + + public static getResourceIdFromPath(resourcePath: string) { + if (!resourcePath || typeof resourcePath !== "string") { + return null; + } + + const trimmedPath = this.trimSlashFromLeftAndRight(resourcePath); + const pathSegments = trimmedPath.split("/"); + + // number of segments of a path must always be even + if (pathSegments.length % 2 !== 0) { + return null; + } + + return pathSegments[pathSegments.length - 1]; + } + + public static parseConnectionPolicy(policy: any): ConnectionPolicy { + if (!policy) { + return new ConnectionPolicy(); + } else if (policy instanceof ConnectionPolicy) { + return policy; + } else { + const connectionPolicy = new ConnectionPolicy(); + for (const key of Object.getOwnPropertyNames(connectionPolicy)) { + if ((policy as any)[key] !== undefined) { + (connectionPolicy as any)[key] = (policy as any)[key]; + } + } + return connectionPolicy; + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/common/index.ts b/sdk/cosmosdb/cosmos/src/common/index.ts new file mode 100644 index 000000000000..c62565958338 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/common/index.ts @@ -0,0 +1,5 @@ +export * from "./constants"; +export * from "./helper"; +export * from "./statusCodes"; +export * from "./uriFactory"; +export * from "./platform"; diff --git a/sdk/cosmosdb/cosmos/src/common/platform.ts b/sdk/cosmosdb/cosmos/src/common/platform.ts new file mode 100644 index 000000000000..e52428f94738 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/common/platform.ts @@ -0,0 +1,44 @@ +import * as os from "os"; +import { Constants } from "."; + +/** @hidden */ + +export class Platform { + public static getPlatformDefaultHeaders(): { [key: string]: string } { + const defaultHeaders: { [key: string]: string } = {}; + defaultHeaders[Constants.HttpHeaders.UserAgent] = Platform.getUserAgent(); + return defaultHeaders; + } + + public static getDecodedDataLength(encodedData: string): number { + const buffer = Buffer.from(encodedData, "base64"); + return buffer.length; + } + + public static getUserAgent() { + // gets the user agent in the following format + // "{OSName}/{OSVersion} Nodejs/{NodejsVersion} documentdb-nodejs-sdk/{SDKVersion}" + // for example: + // "linux/3.4.0+ Nodejs/v0.10.25 documentdb-nodejs-sdk/1.10.0" + // "win32/10.0.14393 Nodejs/v4.4.7 documentdb-nodejs-sdk/1.10.0" + const osName = Platform._getSafeUserAgentSegmentInfo(os.platform()); + const osVersion = Platform._getSafeUserAgentSegmentInfo(os.release()); + const nodejsVersion = Platform._getSafeUserAgentSegmentInfo(process.version); + + const userAgent = `${osName}/${osVersion} Nodejs/${nodejsVersion} ${Constants.SDKName}/${Constants.SDKVersion}`; + return userAgent; + } + + public static _getSafeUserAgentSegmentInfo(s: string) { + // catch null, undefined, etc + if (typeof s !== "string") { + s = "unknown"; + } + // remove all white spaces + s = s.replace(/\s+/g, ""); + if (!s) { + s = "unknown"; + } + return s; + } +} diff --git a/sdk/cosmosdb/cosmos/src/common/statusCodes.ts b/sdk/cosmosdb/cosmos/src/common/statusCodes.ts new file mode 100644 index 000000000000..9da422439934 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/common/statusCodes.ts @@ -0,0 +1,47 @@ +// tslint:disable:object-literal-key-quotes +export const StatusCodes = { + // Success + Ok: 200, + Created: 201, + Accepted: 202, + NoContent: 204, + NotModified: 304, + + // Client error + BadRequest: 400, + Unauthorized: 401, + Forbidden: 403, + NotFound: 404, + MethodNotAllowed: 405, + RequestTimeout: 408, + Conflict: 409, + Gone: 410, + PreconditionFailed: 412, + RequestEntityTooLarge: 413, + TooManyRequests: 429, + RetryWith: 449, + + // Server Error + InternalServerError: 500, + ServiceUnavailable: 503, + + // Operation pause and cancel. These are FAKE status codes for QOS logging purpose only. + OperationPaused: 1200, + OperationCancelled: 1201 +}; + +export const SubStatusCodes = { + Unknown: 0, + + // 400: Bad Request Substatus + CrossPartitionQueryNotServable: 1004, + + // 410: StatusCodeType_Gone: substatus + PartitionKeyRangeGone: 1002, + + // 404: NotFound Substatus + ReadSessionNotAvailable: 1002, + + // 403: Forbidden Substatus + WriteForbidden: 3 +}; diff --git a/sdk/cosmosdb/cosmos/src/common/uriFactory.ts b/sdk/cosmosdb/cosmos/src/common/uriFactory.ts new file mode 100644 index 000000000000..b78723de62d0 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/common/uriFactory.ts @@ -0,0 +1,226 @@ +import { Constants, Helper } from "."; + +/** @hidden */ +export class UriFactory { + /** + * Given a database id, this creates a database link. + * @param {string} databaseId -The database id + * @returns {string} -A database link in the format of dbs/{0} \ + * with {0} being a Uri escaped version of the databaseId + * @description Would be used when creating or deleting a DocumentCollection \ + * or a User in Azure Cosmos DB database service + */ + public static createDatabaseUri(databaseId: string) { + databaseId = Helper.trimSlashFromLeftAndRight(databaseId); + Helper.validateResourceId(databaseId); + + return Constants.Path.DatabasesPathSegment + "/" + databaseId; + } + + /** + * Given a database and collection id, this creates a collection link. + * @param {string} databaseId -The database id + * @param {string} collectionId -The collection id + * @returns {string} A collection link in the format of dbs/{0}/colls/{1} \ + * with {0} being a Uri escaped version of the databaseId and {1} being collectionId + * @description Would be used when updating or deleting a DocumentCollection, creating a \ + * Document, a StoredProcedure, a Trigger, a UserDefinedFunction, or when executing a query \ + * with CreateDocumentQuery in Azure Cosmos DB database service. + */ + public static createDocumentCollectionUri(databaseId: string, collectionId: string) { + collectionId = Helper.trimSlashFromLeftAndRight(collectionId); + Helper.validateResourceId(collectionId); + + return this.createDatabaseUri(databaseId) + "/" + Constants.Path.CollectionsPathSegment + "/" + collectionId; + } + + /** + * Given a database and user id, this creates a user link. + * @param {string} databaseId -The database id + * @param {string} userId -The user id + * @returns {string} A user link in the format of dbs/{0}/users/{1} \ + * with {0} being a Uri escaped version of the databaseId and {1} being userId + * @description Would be used when creating a Permission, or when replacing or deleting \ + * a User in Azure Cosmos DB database service + */ + public static createUserUri(databaseId: string, userId: string) { + userId = Helper.trimSlashFromLeftAndRight(userId); + Helper.validateResourceId(userId); + + return this.createDatabaseUri(databaseId) + "/" + Constants.Path.UsersPathSegment + "/" + userId; + } + + /** + * Given a database and collection id, this creates a collection link. + * @param {string} databaseId -The database id + * @param {string} collectionId -The collection id + * @param {string} documentId -The document id + * @returns {string} -A document link in the format of \ + * dbs/{0}/colls/{1}/docs/{2} with {0} being a Uri escaped version of \ + * the databaseId, {1} being collectionId and {2} being the documentId + * @description Would be used when creating an Attachment, or when replacing \ + * or deleting a Document in Azure Cosmos DB database service + */ + public static createDocumentUri(databaseId: string, collectionId: string, documentId: string) { + documentId = Helper.trimSlashFromLeftAndRight(documentId); + Helper.validateResourceId(documentId); + + return ( + this.createDocumentCollectionUri(databaseId, collectionId) + + "/" + + Constants.Path.DocumentsPathSegment + + "/" + + documentId + ); + } + + /** + * Given a database, collection and document id, this creates a document link. + * @param {string} databaseId -The database Id + * @param {string} userId -The user Id + * @param {string} permissionId - The permissionId + * @returns {string} A permission link in the format of dbs/{0}/users/{1}/permissions/{2} \ + * with {0} being a Uri escaped version of the databaseId, {1} being userId and {2} being permissionId + * @description Would be used when replacing or deleting a Permission in Azure Cosmos DB database service. + */ + public static createPermissionUri(databaseId: string, userId: string, permissionId: string) { + permissionId = Helper.trimSlashFromLeftAndRight(permissionId); + Helper.validateResourceId(permissionId); + + return this.createUserUri(databaseId, userId) + "/" + Constants.Path.PermissionsPathSegment + "/" + permissionId; + } + + /** + * Given a database, collection and stored proc id, this creates a stored proc link. + * @param {string} databaseId -The database Id + * @param {string} collectionId -The collection Id + * @param {string} storedProcedureId -The stored procedure Id + * @returns {string} -A stored procedure link in the format of \ + * dbs/{0}/colls/{1}/sprocs/{2} with {0} being a Uri escaped version of the databaseId, \ + * {1} being collectionId and {2} being the storedProcedureId + * @description Would be used when replacing, executing, or deleting a StoredProcedure in \ + * Azure Cosmos DB database service. + */ + public static createStoredProcedureUri(databaseId: string, collectionId: string, storedProcedureId: string) { + storedProcedureId = Helper.trimSlashFromLeftAndRight(storedProcedureId); + Helper.validateResourceId(storedProcedureId); + + return ( + UriFactory.createDocumentCollectionUri(databaseId, collectionId) + + "/" + + Constants.Path.StoredProceduresPathSegment + + "/" + + storedProcedureId + ); + } + + /** + * @summary Given a database, collection and trigger id, this creates a trigger link. + * @param {string} databaseId -The database Id + * @param {string} collectionId -The collection Id + * @param {string} triggerId -The trigger Id + * @returns {string} -A trigger link in the format of \ + * dbs/{0}/colls/{1}/triggers/{2} with {0} being a Uri escaped version of the databaseId, \ + * {1} being collectionId and {2} being the triggerId + * @description Would be used when replacing, executing, or deleting a Trigger in Azure Cosmos DB database service + */ + public static createTriggerUri(databaseId: string, collectionId: string, triggerId: string) { + triggerId = Helper.trimSlashFromLeftAndRight(triggerId); + Helper.validateResourceId(triggerId); + + return ( + this.createDocumentCollectionUri(databaseId, collectionId) + + "/" + + Constants.Path.TriggersPathSegment + + "/" + + triggerId + ); + } + + /** + * @summary Given a database, collection and udf id, this creates a udf link. + * @param {string} databaseId -The database Id + * @param {string} collectionId -The collection Id + * @param {string} udfId -The User Defined Function Id + * @returns {string} -A udf link in the format of dbs/{0}/colls/{1}/udfs/{2} \ + * with {0} being a Uri escaped version of the databaseId, {1} being collectionId and {2} being the udfId + * @description Would be used when replacing, executing, or deleting a UserDefinedFunction in \ + * Azure Cosmos DB database service + */ + public static createUserDefinedFunctionUri(databaseId: string, collectionId: string, udfId: string) { + udfId = Helper.trimSlashFromLeftAndRight(udfId); + Helper.validateResourceId(udfId); + + return ( + this.createDocumentCollectionUri(databaseId, collectionId) + + "/" + + Constants.Path.UserDefinedFunctionsPathSegment + + "/" + + udfId + ); + } + + /** + * @summary Given a database, collection and conflict id, this creates a conflict link. + * @param {string} databaseId -The database Id + * @param {string} collectionId -The collection Id + * @param {string} conflictId -The conflict Id + * @returns {string} -A conflict link in the format of dbs/{0}/colls/{1}/conflicts/{2} \ + * with {0} being a Uri escaped version of the databaseId, {1} being collectionId and {2} being the conflictId + * @description Would be used when creating a Conflict in Azure Cosmos DB database service. + */ + public static createConflictUri(databaseId: string, collectionId: string, conflictId: string) { + conflictId = Helper.trimSlashFromLeftAndRight(conflictId); + Helper.validateResourceId(conflictId); + + return ( + this.createDocumentCollectionUri(databaseId, collectionId) + + "/" + + Constants.Path.ConflictsPathSegment + + "/" + + conflictId + ); + } + + /** + * @summary Given a database, collection and conflict id, this creates a conflict link. + * @param {string} databaseId -The database Id + * @param {string} collectionId -The collection Id + * @param {string} documentId -The document Id\ + * @param {string} attachmentId -The attachment Id + * @returns {string} -A conflict link in the format of dbs/{0}/colls/{1}/conflicts/{2} \ + * with {0} being a Uri escaped version of the databaseId, {1} being collectionId and {2} being the conflictId + * @description Would be used when creating a Conflict in Azure Cosmos DB database service. + */ + public static createAttachmentUri( + databaseId: string, + collectionId: string, + documentId: string, + attachmentId: string + ) { + attachmentId = Helper.trimSlashFromLeftAndRight(attachmentId); + Helper.validateResourceId(attachmentId); + + return ( + this.createDocumentUri(databaseId, collectionId, documentId) + + "/" + + Constants.Path.AttachmentsPathSegment + + "/" + + attachmentId + ); + } + + /** + * @summary Given a database and collection, this creates a partition key ranges link in\ + * the Azure Cosmos DB database service. + * @param {string} databaseId -The database Id + * @param {string} collectionId -The collection Id + * @returns {string} -A partition key ranges link in the format of \ + * dbs/{0}/colls/{1}/pkranges with {0} being a Uri escaped version of the databaseId and {1} being collectionId + */ + public static createPartitionKeyRangesUri(databaseId: string, collectionId: string) { + return ( + this.createDocumentCollectionUri(databaseId, collectionId) + "/" + Constants.Path.PartitionKeyRangesPathSegment + ); + } +} diff --git a/sdk/cosmosdb/cosmos/src/documents/ConnectionMode.ts b/sdk/cosmosdb/cosmos/src/documents/ConnectionMode.ts new file mode 100644 index 000000000000..159c9e182967 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/ConnectionMode.ts @@ -0,0 +1,5 @@ +/** Determines the connection behavior of the CosmosClient. Note, we currently only support Gateway Mode. */ +export enum ConnectionMode { + /** Gateway mode talks to a intermediate gateway which handles the direct communicationi with your individual partitions. */ + Gateway = 0 +} diff --git a/sdk/cosmosdb/cosmos/src/documents/ConnectionPolicy.ts b/sdk/cosmosdb/cosmos/src/documents/ConnectionPolicy.ts new file mode 100644 index 000000000000..1ec78379b75b --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/ConnectionPolicy.ts @@ -0,0 +1,37 @@ +import { ConnectionMode, MediaReadMode } from "."; +import { RetryOptions } from "../retry"; +/** + * Represents the Connection policy associated with a CosmosClient in the Azure Cosmos DB database service. + */ +export class ConnectionPolicy { + private static readonly defaultRequestTimeout: number = 60000; + private static readonly defaultMediaRequestTimeout: number = 300000; + + /** Determines which mode to connect to Cosmos with. (Currently only supports Gateway option) */ + public ConnectionMode = ConnectionMode.Gateway; + /** Attachment content (aka media) download mode. Should be one of the values of {@link MediaReadMode} */ + public MediaReadMode: keyof typeof MediaReadMode = MediaReadMode.Buffered; + + /** Time to wait for response from network peer for attachment content (aka media) operations. Represented in milliseconds. */ + public MediaRequestTimeout = ConnectionPolicy.defaultMediaRequestTimeout; + /** Request timeout (time to wait for response from network peer). Represented in milliseconds. */ + public RequestTimeout = ConnectionPolicy.defaultRequestTimeout; + /** Flag to enable/disable automatic redirecting of requests based on read/write operations. */ + public EnableEndpointDiscovery = true; + /** List of azure regions to be used as preferred locations for read requests. */ + public PreferredLocations: string[] = []; + /** RetryOptions instance which defines several configurable properties used during retry. */ + public RetryOptions = new RetryOptions(); + /** + * Flag to disable SSL verification for the requests. SSL verification is enabled by default. Don't set this when targeting production endpoints. + * This is intended to be used only when targeting emulator endpoint to avoid failing your requests with SSL related error. + */ + public DisableSSLVerification = false; + /** Http/Https proxy url */ + public ProxyUrl = ""; + /** + * The flag that enables writes on any locations (regions) for geo-replicated database accounts in the Azure Cosmos DB service. + * Default is `false`. + */ + public UseMultipleWriteLocations: boolean = false; +} diff --git a/sdk/cosmosdb/cosmos/src/documents/ConsistencyLevel.ts b/sdk/cosmosdb/cosmos/src/documents/ConsistencyLevel.ts new file mode 100644 index 000000000000..413208702c73 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/ConsistencyLevel.ts @@ -0,0 +1,36 @@ +/** + * Represents the consistency levels supported for Azure Cosmos DB client operations.
+ * The requested ConsistencyLevel must match or be weaker than that provisioned for the database account. + * Consistency levels. + * + * Consistency levels by order of strength are Strong, BoundedStaleness, Session, Consistent Prefix, and Eventual. + * + * See https://aka.ms/cosmos-consistency for more detailed documentation on Consistency Levels. + */ +export enum ConsistencyLevel { + /** + * Strong Consistency guarantees that read operations always return the value that was last written. + */ + Strong = "Strong", + /** + * Bounded Staleness guarantees that reads are not too out-of-date. + * This can be configured based on number of operations (MaxStalenessPrefix) or time (MaxStalenessIntervalInSeconds). + */ + BoundedStaleness = "BoundedStaleness", + /** + * Session Consistency guarantees monotonic reads (you never read old data, then new, then old again), + * monotonic writes (writes are ordered) and read your writes (your writes are immediately visible to your reads) + * within any single session. + */ + Session = "Session", + /** + * Eventual Consistency guarantees that reads will return a subset of writes. + * All writes will be eventually be available for reads. + */ + Eventual = "Eventual", + /** + * ConsistentPrefix Consistency guarantees that reads will return some prefix of all writes with no gaps. + * All writes will be eventually be available for reads.` + */ + ConsistentPrefix = "ConsistentPrefix" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/DataType.ts b/sdk/cosmosdb/cosmos/src/documents/DataType.ts new file mode 100644 index 000000000000..e0490a2e23a0 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/DataType.ts @@ -0,0 +1,15 @@ +/** Defines a target data type of an index path specification in the Azure Cosmos DB service. */ +export enum DataType { + /** Represents a numeric data type. */ + Number = "Number", + /** Represents a string data type. */ + String = "String", + /** Represents a point data type. */ + Point = "Point", + /** Represents a line string data type. */ + LineString = "LineString", + /** Represents a polygon data type. */ + Polygon = "Polygon", + /** Represents a multi-polygon data type. */ + MultiPolygon = "MultiPolygon" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/DatabaseAccount.ts b/sdk/cosmosdb/cosmos/src/documents/DatabaseAccount.ts new file mode 100644 index 000000000000..5807dca2f795 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/DatabaseAccount.ts @@ -0,0 +1,59 @@ +import { ConsistencyLevel } from "."; +import { Constants } from "../common"; +import { IHeaders } from "../queryExecutionContext"; + +/** + * Represents a DatabaseAccount in the Azure Cosmos DB database service. + */ +export class DatabaseAccount { + /** The list of writable locations for a geo-replicated database account. */ + public readonly writableLocations: Location[] = []; + /** The list of readable locations for a geo-replicated database account. */ + public readonly readableLocations: Location[] = []; + /** The self-link for Databases in the databaseAccount. */ + public readonly DatabasesLink: string; + /** The self-link for Media in the databaseAccount. */ + public readonly MediaLink: string; + /** Attachment content (media) storage quota in MBs ( Retrieved from gateway ). */ + public readonly MaxMediaStorageUsageInMB: number; + /** + * Current attachment content (media) usage in MBs (Retrieved from gateway ) + * + * Value is returned from cached information updated periodically and is not guaranteed + * to be real time. + */ + public readonly CurrentMediaStorageUsageInMB: number; + /** Gets the UserConsistencyPolicy settings. */ + public readonly ConsistencyPolicy: ConsistencyLevel; + public readonly enableMultipleWritableLocations: boolean; + + // TODO: body - any + public constructor(body: { [key: string]: any }, headers: IHeaders) { + this.DatabasesLink = "/dbs/"; + this.MediaLink = "/media/"; + this.MaxMediaStorageUsageInMB = headers[Constants.HttpHeaders.MaxMediaStorageUsageInMB]; + this.CurrentMediaStorageUsageInMB = headers[Constants.HttpHeaders.CurrentMediaStorageUsageInMB]; + this.ConsistencyPolicy = body.UserConsistencyPolicy + ? (body.UserConsistencyPolicy.defaultConsistencyLevel as ConsistencyLevel) + : ConsistencyLevel.Session; + if (body[Constants.WritableLocations] && body.id !== "localhost") { + this.writableLocations = body[Constants.WritableLocations] as Location[]; + } + if (body[Constants.ReadableLocations] && body.id !== "localhost") { + this.readableLocations = body[Constants.ReadableLocations] as Location[]; + } + if (body[Constants.ENABLE_MULTIPLE_WRITABLE_LOCATIONS]) { + this.enableMultipleWritableLocations = + body[Constants.ENABLE_MULTIPLE_WRITABLE_LOCATIONS] === true || + body[Constants.ENABLE_MULTIPLE_WRITABLE_LOCATIONS] === "true"; + } + } +} + +/** + * Used to specify the locations that are available, read is index 1 and write is index 0. + */ +export interface Location { + name: string; + databaseAccountEndpoint: string; +} diff --git a/sdk/cosmosdb/cosmos/src/documents/Document.ts b/sdk/cosmosdb/cosmos/src/documents/Document.ts new file mode 100644 index 000000000000..506bd0fe8480 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/Document.ts @@ -0,0 +1,3 @@ +export interface Document { + [key: string]: any; +} diff --git a/sdk/cosmosdb/cosmos/src/documents/IndexKind.ts b/sdk/cosmosdb/cosmos/src/documents/IndexKind.ts new file mode 100644 index 000000000000..d86470a0f630 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/IndexKind.ts @@ -0,0 +1,17 @@ +/** + * Specifies the supported Index types. + */ +export enum IndexKind { + /** + * This is supplied for a path which has no sorting requirement. This kind of an index has better precision than corresponding range index. + */ + Hash = "Hash", + /** + * This is supplied for a path which requires sorting. + */ + Range = "Range", + /** + * This is supplied for a path which requires geospatial indexing. + */ + Spatial = "Spatial" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/IndexingMode.ts b/sdk/cosmosdb/cosmos/src/documents/IndexingMode.ts new file mode 100644 index 000000000000..7c67a1d96dd3 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/IndexingMode.ts @@ -0,0 +1,22 @@ +/** + * Specifies the supported indexing modes. + * @property Consistent + * @property Lazy + */ +export enum IndexingMode { + /** + * Index is updated synchronously with a create or update operation. + * + * With consistent indexing, query behavior is the same as the default consistency level for the container. + * The index is always kept up to date with the data. + */ + consistent = "consistent", + /** + * Index is updated asynchronously with respect to a create or update operation. + * + * With lazy indexing, queries are eventually consistent. The index is updated when the container is idle. + */ + lazy = "lazy", + /** No Index is provided. */ + none = "none" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/IndexingPolicy.ts b/sdk/cosmosdb/cosmos/src/documents/IndexingPolicy.ts new file mode 100644 index 000000000000..8fd1d01545e2 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/IndexingPolicy.ts @@ -0,0 +1,22 @@ +import { DataType, IndexingMode, IndexKind } from "."; + +export interface IndexingPolicy { + /** The indexing mode (consistent or lazy) {@link IndexingMode}. */ + indexingMode?: keyof typeof IndexingMode; + automatic?: boolean; + /** An array of {@link IncludedPath} represents the paths to be included for indexing. */ + includedPaths?: IndexedPath[]; + /** An array of {@link IncludedPath} represents the paths to be excluded for indexing. */ + excludedPaths?: IndexedPath[]; +} + +export interface IndexedPath { + path: string; + indexes?: Index[]; +} + +export interface Index { + kind: keyof typeof IndexKind; + dataType: keyof typeof DataType; + precision?: number; +} diff --git a/sdk/cosmosdb/cosmos/src/documents/MediaReadMode.ts b/sdk/cosmosdb/cosmos/src/documents/MediaReadMode.ts new file mode 100644 index 000000000000..41e6d29dfce5 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/MediaReadMode.ts @@ -0,0 +1,15 @@ +/** + * Enum for media read mode values. + */ +export enum MediaReadMode { + /** + * Content is buffered at the client and not directly streamed from the content store. + *

Use Buffered to reduce the time taken to read and write media files.

+ */ + Buffered = "Buffered", + /** + * Content is directly streamed from the content store without any buffering at the client. + *

Use Streamed to reduce the client memory overhead of reading and writing media files.

+ */ + Streamed = "Streamed" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/PartitionKey.ts b/sdk/cosmosdb/cosmos/src/documents/PartitionKey.ts new file mode 100644 index 000000000000..9f3d7a6a9631 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/PartitionKey.ts @@ -0,0 +1,4 @@ +import { Point, Range } from "../range"; +import { PartitionKeyDefinition } from "./PartitionKeyDefinition"; + +export type PartitionKey = PartitionKeyDefinition | Point | Range; diff --git a/sdk/cosmosdb/cosmos/src/documents/PartitionKeyDefinition.ts b/sdk/cosmosdb/cosmos/src/documents/PartitionKeyDefinition.ts new file mode 100644 index 000000000000..2f4e65a26d6a --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/PartitionKeyDefinition.ts @@ -0,0 +1,6 @@ +import { PartitionKind } from "."; + +export interface PartitionKeyDefinition { + paths: string[]; + kind: keyof typeof PartitionKind; +} diff --git a/sdk/cosmosdb/cosmos/src/documents/PartitionKind.ts b/sdk/cosmosdb/cosmos/src/documents/PartitionKind.ts new file mode 100644 index 000000000000..ee5a54b633da --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/PartitionKind.ts @@ -0,0 +1,3 @@ +export enum PartitionKind { + Hash = "Hash" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/PermissionMode.ts b/sdk/cosmosdb/cosmos/src/documents/PermissionMode.ts new file mode 100644 index 000000000000..c6484ecd551c --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/PermissionMode.ts @@ -0,0 +1,11 @@ +/** + * Enum for permission mode values. + */ +export enum PermissionMode { + /** Permission not valid. */ + None = "none", + /** Permission applicable for read operations only. */ + Read = "read", + /** Permission applicable for all operations. */ + All = "all" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/QueryCompatibilityMode.ts b/sdk/cosmosdb/cosmos/src/documents/QueryCompatibilityMode.ts new file mode 100644 index 000000000000..5354cfaaaacd --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/QueryCompatibilityMode.ts @@ -0,0 +1,6 @@ +// TODO: Should we remove this? +export enum QueryCompatibilityMode { + Default = 0, + Query = 1, + SqlQuery = 2 +} diff --git a/sdk/cosmosdb/cosmos/src/documents/TriggerOperation.ts b/sdk/cosmosdb/cosmos/src/documents/TriggerOperation.ts new file mode 100644 index 000000000000..4e6c6eb726eb --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/TriggerOperation.ts @@ -0,0 +1,16 @@ +/** + * Enum for trigger operation values. + * specifies the operations on which a trigger should be executed. + */ +export enum TriggerOperation { + /** All operations. */ + All = "all", + /** Create operations only. */ + Create = "create", + /** Update operations only. */ + Update = "update", + /** Delete operations only. */ + Delete = "delete", + /** Replace operations only. */ + Replace = "replace" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/TriggerType.ts b/sdk/cosmosdb/cosmos/src/documents/TriggerType.ts new file mode 100644 index 000000000000..00204000ae47 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/TriggerType.ts @@ -0,0 +1,10 @@ +/** + * Enum for trigger type values. + * Specifies the type of the trigger. + */ +export enum TriggerType { + /** Trigger should be executed before the associated operation(s). */ + Pre = "pre", + /** Trigger should be executed after the associated operation(s). */ + Post = "post" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/UserDefinedFunctionType.ts b/sdk/cosmosdb/cosmos/src/documents/UserDefinedFunctionType.ts new file mode 100644 index 000000000000..fcb1ac833783 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/UserDefinedFunctionType.ts @@ -0,0 +1,8 @@ +/** + * Enum for udf type values. + * Specifies the types of user defined functions. + */ +export enum UserDefinedFunctionType { + /** The User Defined Function is written in JavaScript. This is currently the only option. */ + Javascript = "Javascript" +} diff --git a/sdk/cosmosdb/cosmos/src/documents/index.ts b/sdk/cosmosdb/cosmos/src/documents/index.ts new file mode 100644 index 000000000000..d4dec283aa24 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/documents/index.ts @@ -0,0 +1,18 @@ +export * from "./ConnectionMode"; +export * from "./ConnectionPolicy"; +export * from "./ConsistencyLevel"; +export * from "./DatabaseAccount"; +export * from "./DataType"; +export * from "./Document"; +export * from "./IndexingMode"; +export * from "./IndexingPolicy"; +export * from "./IndexKind"; +export * from "./MediaReadMode"; +export * from "./PartitionKey"; +export * from "./PartitionKeyDefinition"; +export * from "./PartitionKind"; +export * from "./PermissionMode"; +export * from "./QueryCompatibilityMode"; +export * from "./TriggerOperation"; +export * from "./TriggerType"; +export * from "./UserDefinedFunctionType"; diff --git a/sdk/cosmosdb/cosmos/src/globalEndpointManager.ts b/sdk/cosmosdb/cosmos/src/globalEndpointManager.ts new file mode 100644 index 000000000000..4d588116df30 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/globalEndpointManager.ts @@ -0,0 +1,226 @@ +import * as url from "url"; +import { RequestOptions } from "."; +import { Constants, Helper } from "./common"; +import { CosmosClient } from "./CosmosClient"; +import { CosmosClientOptions } from "./CosmosClientOptions"; +import { DatabaseAccount } from "./documents"; +import { LocationCache } from "./LocationCache"; +import { CosmosResponse } from "./request"; +import { RequestContext } from "./request/RequestContext"; + +/** + * @hidden + * This internal class implements the logic for endpoint management for geo-replicated database accounts. + * @property {object} client - The document client instance. + * @property {string} defaultEndpoint - The endpoint used to create the client instance. + * @property {bool} enableEndpointDiscovery - Flag to enable/disable automatic redirecting of requests + * based on read/write operations. + * @property {Array} preferredLocations - List of azure regions to be used as preferred locations + * for read requests. + * @property {bool} isEndpointCacheInitialized - Flag to determine whether the endpoint cache is initialized or not. + */ +export class GlobalEndpointManager { + private defaultEndpoint: string; + public enableEndpointDiscovery: boolean; + private isEndpointCacheInitialized: boolean; + private locationCache: LocationCache; + private isRefreshing: boolean; + private readonly backgroundRefreshTimeIntervalInMS: number; + + /** + * @constructor GlobalEndpointManager + * @param {object} options - The document client instance. + */ + constructor( + options: CosmosClientOptions, + private readDatabaseAccount: (opts: RequestOptions) => Promise> + ) { + this.defaultEndpoint = options.endpoint; + this.enableEndpointDiscovery = options.connectionPolicy.EnableEndpointDiscovery; + this.isEndpointCacheInitialized = false; + this.locationCache = new LocationCache(options); + this.isRefreshing = false; + this.backgroundRefreshTimeIntervalInMS = Constants.DefaultUnavailableLocationExpirationTimeMS; + } + + /** + * Gets the current read endpoint from the endpoint cache. + */ + public async getReadEndpoint(): Promise { + if (!this.isEndpointCacheInitialized) { + await this.refreshEndpointList(); + } + return this.locationCache.getReadEndpoint(); + } + + /** + * Gets the current write endpoint from the endpoint cache. + */ + public async getWriteEndpoint(): Promise { + if (!this.isEndpointCacheInitialized) { + await this.refreshEndpointList(); + } + return this.locationCache.getWriteEndpoint(); + } + + public async getReadEndpoints(): Promise> { + if (!this.isEndpointCacheInitialized) { + await this.refreshEndpointList(); + } + return this.locationCache.getReadEndpoints(); + } + + public async getWriteEndpoints(): Promise> { + if (!this.isEndpointCacheInitialized) { + await this.refreshEndpointList(); + } + return this.locationCache.getWriteEndpoints(); + } + + public markCurrentLocationUnavailableForRead(endpoint: string) { + this.locationCache.markCurrentLocationUnavailableForRead(endpoint); + } + + public markCurrentLocationUnavailableForWrite(endpoint: string) { + this.locationCache.markCurrentLocationUnavailableForWrite(endpoint); + } + + public canUseMultipleWriteLocations(request: RequestContext) { + return this.locationCache.canUseMultipleWriteLocations(request); + } + + public async resolveServiceEndpoint(request: RequestContext) { + if (!this.isEndpointCacheInitialized) { + await this.refreshEndpointList(); + } + return this.locationCache.resolveServiceEndpoint(request); + } + + /** + * Refreshes the endpoint list by retrieving the writable and readable locations + * from the geo-replicated database account and then updating the locations cache. + * We skip the refreshing if EnableEndpointDiscovery is set to False + */ + public async refreshEndpointList(): Promise { + if (!this.isRefreshing) { + this.isRefreshing = true; + let shouldRefresh = false; + const databaseAccount = await this.getDatabaseAccountFromAnyEndpoint(); + if (databaseAccount) { + this.locationCache.onDatabaseAccountRead(databaseAccount); + } + + ({ shouldRefresh } = this.locationCache.shouldRefreshEndpoints()); + if (shouldRefresh) { + this.backgroundRefresh(); + return; + } else { + this.isRefreshing = false; + this.isEndpointCacheInitialized = true; + } + } + } + + private backgroundRefresh() { + process.nextTick(async () => { + this.isRefreshing = true; + let shouldRefresh = false; + try { + do { + const databaseAccount = await this.getDatabaseAccountFromAnyEndpoint(); + if (databaseAccount) { + this.locationCache.onDatabaseAccountRead(databaseAccount); + } + + ({ shouldRefresh } = this.locationCache.shouldRefreshEndpoints()); + if (!shouldRefresh) { + break; + } + await Helper.sleep(this.backgroundRefreshTimeIntervalInMS); + } while (shouldRefresh); + } catch (err) { + /* swallow error */ + // TODO: Tracing + } + this.isRefreshing = false; + this.isEndpointCacheInitialized = true; + }); + } + + /** + * Gets the database account first by using the default endpoint, and if that doesn't returns + * use the endpoints for the preferred locations in the order they are specified to get + * the database account. + * @memberof GlobalEndpointManager + * @instance + * @param {function} callback - The callback function which takes databaseAccount(object) as an argument. + */ + private async getDatabaseAccountFromAnyEndpoint(): Promise { + try { + const options = { urlConnection: this.defaultEndpoint }; + const { body: databaseAccount } = await this.readDatabaseAccount(options); + return databaseAccount; + // If for any reason(non - globaldb related), we are not able to get the database + // account from the above call to readDatabaseAccount, + // we would try to get this information from any of the preferred locations that the user + // might have specified (by creating a locational endpoint) + // and keeping eating the exception until we get the database account and return None at the end, + // if we are not able to get that info from any endpoints + } catch (err) { + // TODO: Tracing + } + + if (this.locationCache.prefferredLocations) { + for (const location of this.locationCache.prefferredLocations) { + try { + const locationalEndpoint = GlobalEndpointManager.getLocationalEndpoint(this.defaultEndpoint, location); + const options = { urlConnection: locationalEndpoint }; + const { body: databaseAccount } = await this.readDatabaseAccount(options); + if (databaseAccount) { + return databaseAccount; + } + } catch (err) { + // TODO: Tracing + } + } + } + } + + /** + * Gets the locational endpoint using the location name passed to it using the default endpoint. + * @memberof GlobalEndpointManager + * @instance + * @param {string} defaultEndpoint - The default endpoint to use for the endpoint. + * @param {string} locationName - The location name for the azure region like "East US". + */ + private static getLocationalEndpoint(defaultEndpoint: string, locationName: string) { + // For defaultEndpoint like 'https://contoso.documents.azure.com:443/' parse it to generate URL format + // This defaultEndpoint should be global endpoint(and cannot be a locational endpoint) + // and we agreed to document that + const endpointUrl = url.parse(defaultEndpoint, true, true); + + // hostname attribute in endpointUrl will return 'contoso.documents.azure.com' + if (endpointUrl.hostname) { + const hostnameParts = endpointUrl.hostname + .toString() + .toLowerCase() + .split("."); + if (hostnameParts) { + // globalDatabaseAccountName will return 'contoso' + const globalDatabaseAccountName = hostnameParts[0]; + + // Prepare the locationalDatabaseAccountName as contoso-EastUS for location_name 'East US' + const locationalDatabaseAccountName = globalDatabaseAccountName + "-" + locationName.replace(" ", ""); + + // Replace 'contoso' with 'contoso-EastUS' and + // return locationalEndpoint as https://contoso-EastUS.documents.azure.com:443/ + const locationalEndpoint = defaultEndpoint + .toLowerCase() + .replace(globalDatabaseAccountName, locationalDatabaseAccountName); + return locationalEndpoint; + } + } + + return null; + } +} diff --git a/sdk/cosmosdb/cosmos/src/index.ts b/sdk/cosmosdb/cosmos/src/index.ts new file mode 100644 index 000000000000..3364c07b688d --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/index.ts @@ -0,0 +1,35 @@ +import * as DocumentBase from "./documents"; +export { + ConnectionMode, + ConsistencyLevel, + ConnectionPolicy, + DatabaseAccount, + DataType, + Index, + IndexedPath, + IndexingMode, + IndexingPolicy, + IndexKind, + Location, + MediaReadMode, + PartitionKey, + PartitionKeyDefinition, + PartitionKind, + PermissionMode, + QueryCompatibilityMode, + TriggerOperation, + TriggerType, + UserDefinedFunctionType +} from "./documents"; + +export { UniqueKeyPolicy, UniqueKey } from "./client/Container/UniqueKeyPolicy"; +export { DocumentBase, DocumentBase as AzureDocuments }; +export { Constants, UriFactory } from "./common"; +export { RetryOptions } from "./retry"; +export { Response, RequestOptions, FeedOptions, MediaOptions, ErrorResponse } from "./request"; +export { IHeaders, SqlParameter, SqlQuerySpec } from "./queryExecutionContext"; +export { QueryIterator } from "./queryIterator"; +export * from "./queryMetrics"; +export { CosmosClient } from "./CosmosClient"; +export { CosmosClientOptions } from "./CosmosClientOptions"; +export * from "./client"; diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/AverageAggregator.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/AverageAggregator.ts new file mode 100644 index 000000000000..584f0b0333da --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/AverageAggregator.ts @@ -0,0 +1,42 @@ +import { IAggregator } from "./IAggregator"; + +/** @hidden */ +export interface IAverageAggregator { + sum: number; + count: number; +} + +/** @hidden */ +export class AverageAggregator implements IAverageAggregator, IAggregator { + public sum: number; + public count: number; + /** + * Add the provided item to aggregation result. + * @memberof AverageAggregator + * @instance + * @param other + */ + public aggregate(other: IAverageAggregator) { + if (other == null || other.sum == null) { + return; + } + if (this.sum == null) { + this.sum = 0.0; + this.count = 0; + } + this.sum += other.sum; + this.count += other.count; + } + + /** + * Get the aggregation result. + * @memberof AverageAggregator + * @instance + */ + public getResult() { + if (this.sum == null || this.count <= 0) { + return undefined; + } + return this.sum / this.count; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/CountAggregator.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/CountAggregator.ts new file mode 100644 index 000000000000..643238e68a98 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/CountAggregator.ts @@ -0,0 +1,32 @@ +import { IAggregator } from "./IAggregator"; + +/** @hidden */ +export class CountAggregator implements IAggregator { + public value: number; + /** + * Represents an aggregator for COUNT operator. + * @constructor CountAggregator + * @ignore + */ + constructor() { + this.value = 0; + } + /** + * Add the provided item to aggregation result. + * @memberof CountAggregator + * @instance + * @param other + */ + public aggregate(other: number) { + this.value += other; + } + + /** + * Get the aggregation result. + * @memberof CountAggregator + * @instance + */ + public getResult() { + return this.value; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/IAggregator.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/IAggregator.ts new file mode 100644 index 000000000000..cbc8625049b2 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/IAggregator.ts @@ -0,0 +1,5 @@ +/** @hidden */ +export interface IAggregator { + aggregate: (other: T) => void; + getResult: () => number; +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/MaxAggregator.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/MaxAggregator.ts new file mode 100644 index 000000000000..acbe634434ba --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/MaxAggregator.ts @@ -0,0 +1,39 @@ +import { OrderByDocumentProducerComparator } from "../orderByDocumentProducerComparator"; +import { IAggregator } from "./IAggregator"; + +/** @hidden */ +export class MaxAggregator implements IAggregator { + private value: number; + private comparer: OrderByDocumentProducerComparator; + /** + * Represents an aggregator for MAX operator. + * @constructor MaxAggregator + * @ignore + */ + constructor() { + this.value = undefined; + this.comparer = new OrderByDocumentProducerComparator(["Ascending"]); + } + /** + * Add the provided item to aggregation result. + * @memberof MaxAggregator + * @instance + * @param other + */ + public aggregate(other: number) { + if (this.value === undefined) { + this.value = other; + } else if (this.comparer.compareValue(other, typeof other, this.value, typeof this.value) > 0) { + this.value = other; + } + } + + /** + * Get the aggregation result. + * @memberof MaxAggregator + * @instance + */ + public getResult() { + return this.value; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/MinAggregator.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/MinAggregator.ts new file mode 100644 index 000000000000..e8c8516b1ffe --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/MinAggregator.ts @@ -0,0 +1,42 @@ +import { OrderByDocumentProducerComparator } from "../orderByDocumentProducerComparator"; +import { IAggregator } from "./IAggregator"; + +/** @hidden */ +export class MinAggregator implements IAggregator { + private value: number; + private comparer: OrderByDocumentProducerComparator; + /** + * Represents an aggregator for MIN operator. + * @constructor MinAggregator + * @ignore + */ + constructor() { + this.value = undefined; + this.comparer = new OrderByDocumentProducerComparator(["Ascending"]); + } + /** + * Add the provided item to aggregation result. + * @memberof MinAggregator + * @instance + * @param other + */ + public aggregate(other: number) { + if (this.value === undefined) { + this.value = other; + } else { + const otherType = other == null ? "NoValue" : typeof other; + if (this.comparer.compareValue(other, otherType, this.value, typeof this.value) < 0) { + this.value = other; + } + } + } + + /** + * Get the aggregation result. + * @memberof MinAggregator + * @instance + */ + public getResult() { + return this.value; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/SumAggregator.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/SumAggregator.ts new file mode 100644 index 000000000000..d38ba9462076 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/SumAggregator.ts @@ -0,0 +1,31 @@ +import { IAggregator } from "./IAggregator"; + +/** @hidden */ +export class SumAggregator implements IAggregator { + public sum: number; + /** + * Add the provided item to aggregation result. + * @memberof SumAggregator + * @instance + * @param other + */ + public aggregate(other: number) { + if (other === undefined) { + return; + } + if (this.sum === undefined) { + this.sum = other; + } else { + this.sum += other; + } + } + + /** + * Get the aggregation result. + * @memberof SumAggregator + * @instance + */ + public getResult() { + return this.sum; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/index.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/index.ts new file mode 100644 index 000000000000..bdc4f74e76b1 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/Aggregators/index.ts @@ -0,0 +1,6 @@ +export * from "./AverageAggregator"; +export * from "./CountAggregator"; +export * from "./MaxAggregator"; +export * from "./MinAggregator"; +export * from "./SumAggregator"; +export * from "./IAggregator"; diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/AggregateEndpointComponent.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/AggregateEndpointComponent.ts new file mode 100644 index 000000000000..aa43cc892e6e --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/AggregateEndpointComponent.ts @@ -0,0 +1,155 @@ +import { IHeaders } from ".."; +import { Response } from "../../request/request"; +import { AverageAggregator, CountAggregator, MaxAggregator, MinAggregator, SumAggregator } from "../Aggregators"; +import { IExecutionContext } from "../IExecutionContext"; +import { IEndpointComponent } from "./IEndpointComponent"; + +/** @hidden */ +export class AggregateEndpointComponent implements IEndpointComponent { + private toArrayTempResources: any[]; + private aggregateValues: any[]; + private aggregateValuesIndex: number; + private localAggregators: any[]; + + /** + * Represents an endpoint in handling aggregate queries. + * @constructor AggregateEndpointComponent + * @param { object } executionContext - Underlying Execution Context + * @ignore + */ + constructor(private executionContext: IExecutionContext, aggregateOperators: string[]) { + // TODO: any + this.executionContext = executionContext; + this.localAggregators = []; + aggregateOperators.forEach((aggregateOperator: string) => { + switch (aggregateOperator) { + case "Average": + this.localAggregators.push(new AverageAggregator()); + break; + case "Count": + this.localAggregators.push(new CountAggregator()); + break; + case "Max": + this.localAggregators.push(new MaxAggregator()); + break; + case "Min": + this.localAggregators.push(new MinAggregator()); + break; + case "Sum": + this.localAggregators.push(new SumAggregator()); + break; + } + }); + } + /** + * Populate the aggregated values + * @ignore + */ + private async _getAggregateResult(): Promise> { + this.toArrayTempResources = []; + this.aggregateValues = []; + this.aggregateValuesIndex = -1; + + try { + const { result: resources, headers } = await this._getQueryResults(); + + resources.forEach((resource: any) => { + // TODO: any + this.localAggregators.forEach(aggregator => { + let itemValue; + // Get the value of the first property if it exists + if (resource && Object.keys(resource).length > 0) { + const key = Object.keys(resource)[0]; + itemValue = resource[key]; + } + aggregator.aggregate(itemValue); + }); + }); + + // Get the aggregated results + this.localAggregators.forEach(aggregator => { + this.aggregateValues.push(aggregator.getResult()); + }); + + return { result: this.aggregateValues, headers }; + } catch (err) { + throw err; + } + } + + /** + * Get the results of queries from all partitions + * @ignore + */ + public async _getQueryResults(): Promise> { + try { + const { result: item, headers } = await this.executionContext.nextItem(); + if (item === undefined) { + // no more results + return { result: this.toArrayTempResources, headers }; + } + + this.toArrayTempResources = this.toArrayTempResources.concat(item); + return this._getQueryResults(); + } catch (err) { + throw err; + } + } + + /** + * Execute a provided function on the next element in the AggregateEndpointComponent. + * @memberof AggregateEndpointComponent + * @instance + * @param {callback} callback - Function to execute for each element. \ + * the function takes two parameters error, element. + */ + public async nextItem(): Promise> { + try { + let resHeaders: IHeaders; + let resources: any; + if (this.aggregateValues === undefined) { + ({ result: resources, headers: resHeaders } = await this._getAggregateResult()); + } + const resource = + this.aggregateValuesIndex < this.aggregateValues.length + ? this.aggregateValues[++this.aggregateValuesIndex] + : undefined; + + return { result: resource, headers: resHeaders }; + } catch (err) { + throw err; + } + } + + /** + * Retrieve the current element on the AggregateEndpointComponent. + * @memberof AggregateEndpointComponent + * @instance + * @param {callback} callback - Function to execute for the current element. \ + * the function takes two parameters error, element. + */ + public async current(): Promise> { + if (this.aggregateValues === undefined) { + const { result: resouces, headers } = await this._getAggregateResult(); + return { + result: this.aggregateValues[this.aggregateValuesIndex], + headers + }; + } else { + return { + result: this.aggregateValues[this.aggregateValuesIndex], + headers: undefined + }; + } + } + + /** + * Determine if there are still remaining resources to processs. + * @memberof AggregateEndpointComponent + * @instance + * @returns {Boolean} true if there is other elements to process in the AggregateEndpointComponent. + */ + public hasMoreResults() { + return this.aggregateValues != null && this.aggregateValuesIndex < this.aggregateValues.length - 1; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/IEndpointComponent.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/IEndpointComponent.ts new file mode 100644 index 000000000000..65f577197ab5 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/IEndpointComponent.ts @@ -0,0 +1,9 @@ +import { Response } from "../../request/request"; + +/** @hidden */ +export interface IEndpointComponent { + nextItem: () => Promise>; + current: () => Promise>; + hasMoreResults: () => boolean; + fetchMore?: () => Promise>; +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/OrderByEndpointComponent.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/OrderByEndpointComponent.ts new file mode 100644 index 000000000000..48604052354f --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/OrderByEndpointComponent.ts @@ -0,0 +1,62 @@ +import { Response } from "../../request/request"; +import { IExecutionContext } from "../IExecutionContext"; +import { IEndpointComponent } from "./IEndpointComponent"; + +/** @hidden */ +export class OrderByEndpointComponent implements IEndpointComponent { + /** + * Represents an endpoint in handling an order by query. For each processed orderby \ + * result it returns 'payload' item of the result + * @constructor OrderByEndpointComponent + * @param {object} executionContext - Underlying Execution Context + * @ignore + */ + constructor(private executionContext: IExecutionContext) {} + /** + * Execute a provided function on the next element in the OrderByEndpointComponent. + * @memberof OrderByEndpointComponent + * @instance + * @param {callback} callback - Function to execute for each element. the function \ + * takes two parameters error, element. + */ + public async nextItem(): Promise> { + try { + const { result: item, headers } = await this.executionContext.nextItem(); + return { + result: item !== undefined ? item.payload : undefined, + headers + }; + } catch (err) { + throw err; + } + } + + /** + * Retrieve the current element on the OrderByEndpointComponent. + * @memberof OrderByEndpointComponent + * @instance + * @param {callback} callback - Function to execute for the current element. \ + * the function takes two parameters error, element. + */ + public async current(): Promise> { + try { + const { result: item, headers } = await this.executionContext.current(); + return { + result: item !== undefined ? item.payload : undefined, + headers + }; + } catch (err) { + throw err; + } + } + + /** + * Determine if there are still remaining resources to processs. + * @memberof OrderByEndpointComponent + * @instance + * @returns {Boolean} true if there is other elements to process in the OrderByEndpointComponent. + */ + public hasMoreResults() { + return this.executionContext.hasMoreResults(); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/TopEndpointComponent.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/TopEndpointComponent.ts new file mode 100644 index 000000000000..28581e6e0ddd --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/TopEndpointComponent.ts @@ -0,0 +1,61 @@ +import { Response } from "../../request/request"; +import { IExecutionContext } from "../IExecutionContext"; +import { IEndpointComponent } from "./IEndpointComponent"; + +/** @hidden */ +export class TopEndpointComponent implements IEndpointComponent { + /** + * Represents an endpoint in handling top query. It only returns as many results as top arg specified. + * @constructor TopEndpointComponent + * @param { object } executionContext - Underlying Execution Context + * @ignore + */ + constructor(private executionContext: IExecutionContext, private topCount: number) {} + + /** + * Execute a provided function on the next element in the TopEndpointComponent. + * @memberof TopEndpointComponent + * @instance + * @param {callback} callback - Function to execute for each element. \ + * the function takes two parameters error, element. + */ + public async nextItem(): Promise> { + if (this.topCount <= 0) { + return { result: undefined, headers: undefined }; + } + this.topCount--; + try { + return this.executionContext.nextItem(); + } catch (err) { + throw err; + } + } + + /** + * Retrieve the current element on the TopEndpointComponent. + * @memberof TopEndpointComponent + * @instance + * @param {callback} callback - Function to execute for the current element. \ + * the function takes two parameters error, element. + */ + public async current(): Promise> { + if (this.topCount <= 0) { + return { result: undefined, headers: undefined }; + } + try { + return this.executionContext.current(); + } catch (err) { + throw err; + } + } + + /** + * Determine if there are still remaining resources to processs. + * @memberof TopEndpointComponent + * @instance + * @returns {Boolean} true if there is other elements to process in the TopEndpointComponent. + */ + public hasMoreResults() { + return this.topCount > 0 && this.executionContext.hasMoreResults(); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/index.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/index.ts new file mode 100644 index 000000000000..25d9d69a0a66 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/EndpointComponent/index.ts @@ -0,0 +1,4 @@ +export * from "./AggregateEndpointComponent"; +export * from "./IEndpointComponent"; +export * from "./OrderByEndpointComponent"; +export * from "./TopEndpointComponent"; diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/FetchResult.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/FetchResult.ts new file mode 100644 index 000000000000..6e1dfd1fa74e --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/FetchResult.ts @@ -0,0 +1,31 @@ +/** @hidden */ +export enum FetchResultType { + "Done" = 0, + "Exception" = 1, + "Result" = 2 +} + +/** @hidden */ +export class FetchResult { + public feedResponse: any; + public fetchResultType: FetchResultType; + public error: any; + /** + * Wraps fetch results for the document producer. + * This allows the document producer to buffer exceptions so that actual results don't get flushed during splits. + * @constructor DocumentProducer + * @param {object} feedReponse - The response the document producer got back on a successful fetch + * @param {object} error - The exception meant to be buffered on an unsuccessful fetch + * @ignore + */ + constructor(feedResponse: any, error: any) { + // TODO: feedResponse/error + if (feedResponse) { + this.feedResponse = feedResponse; + this.fetchResultType = FetchResultType.Result; + } else { + this.error = error; + this.fetchResultType = FetchResultType.Exception; + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/IExecutionContext.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/IExecutionContext.ts new file mode 100644 index 000000000000..d1a834292616 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/IExecutionContext.ts @@ -0,0 +1,9 @@ +import { Response } from "../request/request"; + +/** @hidden */ +export interface IExecutionContext { + nextItem: () => Promise>; + current: () => Promise>; + hasMoreResults: () => boolean; + fetchMore?: () => Promise>; // TODO: code smell +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/IHeaders.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/IHeaders.ts new file mode 100644 index 000000000000..8d27ec2e198c --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/IHeaders.ts @@ -0,0 +1,3 @@ +export interface IHeaders { + [key: string]: string | boolean | number; +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/SqlQuerySpec.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/SqlQuerySpec.ts new file mode 100644 index 000000000000..2330692f9535 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/SqlQuerySpec.ts @@ -0,0 +1,31 @@ +/** + * Represents a SQL query in the Azure Cosmos DB service. + * + * Queries with inputs should be parameterized to protect against SQL injection. + * + * @example Parameterized SQL Query + * ```typescript + * const query: SqlQuerySpec = { + * query: "SELECT * FROM Families f where f.lastName = @lastName", + * parameters: [ + * {name: "@lastName", value: "Wakefield"} + * ] + * }; + * ``` + */ +export interface SqlQuerySpec { + /** The text of the SQL query */ + query: string; + /** The parameters you provide in the query */ + parameters?: SqlParameter[]; +} + +/** + * Represents a parameter in a Parameterized SQL query, specified in {@link SqlQuerySpec} + */ +export interface SqlParameter { + /** Name of the parameter. (i.e. "@lastName") */ + name: string; + /** Value of the parameter (this is safe to come from users, assuming they are authorized) */ + value: string | number | boolean; +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/defaultQueryExecutionContext.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/defaultQueryExecutionContext.ts new file mode 100644 index 000000000000..309b358ccd10 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/defaultQueryExecutionContext.ts @@ -0,0 +1,203 @@ +import { IExecutionContext } from "."; +import { ClientContext } from "../ClientContext"; +import { Constants } from "../common"; +import { ClientSideMetrics, QueryMetrics } from "../queryMetrics"; +import { Response } from "../request"; +import { SqlQuerySpec } from "./SqlQuerySpec"; + +/** @hidden */ +export type FetchFunctionCallback = (options: any) => Promise>; + +/** @hidden */ +enum STATES { + start = "start", + inProgress = "inProgress", + ended = "ended" +} + +/** @hidden */ +export class DefaultQueryExecutionContext implements IExecutionContext { + private static readonly STATES = STATES; + private query: string | SqlQuerySpec; + private resources: any; // TODO: any resources + private currentIndex: number; + private currentPartitionIndex: number; + private fetchFunctions: FetchFunctionCallback[]; + private options: any; // TODO: any options + public continuation: any; // TODO: any continuation + private state: STATES; + /** + * Provides the basic Query Execution Context. + * This wraps the internal logic query execution using provided fetch functions + * @constructor DefaultQueryExecutionContext + * @param {ClientContext} clientContext - Is used to read the partitionKeyRanges for split proofing + * @param {SqlQuerySpec | string} query - A SQL query. + * @param {FeedOptions} [options] - Represents the feed options. + * @param {callback | callback[]} fetchFunctions - A function to retrieve each page of data. + * An array of functions may be used to query more than one partition. + * @ignore + */ + constructor( + private clientContext: ClientContext, + query: string | SqlQuerySpec, + options: any, + fetchFunctions: FetchFunctionCallback | FetchFunctionCallback[] + ) { + // TODO: any options + this.query = query; + this.resources = []; + this.currentIndex = 0; + this.currentPartitionIndex = 0; + this.fetchFunctions = Array.isArray(fetchFunctions) ? fetchFunctions : [fetchFunctions]; + this.options = options || {}; + this.continuation = this.options.continuation || null; + this.state = DefaultQueryExecutionContext.STATES.start; + } + + /** + * Execute a provided callback on the next element in the execution context. + * @memberof DefaultQueryExecutionContext + * @instance + */ + public async nextItem(): Promise> { + ++this.currentIndex; + const response = await this.current(); + return response; + } + + /** + * Retrieve the current element on the execution context. + * @memberof DefaultQueryExecutionContext + * @instance + */ + public async current(): Promise> { + if (this.currentIndex < this.resources.length) { + return { + result: this.resources[this.currentIndex], + headers: undefined + }; + } + + if (this._canFetchMore()) { + const { result: resources, headers } = await this.fetchMore(); + // if (err) { + // return callback(err, undefined, headers); + // } + // TODO: returning data and error is an anti-pattern + + this.resources = resources; + if (this.resources.length === 0) { + if (!this.continuation && this.currentPartitionIndex >= this.fetchFunctions.length) { + this.state = DefaultQueryExecutionContext.STATES.ended; + return { result: undefined, headers }; + } else { + return this.current(); + } + } + return { result: this.resources[this.currentIndex], headers }; + } else { + this.state = DefaultQueryExecutionContext.STATES.ended; + return { result: undefined, headers: undefined }; + } + } + + /** + * Determine if there are still remaining resources to processs based on + * the value of the continuation token or the elements remaining on the current batch in the execution context. + * @memberof DefaultQueryExecutionContext + * @instance + * @returns {Boolean} true if there is other elements to process in the DefaultQueryExecutionContext. + */ + public hasMoreResults() { + return ( + this.state === DefaultQueryExecutionContext.STATES.start || + this.continuation !== undefined || + this.currentIndex < this.resources.length - 1 || + this.currentPartitionIndex < this.fetchFunctions.length + ); + } + + /** + * Fetches the next batch of the feed and pass them as an array to a callback + * @memberof DefaultQueryExecutionContext + * @instance + */ + public async fetchMore(): Promise> { + if (this.currentPartitionIndex >= this.fetchFunctions.length) { + return { headers: undefined, result: undefined }; + } + + // Keep to the original continuation and to restore the value after fetchFunction call + const originalContinuation = this.options.continuation; + this.options.continuation = this.continuation; + + // Return undefined if there is no more results + if (this.currentPartitionIndex >= this.fetchFunctions.length) { + return { headers: undefined, result: undefined }; + } + + const fetchFunction = this.fetchFunctions[this.currentPartitionIndex]; + let resources; + let responseHeaders; + try { + const response = await fetchFunction(this.options); + resources = response.result; + responseHeaders = response.headers; + } catch (err) { + this.state = DefaultQueryExecutionContext.STATES.ended; + // return callback(err, undefined, responseHeaders); + // TODO: Error and data being returned is an antipattern, this might broken + throw err; + } + + this.continuation = responseHeaders[Constants.HttpHeaders.Continuation]; + if (!this.continuation) { + ++this.currentPartitionIndex; + } + + this.state = DefaultQueryExecutionContext.STATES.inProgress; + this.currentIndex = 0; + this.options.continuation = originalContinuation; + + // deserializing query metrics so that we aren't working with delimited strings in the rest of the code base + if (Constants.HttpHeaders.QueryMetrics in responseHeaders) { + const delimitedString = responseHeaders[Constants.HttpHeaders.QueryMetrics]; + let queryMetrics = QueryMetrics.createFromDelimitedString(delimitedString); + + // Add the request charge to the query metrics so that we can have per partition request charge. + if (Constants.HttpHeaders.RequestCharge in responseHeaders) { + queryMetrics = new QueryMetrics( + queryMetrics.retrievedDocumentCount, + queryMetrics.retrievedDocumentSize, + queryMetrics.outputDocumentCount, + queryMetrics.outputDocumentSize, + queryMetrics.indexHitDocumentCount, + queryMetrics.totalQueryExecutionTime, + queryMetrics.queryPreparationTimes, + queryMetrics.indexLookupTime, + queryMetrics.documentLoadTime, + queryMetrics.vmExecutionTime, + queryMetrics.runtimeExecutionTimes, + queryMetrics.documentWriteTime, + new ClientSideMetrics(responseHeaders[Constants.HttpHeaders.RequestCharge]) + ); + } + + // Wraping query metrics in a object where the key is '0' just so single partition + // and partition queries have the same response schema + responseHeaders[Constants.HttpHeaders.QueryMetrics] = {}; + responseHeaders[Constants.HttpHeaders.QueryMetrics]["0"] = queryMetrics; + } + + return { result: resources, headers: responseHeaders }; + } + + private _canFetchMore() { + const res = + this.state === DefaultQueryExecutionContext.STATES.start || + (this.continuation && this.state === DefaultQueryExecutionContext.STATES.inProgress) || + (this.currentPartitionIndex < this.fetchFunctions.length && + this.state === DefaultQueryExecutionContext.STATES.inProgress); + return res; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/documentProducer.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/documentProducer.ts new file mode 100644 index 000000000000..53a2f2e41d96 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/documentProducer.ts @@ -0,0 +1,304 @@ +import { FetchFunctionCallback, SqlQuerySpec } from "."; +import { ClientContext } from "../ClientContext"; +import { Constants, Helper, StatusCodes, SubStatusCodes } from "../common"; +import { FeedOptions } from "../request"; +import { Response } from "../request/request"; +import { DefaultQueryExecutionContext } from "./defaultQueryExecutionContext"; +import { FetchResult, FetchResultType } from "./FetchResult"; +import { HeaderUtils, IHeaders } from "./headerUtils"; + +/** @hidden */ +const HttpHeaders = Constants; + +/** @hidden */ +enum DocumentProducerStates { + started = "started", + inProgress = "inProgress", + ended = "ended" +} + +/** @hidden */ +export class DocumentProducer { + // // Static Members + // STATES: Object.freeze({ started: "started", inProgress: "inProgress", ended: "ended" }) + private static readonly STATES = DocumentProducerStates; + private collectionLink: string; + private query: string | SqlQuerySpec; + public targetPartitionKeyRange: any; // TODO: any partitionkeyrange + public fetchResults: FetchResult[]; + private state: DocumentProducerStates; + public allFetched: boolean; + private err: Error; + public previousContinuationToken: string; + public continuationToken: string; + private respHeaders: IHeaders; + private internalExecutionContext: DefaultQueryExecutionContext; + + /** + * Provides the Target Partition Range Query Execution Context. + * @constructor DocumentProducer + * @param {ClientContext} clientContext - The service endpoint to use to create the client. + * @param {String} collectionLink - Represents collection link + * @param {SqlQuerySpec | string} query - A SQL query. + * @param {object} targetPartitionKeyRange - Query Target Partition key Range + * @ignore + */ + constructor( + private clientContext: ClientContext, + collectionLink: string, + query: SqlQuerySpec, + targetPartitionKeyRange: any, // TODO: any partition key range + options: FeedOptions + ) { + // TODO: any options + this.collectionLink = collectionLink; + this.query = query; + this.targetPartitionKeyRange = targetPartitionKeyRange; + this.fetchResults = []; + + this.state = DocumentProducer.STATES.started; + this.allFetched = false; + this.err = undefined; + + this.previousContinuationToken = undefined; + this.continuationToken = undefined; + this.respHeaders = HeaderUtils.getInitialHeader(); + + // tslint:disable-next-line:no-shadowed-variable + this.internalExecutionContext = new DefaultQueryExecutionContext(clientContext, query, options, this.fetchFunction); + this.state = DocumentProducer.STATES.inProgress; + } + /** + * Synchronously gives the contiguous buffered results (stops at the first non result) if any + * @returns {Object} - buffered current items if any + * @ignore + */ + public peekBufferedItems() { + const bufferedResults = []; + for (let i = 0, done = false; i < this.fetchResults.length && !done; i++) { + const fetchResult = this.fetchResults[i]; + switch (fetchResult.fetchResultType) { + case FetchResultType.Done: + done = true; + break; + case FetchResultType.Exception: + done = true; + break; + case FetchResultType.Result: + bufferedResults.push(fetchResult.feedResponse); + break; + } + } + return bufferedResults; + } + + public fetchFunction: FetchFunctionCallback = async (options: any) => { + const path = Helper.getPathFromLink(this.collectionLink, "docs"); + const id = Helper.getIdFromLink(this.collectionLink); + + return this.clientContext.queryFeed( + path, + "docs", + id, + (result: any) => result.Documents, // TODO: any + this.query, + options, + this.targetPartitionKeyRange["id"] + ); + }; + + public hasMoreResults() { + return this.internalExecutionContext.hasMoreResults() || this.fetchResults.length !== 0; + } + + public gotSplit() { + const fetchResult = this.fetchResults[0]; + if (fetchResult.fetchResultType === FetchResultType.Exception) { + if (DocumentProducer._needPartitionKeyRangeCacheRefresh(fetchResult.error)) { + return true; + } + } + + return false; + } + + private _getAndResetActiveResponseHeaders() { + const ret = this.respHeaders; + this.respHeaders = HeaderUtils.getInitialHeader(); + return ret; + } + + private _updateStates(err: any, allFetched: boolean) { + // TODO: any Error + if (err) { + this.state = DocumentProducer.STATES.ended; + this.err = err; + return; + } + if (allFetched) { + this.allFetched = true; + } + if (this.allFetched && this.peekBufferedItems().length === 0) { + this.state = DocumentProducer.STATES.ended; + } + if (this.internalExecutionContext.continuation === this.continuationToken) { + // nothing changed + return; + } + this.previousContinuationToken = this.continuationToken; + this.continuationToken = this.internalExecutionContext.continuation; + } + + private static _needPartitionKeyRangeCacheRefresh(error: any) { + // TODO: error + return ( + error.code === StatusCodes.Gone && + "substatus" in error && + error["substatus"] === SubStatusCodes.PartitionKeyRangeGone + ); + } + + /** + * Fetches and bufferes the next page of results and executes the given callback + * @memberof DocumentProducer + * @instance + */ + public async bufferMore(): Promise> { + if (this.err) { + throw this.err; + } + + try { + const { result: resources, headers: headerResponse } = await this.internalExecutionContext.fetchMore(); + this._updateStates(undefined, resources === undefined); + if (resources !== undefined) { + // some more results + resources.forEach((element: any) => { + // TODO: resources any + this.fetchResults.push(new FetchResult(element, undefined)); + }); + } + + // need to modify the header response so that the query metrics are per partition + if (headerResponse != null && Constants.HttpHeaders.QueryMetrics in headerResponse) { + // "0" is the default partition before one is actually assigned. + const queryMetrics = headerResponse[Constants.HttpHeaders.QueryMetrics]["0"]; + + // Wraping query metrics in a object where the keys are the partition key range. + headerResponse[Constants.HttpHeaders.QueryMetrics] = {}; + headerResponse[Constants.HttpHeaders.QueryMetrics][this.targetPartitionKeyRange.id] = queryMetrics; + } + + return { result: resources, headers: headerResponse }; + } catch (err) { + // TODO: any error + if (DocumentProducer._needPartitionKeyRangeCacheRefresh(err)) { + // Split just happend + // Buffer the error so the execution context can still get the feedResponses in the itemBuffer + const bufferedError = new FetchResult(undefined, err); + this.fetchResults.push(bufferedError); + // Putting a dummy result so that the rest of code flows + return { result: [bufferedError], headers: err.headers }; + } else { + this._updateStates(err, err.resources === undefined); + throw err; + } + } + } + + /** + * Synchronously gives the bufferend current item if any + * @returns {Object} - buffered current item if any + * @ignore + */ + public getTargetParitionKeyRange() { + return this.targetPartitionKeyRange; + } + + /** + * Execute a provided function on the next element in the DocumentProducer. + * @memberof DocumentProducer + * @instance + * @param {callback} callback - Function to execute for each element. the function \ + * takes two parameters error, element. + */ + public async nextItem(): Promise> { + if (this.err) { + this._updateStates(this.err, undefined); + throw this.err; + } + + try { + const { result, headers } = await this.current(); + + const fetchResult = this.fetchResults.shift(); + this._updateStates(undefined, result === undefined); + if (fetchResult.feedResponse !== result) { + throw new Error(`Expected ${fetchResult.feedResponse} to equal ${result}`); + } + switch (fetchResult.fetchResultType) { + case FetchResultType.Done: + return { result: undefined, headers }; + case FetchResultType.Exception: + fetchResult.error.headers = headers; + throw fetchResult.error; + case FetchResultType.Result: + return { result: fetchResult.feedResponse, headers }; + } + } catch (err) { + this._updateStates(err, err.item === undefined); + throw err; + } + } + + /** + * Retrieve the current element on the DocumentProducer. + * @memberof DocumentProducer + * @instance + * @param {callback} callback - Function to execute for the current element. \ + * the function takes two parameters error, element. + */ + public async current(): Promise> { + // If something is buffered just give that + if (this.fetchResults.length > 0) { + const fetchResult = this.fetchResults[0]; + // Need to unwrap fetch results + switch (fetchResult.fetchResultType) { + case FetchResultType.Done: + return { + result: undefined, + headers: this._getAndResetActiveResponseHeaders() + }; + case FetchResultType.Exception: + fetchResult.error.headers = this._getAndResetActiveResponseHeaders(); + throw fetchResult.error; + case FetchResultType.Result: + return { + result: fetchResult.feedResponse, + headers: this._getAndResetActiveResponseHeaders() + }; + } + } + + // If there isn't anymore items left to fetch then let the user know. + if (this.allFetched) { + return { + result: undefined, + headers: this._getAndResetActiveResponseHeaders() + }; + } + + // If there are no more bufferd items and there are still items to be fetched then buffer more + try { + const { result, headers } = await this.bufferMore(); + if (result === undefined) { + return { result: undefined, headers }; + } + HeaderUtils.mergeHeaders(this.respHeaders, headers); + + return this.current(); + } catch (err) { + throw err; + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/headerUtils.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/headerUtils.ts new file mode 100644 index 000000000000..f1f7127d0d5b --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/headerUtils.ts @@ -0,0 +1,70 @@ +import { Constants } from "../common"; +import { QueryMetrics } from "../queryMetrics"; + +export interface IHeaders { + [key: string]: any; +} + +/** @hidden */ +// TODO: docs +export class HeaderUtils { + public static getRequestChargeIfAny(headers: IHeaders): number { + if (typeof headers === "number") { + return headers; + } else if (typeof headers === "string") { + return parseFloat(headers); + } + + if (headers) { + const rc = headers[Constants.HttpHeaders.RequestCharge]; + if (rc) { + return parseFloat(rc as string); + } else { + return 0; + } + } else { + return 0; + } + } + + public static getInitialHeader(): IHeaders { + const headers: IHeaders = {}; + headers[Constants.HttpHeaders.RequestCharge] = 0; + headers[Constants.HttpHeaders.QueryMetrics] = {}; + return headers; + } + + // TODO: The name of this method isn't very accurate to what it does + public static mergeHeaders(headers: IHeaders, toBeMergedHeaders: IHeaders) { + if (headers[Constants.HttpHeaders.RequestCharge] === undefined) { + headers[Constants.HttpHeaders.RequestCharge] = 0; + } + + if (headers[Constants.HttpHeaders.QueryMetrics] === undefined) { + headers[Constants.HttpHeaders.QueryMetrics] = QueryMetrics.zero; + } + + if (!toBeMergedHeaders) { + return; + } + + (headers[Constants.HttpHeaders.RequestCharge] as number) += HeaderUtils.getRequestChargeIfAny(toBeMergedHeaders); + if (toBeMergedHeaders[Constants.HttpHeaders.IsRUPerMinuteUsed]) { + headers[Constants.HttpHeaders.IsRUPerMinuteUsed] = toBeMergedHeaders[Constants.HttpHeaders.IsRUPerMinuteUsed]; + } + + if (Constants.HttpHeaders.QueryMetrics in toBeMergedHeaders) { + const headerQueryMetrics = headers[Constants.HttpHeaders.QueryMetrics]; + const toBeMergedHeaderQueryMetrics = toBeMergedHeaders[Constants.HttpHeaders.QueryMetrics]; + + for (const partitionId in toBeMergedHeaderQueryMetrics) { + if (partitionId in headerQueryMetrics) { + const combinedQueryMetrics = headerQueryMetrics[partitionId].add(toBeMergedHeaderQueryMetrics[partitionId]); + headerQueryMetrics[partitionId] = combinedQueryMetrics; + } else { + headerQueryMetrics[partitionId] = toBeMergedHeaderQueryMetrics[partitionId]; + } + } + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/index.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/index.ts new file mode 100644 index 000000000000..600c83fec4dd --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/index.ts @@ -0,0 +1,15 @@ +export * from "./headerUtils"; +export * from "./SqlQuerySpec"; +export * from "./defaultQueryExecutionContext"; +export * from "./Aggregators"; +export * from "./EndpointComponent"; +export * from "./documentProducer"; +export * from "./FetchResult"; +export * from "./orderByDocumentProducerComparator"; +export * from "./IExecutionContext"; +export * from "./partitionedQueryExecutionContextInfoParser"; +export * from "./parallelQueryExecutionContextBase"; +export * from "./parallelQueryExecutionContext"; +export * from "./orderByQueryExecutionContext"; +export * from "./pipelinedQueryExecutionContext"; +export * from "./proxyQueryExecutionContext"; diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/orderByDocumentProducerComparator.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/orderByDocumentProducerComparator.ts new file mode 100644 index 000000000000..cea50281d950 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/orderByDocumentProducerComparator.ts @@ -0,0 +1,139 @@ +import { DocumentProducer } from "./documentProducer"; + +// TODO: this smells funny +/** @hidden */ +const TYPEORDCOMPARATOR: { + [type: string]: { ord: number; compFunc?: (a: any, b: any) => number }; +} = Object.freeze({ + NoValue: { + ord: 0 + }, + undefined: { + ord: 1 + }, + boolean: { + ord: 2, + compFunc: (a: boolean, b: boolean) => { + return a === b ? 0 : a > b ? 1 : -1; + } + }, + number: { + ord: 4, + compFunc: (a: number, b: number) => { + return a === b ? 0 : a > b ? 1 : -1; + } + }, + string: { + ord: 5, + compFunc: (a: string, b: string) => { + return a === b ? 0 : a > b ? 1 : -1; + } + } +}); + +/** @hidden */ +export class OrderByDocumentProducerComparator { + constructor(public sortOrder: string[]) {} // TODO: This should be an enum + + public targetPartitionKeyRangeDocProdComparator(docProd1: DocumentProducer, docProd2: DocumentProducer) { + const a = docProd1.getTargetParitionKeyRange()["minInclusive"]; + const b = docProd2.getTargetParitionKeyRange()["minInclusive"]; + return a === b ? 0 : a > b ? 1 : -1; + } + + public compare(docProd1: DocumentProducer, docProd2: DocumentProducer) { + // Need to check for split, since we don't want to dereference "item" of undefined / exception + if (docProd1.gotSplit()) { + return -1; + } + if (docProd2.gotSplit()) { + return 1; + } + + const orderByItemsRes1 = this.getOrderByItems(docProd1.peekBufferedItems()[0]); + const orderByItemsRes2 = this.getOrderByItems(docProd2.peekBufferedItems()[0]); + + // validate order by items and types + // TODO: once V1 order by on different types is fixed this need to change + this.validateOrderByItems(orderByItemsRes1, orderByItemsRes2); + + // no async call in the for loop + for (let i = 0; i < orderByItemsRes1.length; i++) { + // compares the orderby items one by one + const compRes = this.compareOrderByItem(orderByItemsRes1[i], orderByItemsRes2[i]); + if (compRes !== 0) { + if (this.sortOrder[i] === "Ascending") { + return compRes; + } else if (this.sortOrder[i] === "Descending") { + return -compRes; + } + } + } + + return this.targetPartitionKeyRangeDocProdComparator(docProd1, docProd2); + } + + // TODO: This smells funny + public compareValue(item1: any, type1: string, item2: any, type2: string) { + const type1Ord = TYPEORDCOMPARATOR[type1].ord; + const type2Ord = TYPEORDCOMPARATOR[type2].ord; + const typeCmp = type1Ord - type2Ord; + + if (typeCmp !== 0) { + // if the types are different, use type ordinal + return typeCmp; + } + + // both are of the same type + if (type1Ord === TYPEORDCOMPARATOR["undefined"].ord || type1Ord === TYPEORDCOMPARATOR["NoValue"].ord) { + // if both types are undefined or Null they are equal + return 0; + } + + const compFunc = TYPEORDCOMPARATOR[type1].compFunc; + if (typeof compFunc === "undefined") { + throw new Error("Cannot find the comparison function"); + } + // same type and type is defined compare the items + return compFunc(item1, item2); + } + + public compareOrderByItem(orderByItem1: any, orderByItem2: any) { + const type1 = this.getType(orderByItem1); + const type2 = this.getType(orderByItem2); + return this.compareValue(orderByItem1["item"], type1, orderByItem2["item"], type2); + } + + public validateOrderByItems(res1: string[], res2: string[]) { + this._throwIf(res1.length !== res2.length, `Expected ${res1.length}, but got ${res2.length}.`); + this._throwIf(res1.length !== this.sortOrder.length, "orderByItems cannot have a different size than sort orders."); + + for (let i = 0; i < this.sortOrder.length; i++) { + const type1 = this.getType(res1[i]); + const type2 = this.getType(res2[i]); + this._throwIf(type1 !== type2, `Expected ${type1}, but got ${type2}.`); + } + } + + public getType(orderByItem: any) { + // TODO: any item? + if (orderByItem === undefined || orderByItem.item === undefined) { + return "NoValue"; + } + const type = typeof orderByItem.item; + this._throwIf(TYPEORDCOMPARATOR[type] === undefined, `unrecognizable type ${type}`); + return type; + } + + public getOrderByItems(res: any) { + // TODO: any res? + return res["orderByItems"]; + } + + // TODO: this should be done differently... + public _throwIf(condition: boolean, msg: string) { + if (condition) { + throw Error(msg); + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/orderByQueryExecutionContext.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/orderByQueryExecutionContext.ts new file mode 100644 index 000000000000..d23964638bfe --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/orderByQueryExecutionContext.ts @@ -0,0 +1,49 @@ +import { + DocumentProducer, + IExecutionContext, + OrderByDocumentProducerComparator, + ParallelQueryExecutionContextBase, + PartitionedQueryExecutionContextInfo +} from "."; +import { ClientContext } from "../ClientContext"; + +/** @hidden */ +export class OrderByQueryExecutionContext extends ParallelQueryExecutionContextBase implements IExecutionContext { + private orderByComparator: any; + /** + * Provides the OrderByQueryExecutionContext. + * This class is capable of handling orderby queries and dervives from ParallelQueryExecutionContextBase. + * + * When handling a parallelized query, it instantiates one instance of + * DocumentProcuder per target partition key range and aggregates the result of each. + * + * @constructor ParallelQueryExecutionContext + * @param {ClientContext} clientContext - The service endpoint to use to create the client. + * @param {string} collectionLink - The Collection Link + * @param {FeedOptions} [options] - Represents the feed options. + * @param {object} partitionedQueryExecutionInfo - PartitionedQueryExecutionInfo + * @ignore + */ + constructor( + clientContext: ClientContext, + collectionLink: string, + query: any, // TODO: any query + options: any, // TODO: any options + partitionedQueryExecutionInfo: PartitionedQueryExecutionContextInfo + ) { + // Calling on base class constructor + super(clientContext, collectionLink, query, options, partitionedQueryExecutionInfo); + this.orderByComparator = new OrderByDocumentProducerComparator(this.sortOrders); + } + // Instance members are inherited + + // Overriding documentProducerComparator for OrderByQueryExecutionContexts + /** + * Provides a Comparator for document producers which respects orderby sort order. + * @returns {object} - Comparator Function + * @ignore + */ + public documentProducerComparator(docProd1: DocumentProducer, docProd2: DocumentProducer) { + return this.orderByComparator.compare(docProd1, docProd2); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/parallelQueryExecutionContext.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/parallelQueryExecutionContext.ts new file mode 100644 index 000000000000..97b55fea87d8 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/parallelQueryExecutionContext.ts @@ -0,0 +1,80 @@ +import { + DocumentProducer, + IExecutionContext, + ParallelQueryExecutionContextBase, + PartitionedQueryExecutionContextInfo +} from "."; +import { ClientContext } from "../ClientContext"; +import { PARITIONKEYRANGE } from "../routing"; + +/** @hidden */ +export class ParallelQueryExecutionContext extends ParallelQueryExecutionContextBase implements IExecutionContext { + /** + * Provides the ParallelQueryExecutionContext. + * This class is capable of handling parallelized queries and dervives from ParallelQueryExecutionContextBase. + * + * @constructor ParallelQueryExecutionContext + * @param {ClientContext} clientContext - The service endpoint to use to create the client. + * @param {string} collectionLink - The Collection Link + * @param {FeedOptions} [options] - Represents the feed options. + * @param {object} partitionedQueryExecutionInfo - PartitionedQueryExecutionInfo + * @ignore + */ + constructor( + clientContext: ClientContext, + collectionLink: string, + query: any, + options: any, + partitionedQueryExecutionInfo: PartitionedQueryExecutionContextInfo + ) { + // Calling on base class constructor + super(clientContext, collectionLink, query, options, partitionedQueryExecutionInfo); + } + // Instance members are inherited + + // Overriding documentProducerComparator for ParallelQueryExecutionContexts + /** + * Provides a Comparator for document producers using the min value of the corresponding target partition. + * @returns {object} - Comparator Function + * @ignore + */ + public documentProducerComparator(docProd1: DocumentProducer, docProd2: DocumentProducer) { + const a = docProd1.getTargetParitionKeyRange()["minInclusive"]; + const b = docProd2.getTargetParitionKeyRange()["minInclusive"]; + return a === b ? 0 : a > b ? 1 : -1; + } + + private _buildContinuationTokenFrom(documentProducer: DocumentProducer) { + // given the document producer constructs the continuation token + if (documentProducer.allFetched && documentProducer.peekBufferedItems().length === 0) { + return undefined; + } + + const min = documentProducer.targetPartitionKeyRange[PARITIONKEYRANGE.MinInclusive]; + const max = documentProducer.targetPartitionKeyRange[PARITIONKEYRANGE.MaxExclusive]; + const range = { + min, + max, + id: documentProducer.targetPartitionKeyRange.id + }; + + // TODO: static method + const withNullDefault = (token: any) => { + if (token) { + return token; + } else if (token === null || token === undefined) { + return null; + } + }; + + const documentProducerContinuationToken = + documentProducer.peekBufferedItems().length > 0 + ? documentProducer.previousContinuationToken + : documentProducer.continuationToken; + + return { + token: withNullDefault(documentProducerContinuationToken), + range + }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/parallelQueryExecutionContextBase.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/parallelQueryExecutionContextBase.ts new file mode 100644 index 000000000000..b1a92a01af77 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/parallelQueryExecutionContextBase.ts @@ -0,0 +1,571 @@ +import * as bs from "binary-search-bounds"; +import PriorityQueue from "priorityqueuejs"; +import semaphore from "semaphore"; +import { + DocumentProducer, + HeaderUtils, + IExecutionContext, + IHeaders, + PartitionedQueryExecutionContextInfo, + PartitionedQueryExecutionContextInfoParser +} from "."; +import { ClientContext } from "../ClientContext"; +import { StatusCodes, SubStatusCodes } from "../common"; +import { Response } from "../request/request"; +import { PARITIONKEYRANGE, QueryRange, SmartRoutingMapProvider } from "../routing"; + +/** @hidden */ +export enum ParallelQueryExecutionContextBaseStates { + started = "started", + inProgress = "inProgress", + ended = "ended" +} + +/** @hidden */ +export abstract class ParallelQueryExecutionContextBase implements IExecutionContext { + private static readonly DEFAULT_PAGE_SIZE = 10; + + private err: any; + private state: any; + private static STATES = ParallelQueryExecutionContextBaseStates; + private routingProvider: SmartRoutingMapProvider; + protected sortOrders: any; + private pageSize: any; + private requestContinuation: any; + private respHeaders: IHeaders; + private orderByPQ: PriorityQueue; + private sem: any; + private waitingForInternalExecutionContexts: number; + /** + * Provides the ParallelQueryExecutionContextBase. + * This is the base class that ParallelQueryExecutionContext and OrderByQueryExecutionContext will derive from. + * + * When handling a parallelized query, it instantiates one instance of + * DocumentProcuder per target partition key range and aggregates the result of each. + * + * @constructor ParallelQueryExecutionContext + * @param {ClientContext} clientContext - The service endpoint to use to create the client. + * @param {string} collectionLink - The Collection Link + * @param {FeedOptions} [options] - Represents the feed options. + * @param {object} partitionedQueryExecutionInfo - PartitionedQueryExecutionInfo + * @ignore + */ + constructor( + private clientContext: ClientContext, + private collectionLink: string, + private query: any, // TODO: any - It's not SQLQuerySpec + private options: any, + private partitionedQueryExecutionInfo: PartitionedQueryExecutionContextInfo + ) { + this.clientContext = clientContext; + this.collectionLink = collectionLink; + this.query = query; + this.options = options; + this.partitionedQueryExecutionInfo = partitionedQueryExecutionInfo; + + this.err = undefined; + this.state = ParallelQueryExecutionContextBase.STATES.started; + this.routingProvider = new SmartRoutingMapProvider(this.clientContext); + this.sortOrders = PartitionedQueryExecutionContextInfoParser.parseOrderBy(this.partitionedQueryExecutionInfo); + + if (options === undefined || options["maxItemCount"] === undefined) { + this.pageSize = ParallelQueryExecutionContextBase.DEFAULT_PAGE_SIZE; + this.options["maxItemCount"] = this.pageSize; + } else { + this.pageSize = options["maxItemCount"]; + } + + this.requestContinuation = options ? options.continuation : null; + // response headers of undergoing operation + this.respHeaders = HeaderUtils.getInitialHeader(); + + // Make priority queue for documentProducers + // The comparator is supplied by the derived class + this.orderByPQ = new PriorityQueue((a: DocumentProducer, b: DocumentProducer) => + this.documentProducerComparator(b, a) + ); + // Creating the documentProducers + this.sem = semaphore(1); + // Creating callback for semaphore + // TODO: Code smell + const createDocumentProducersAndFillUpPriorityQueueFunc = async () => { + // ensure the lock is released after finishing up + try { + const targetPartitionRanges = await this._onTargetPartitionRanges(); + this.waitingForInternalExecutionContexts = targetPartitionRanges.length; + // default to 1 if none is provided. + const maxDegreeOfParallelism = + options.maxDegreeOfParallelism > 0 + ? Math.min(options.maxDegreeOfParallelism, targetPartitionRanges.length) + : targetPartitionRanges.length; + + const parallelismSem = semaphore(maxDegreeOfParallelism); + let filteredPartitionKeyRanges = []; + // The document producers generated from filteredPartitionKeyRanges + const targetPartitionQueryExecutionContextList: DocumentProducer[] = []; + + if (this.requestContinuation) { + // Need to create the first documentProducer with the suppliedCompositeContinuationToken + try { + const suppliedCompositeContinuationToken = JSON.parse(this.requestContinuation); + filteredPartitionKeyRanges = this.getPartitionKeyRangesForContinuation( + suppliedCompositeContinuationToken, + targetPartitionRanges + ); + if (filteredPartitionKeyRanges.length > 0) { + targetPartitionQueryExecutionContextList.push( + this._createTargetPartitionQueryExecutionContext( + filteredPartitionKeyRanges[0], + suppliedCompositeContinuationToken.token + ) + ); + // Slicing the first element off, since we already made a documentProducer for it + filteredPartitionKeyRanges = filteredPartitionKeyRanges.slice(1); + } + } catch (e) { + this.err = e; + this.sem.leave(); + } + } else { + filteredPartitionKeyRanges = targetPartitionRanges; + } + + // Create one documentProducer for each partitionTargetRange + filteredPartitionKeyRanges.forEach((partitionTargetRange: any) => { + // TODO: any partitionTargetRange + // no async callback + targetPartitionQueryExecutionContextList.push( + this._createTargetPartitionQueryExecutionContext(partitionTargetRange) + ); + }); + + // Fill up our priority queue with documentProducers + targetPartitionQueryExecutionContextList.forEach(documentProducer => { + // has async callback + const throttledFunc = async () => { + try { + const { result: document, headers } = await documentProducer.current(); + this._mergeWithActiveResponseHeaders(headers); + if (document === undefined) { + // no results on this one + return; + } + // if there are matching results in the target ex range add it to the priority queue + try { + this.orderByPQ.enq(documentProducer); + } catch (e) { + this.err = e; + } + } catch (err) { + this._mergeWithActiveResponseHeaders(err.headers); + this.err = err; + } finally { + parallelismSem.leave(); + this._decrementInitiationLock(); + } + }; + parallelismSem.take(throttledFunc); + }); + } catch (err) { + this.err = err; + // release the lock + this.sem.leave(); + return; + } + }; + this.sem.take(createDocumentProducersAndFillUpPriorityQueueFunc); + } + + protected abstract documentProducerComparator(dp1: DocumentProducer, dp2: DocumentProducer): number; + // TODO: any TODO: any + public getPartitionKeyRangesForContinuation(suppliedCompositeContinuationToken: any, partitionKeyRanges: any) { + const startRange: any = {}; // TODO: any + startRange[PARITIONKEYRANGE.MinInclusive] = suppliedCompositeContinuationToken.range.min; + startRange[PARITIONKEYRANGE.MaxExclusive] = suppliedCompositeContinuationToken.range.max; + + const vbCompareFunction = (x: any, y: any) => { + // TODO: any + if (x[PARITIONKEYRANGE.MinInclusive] > y[PARITIONKEYRANGE.MinInclusive]) { + return 1; + } + if (x[PARITIONKEYRANGE.MinInclusive] < y[PARITIONKEYRANGE.MinInclusive]) { + return -1; + } + + return 0; + }; + + const minIndex = bs.le(partitionKeyRanges, startRange, vbCompareFunction); + // that's an error + + if (minIndex > 0) { + throw new Error("BadRequestException: InvalidContinuationToken"); + } + + // return slice of the partition key ranges + return partitionKeyRanges.slice(minIndex, partitionKeyRanges.length - minIndex); + } + + private _decrementInitiationLock() { + // decrements waitingForInternalExecutionContexts + // if waitingForInternalExecutionContexts reaches 0 releases the semaphore and changes the state + this.waitingForInternalExecutionContexts = this.waitingForInternalExecutionContexts - 1; + if (this.waitingForInternalExecutionContexts === 0) { + this.sem.leave(); + if (this.orderByPQ.size() === 0) { + this.state = ParallelQueryExecutionContextBase.STATES.inProgress; + } + } + } + + private _mergeWithActiveResponseHeaders(headers: IHeaders) { + HeaderUtils.mergeHeaders(this.respHeaders, headers); + } + + private _getAndResetActiveResponseHeaders() { + const ret = this.respHeaders; + this.respHeaders = HeaderUtils.getInitialHeader(); + return ret; + } + + private async _onTargetPartitionRanges() { + // invokes the callback when the target partition ranges are ready + const parsedRanges = PartitionedQueryExecutionContextInfoParser.parseQueryRanges( + this.partitionedQueryExecutionInfo + ); + const queryRanges = parsedRanges.map((item: any) => QueryRange.parseFromDict(item)); // TODO: any + return this.routingProvider.getOverlappingRanges(this.collectionLink, queryRanges); + } + + /** + * Gets the replacement ranges for a partitionkeyrange that has been split + * @memberof ParallelQueryExecutionContextBase + * @instance + */ + private async _getReplacementPartitionKeyRanges(documentProducer: DocumentProducer) { + const routingMapProvider = this.clientContext.partitionKeyDefinitionCache; + const partitionKeyRange = documentProducer.targetPartitionKeyRange; + // Download the new routing map + this.routingProvider = new SmartRoutingMapProvider(this.clientContext); + // Get the queryRange that relates to this partitionKeyRange + const queryRange = QueryRange.parsePartitionKeyRange(partitionKeyRange); + return this.routingProvider.getOverlappingRanges(this.collectionLink, [queryRange]); + } + + // TODO: P0 Code smell - can barely tell what this is doing + /** + * Removes the current document producer from the priqueue, + * replaces that document producer with child document producers, + * then reexecutes the originFunction with the corrrected executionContext + * @memberof ParallelQueryExecutionContextBase + * @instance + */ + private async _repairExecutionContext(originFunction: any) { + // TODO: any + // Get the replacement ranges + // Removing the invalid documentProducer from the orderByPQ + const parentDocumentProducer = this.orderByPQ.deq(); + try { + const replacementPartitionKeyRanges: any[] = await this._getReplacementPartitionKeyRanges(parentDocumentProducer); + const replacementDocumentProducers: DocumentProducer[] = []; + // Create the replacement documentProducers + replacementPartitionKeyRanges.forEach(partitionKeyRange => { + // Create replacment document producers with the parent's continuationToken + const replacementDocumentProducer = this._createTargetPartitionQueryExecutionContext( + partitionKeyRange, + parentDocumentProducer.continuationToken + ); + replacementDocumentProducers.push(replacementDocumentProducer); + }); + // We need to check if the documentProducers even has anything left to fetch from before enqueing them + const checkAndEnqueueDocumentProducer = async ( + documentProducerToCheck: DocumentProducer, + checkNextDocumentProducerCallback: any + ) => { + try { + const { result: afterItem, headers } = await documentProducerToCheck.current(); + if (afterItem === undefined) { + // no more results left in this document producer, so we don't enqueue it + } else { + // Safe to put document producer back in the queue + this.orderByPQ.enq(documentProducerToCheck); + } + + await checkNextDocumentProducerCallback(); + } catch (err) { + this.err = err; + return; + } + }; + const checkAndEnqueueDocumentProducers = async (rdp: DocumentProducer[]) => { + if (rdp.length > 0) { + // We still have a replacementDocumentProducer to check + const replacementDocumentProducer = rdp.shift(); + await checkAndEnqueueDocumentProducer(replacementDocumentProducer, async () => { + await checkAndEnqueueDocumentProducers(rdp); + }); + } else { + // reexecutes the originFunction with the corrrected executionContext + return originFunction(); + } + }; + // Invoke the recursive function to get the ball rolling + await checkAndEnqueueDocumentProducers(replacementDocumentProducers); + } catch (err) { + this.err = err; + throw err; + } + } + + private static _needPartitionKeyRangeCacheRefresh(error: any) { + // TODO: any error + return ( + error.code === StatusCodes.Gone && + "substatus" in error && + error["substatus"] === SubStatusCodes.PartitionKeyRangeGone + ); + } + + /** + * Checks to see if the executionContext needs to be repaired. + * if so it repairs the execution context and executes the ifCallback, + * else it continues with the current execution context and executes the elseCallback + * @memberof ParallelQueryExecutionContextBase + * @instance + */ + private async _repairExecutionContextIfNeeded(ifCallback: any, elseCallback: any) { + const documentProducer = this.orderByPQ.peek(); + // Check if split happened + try { + const { result: element, headers } = await documentProducer.current(); + elseCallback(); + } catch (err) { + if (ParallelQueryExecutionContextBase._needPartitionKeyRangeCacheRefresh(err)) { + // Split has happened so we need to repair execution context before continueing + return this._repairExecutionContext(ifCallback); + } else { + // Something actually bad happened ... + this.err = err; + throw err; + } + } + } + + /** + * Execute a provided function on the next element in the ParallelQueryExecutionContextBase. + * @memberof ParallelQueryExecutionContextBase + * @instance + * @param {callback} callback - Function to execute for each element. the function takes two \ + * parameters error, element. + */ + public async nextItem(): Promise> { + if (this.err) { + // if there is a prior error return error + throw this.err; + } + return new Promise>((resolve, reject) => { + this.sem.take(() => { + // NOTE: lock must be released before invoking quitting + if (this.err) { + // release the lock before invoking callback + this.sem.leave(); + // if there is a prior error return error + this.err.headers = this._getAndResetActiveResponseHeaders(); + reject(this.err); + return; + } + + if (this.orderByPQ.size() === 0) { + // there is no more results + this.state = ParallelQueryExecutionContextBase.STATES.ended; + // release the lock before invoking callback + this.sem.leave(); + return resolve({ + result: undefined, + headers: this._getAndResetActiveResponseHeaders() + }); + } + + const ifCallback = () => { + // Release the semaphore to avoid deadlock + this.sem.leave(); + // Reexcute the function + return resolve(this.nextItem()); + }; + const elseCallback = async () => { + let documentProducer: DocumentProducer; + try { + documentProducer = this.orderByPQ.deq(); + } catch (e) { + // if comparing elements of the priority queue throws exception + // set that error and return error + this.err = e; + // release the lock before invoking callback + this.sem.leave(); + this.err.headers = this._getAndResetActiveResponseHeaders(); + reject(this.err); + return; + } + + let item: any; + let headers: IHeaders; + try { + const response = await documentProducer.nextItem(); + item = response.result; + headers = response.headers; + this._mergeWithActiveResponseHeaders(headers); + if (item === undefined) { + // this should never happen + // because the documentProducer already has buffered an item + // assert item !== undefined + this.err = new Error( + `Extracted DocumentProducer from the priority queue \ + doesn't have any buffered item!` + ); + // release the lock before invoking callback + this.sem.leave(); + return resolve({ + result: undefined, + headers: this._getAndResetActiveResponseHeaders() + }); + } + } catch (err) { + this.err = new Error( + `Extracted DocumentProducer from the priority queue fails to get the \ + buffered item. Due to ${JSON.stringify(err)}` + ); + this.err.headers = this._getAndResetActiveResponseHeaders(); + // release the lock before invoking callback + this.sem.leave(); + reject(this.err); + return; + } + + // we need to put back the document producer to the queue if it has more elements. + // the lock will be released after we know document producer must be put back in the queue or not + try { + const { result: afterItem, headers: currentHeaders } = await documentProducer.current(); + if (afterItem === undefined) { + // no more results is left in this document producer + } else { + try { + const headItem = documentProducer.fetchResults[0]; + if (typeof headItem === "undefined") { + throw new Error("Extracted DocumentProducer from PQ is invalid state with no result!"); + } + this.orderByPQ.enq(documentProducer); + } catch (e) { + // if comparing elements in priority queue throws exception + // set error + this.err = e; + } + } + } catch (err) { + if (ParallelQueryExecutionContextBase._needPartitionKeyRangeCacheRefresh(err)) { + // We want the document producer enqueued + // So that later parts of the code can repair the execution context + this.orderByPQ.enq(documentProducer); + } else { + // Something actually bad happened + this.err = err; + reject(this.err); + } + } finally { + // release the lock before returning + this.sem.leave(); + } + // invoke the callback on the item + return resolve({ + result: item, + headers: this._getAndResetActiveResponseHeaders() + }); + }; + this._repairExecutionContextIfNeeded(ifCallback, elseCallback).catch(reject); + }); + }); + } + + /** + * Retrieve the current element on the ParallelQueryExecutionContextBase. + * @memberof ParallelQueryExecutionContextBase + * @instance + * @param {callback} callback - Function to execute for the current element. \ + * the function takes two parameters error, element. + */ + public async current(): Promise> { + if (this.err) { + this.err.headerse = this._getAndResetActiveResponseHeaders(); + throw this.err; + } + return new Promise>((resolve, reject) => { + this.sem.take(() => { + try { + if (this.err) { + this.err = this._getAndResetActiveResponseHeaders(); + throw this.err; + } + + if (this.orderByPQ.size() === 0) { + return resolve({ + result: undefined, + headers: this._getAndResetActiveResponseHeaders() + }); + } + + const ifCallback = () => { + // Reexcute the function + return resolve(this.current()); + }; + + const elseCallback = () => { + const documentProducer = this.orderByPQ.peek(); + return resolve(documentProducer.current()); + }; + + this._repairExecutionContextIfNeeded(ifCallback, elseCallback).catch(reject); + } finally { + this.sem.leave(); + } + }); + }); + } + + /** + * Determine if there are still remaining resources to processs based on the value of the continuation \ + * token or the elements remaining on the current batch in the QueryIterator. + * @memberof ParallelQueryExecutionContextBase + * @instance + * @returns {Boolean} true if there is other elements to process in the ParallelQueryExecutionContextBase. + */ + public hasMoreResults() { + return !(this.state === ParallelQueryExecutionContextBase.STATES.ended || this.err !== undefined); + } + + /** + * Creates document producers + */ + private _createTargetPartitionQueryExecutionContext(partitionKeyTargetRange: any, continuationToken?: any) { + // TODO: any + // creates target partition range Query Execution Context + let rewrittenQuery = PartitionedQueryExecutionContextInfoParser.parseRewrittenQuery( + this.partitionedQueryExecutionInfo + ); + let query = this.query; + if (typeof query === "string") { + query = { query }; + } + + const formatPlaceHolder = "{documentdb-formattableorderbyquery-filter}"; + if (rewrittenQuery) { + query = JSON.parse(JSON.stringify(query)); + // We hardcode the formattable filter to true for now + rewrittenQuery = rewrittenQuery.replace(formatPlaceHolder, "true"); + query["query"] = rewrittenQuery; + } + + const options = JSON.parse(JSON.stringify(this.options)); + options.continuationToken = continuationToken; + + return new DocumentProducer(this.clientContext, this.collectionLink, query, partitionKeyTargetRange, options); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/partitionedQueryExecutionContextInfoParser.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/partitionedQueryExecutionContextInfoParser.ts new file mode 100644 index 000000000000..42ca0700b4df --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/partitionedQueryExecutionContextInfoParser.ts @@ -0,0 +1,50 @@ +/** @hidden */ +const PartitionedQueryContants = { + QueryInfoPath: "queryInfo", + TopPath: ["queryInfo", "top"], + OrderByPath: ["queryInfo", "orderBy"], + AggregatePath: ["queryInfo", "aggregates"], + QueryRangesPath: "queryRanges", + RewrittenQueryPath: ["queryInfo", "rewrittenQuery"] +}; + +/** @hidden */ +export interface PartitionedQueryExecutionContextInfo { + [key: string]: any; +} + +// TODO: any partitionedQueryExecutionInfo +/** @hidden */ +export class PartitionedQueryExecutionContextInfoParser { + public static parseRewrittenQuery(partitionedQueryExecutionInfo: { [key: string]: any }) { + return this._extract(partitionedQueryExecutionInfo, PartitionedQueryContants.RewrittenQueryPath); + } + public static parseQueryRanges(partitionedQueryExecutionInfo: { [key: string]: any }) { + return this._extract(partitionedQueryExecutionInfo, PartitionedQueryContants.QueryRangesPath); + } + public static parseOrderBy(partitionedQueryExecutionInfo: { [key: string]: any }) { + return this._extract(partitionedQueryExecutionInfo, PartitionedQueryContants.OrderByPath); + } + public static parseAggregates(partitionedQueryExecutionInfo: { [key: string]: any }) { + return this._extract(partitionedQueryExecutionInfo, PartitionedQueryContants.AggregatePath); + } + public static parseTop(partitionedQueryExecutionInfo: { [key: string]: any }) { + return this._extract(partitionedQueryExecutionInfo, PartitionedQueryContants.TopPath); + } + private static _extract(partitionedQueryExecutionInfo: { [key: string]: any }, path: string | string[]) { + let item = partitionedQueryExecutionInfo; + if (typeof path === "string") { + return item[path]; + } + if (!Array.isArray(path)) { + throw new Error(`JSON.stringify(path is expected to be an array`); + } + for (const p of path) { + item = item[p]; + if (item === undefined) { + return; + } + } + return item; + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/pipelinedQueryExecutionContext.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/pipelinedQueryExecutionContext.ts new file mode 100644 index 000000000000..39033d67c8d0 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/pipelinedQueryExecutionContext.ts @@ -0,0 +1,140 @@ +import { + HeaderUtils, + IExecutionContext, + IHeaders, + OrderByQueryExecutionContext, + ParallelQueryExecutionContext, + PartitionedQueryExecutionContextInfo, + PartitionedQueryExecutionContextInfoParser +} from "."; +import { ClientContext } from "../ClientContext"; +import { Response } from "../request/request"; +import { + AggregateEndpointComponent, + IEndpointComponent, + OrderByEndpointComponent, + TopEndpointComponent +} from "./EndpointComponent"; + +/** @hidden */ +export class PipelinedQueryExecutionContext implements IExecutionContext { + private fetchBuffer: any[]; + private fetchMoreRespHeaders: IHeaders; + private endpoint: IEndpointComponent; + private pageSize: number; + private static DEFAULT_PAGE_SIZE = 10; + constructor( + private clientContext: ClientContext, + private collectionLink: string, + private query: any, // TODO: any query + private options: any, // TODO: any options + private partitionedQueryExecutionInfo: PartitionedQueryExecutionContextInfo + ) { + this.endpoint = null; + this.pageSize = options["maxItemCount"]; + if (this.pageSize === undefined) { + this.pageSize = PipelinedQueryExecutionContext.DEFAULT_PAGE_SIZE; + } + + // Pick between parallel vs order by execution context + const sortOrders = PartitionedQueryExecutionContextInfoParser.parseOrderBy(partitionedQueryExecutionInfo); + if (Array.isArray(sortOrders) && sortOrders.length > 0) { + // Need to wrap orderby execution context in endpoint component, since the data is nested as a \ + // "payload" property. + this.endpoint = new OrderByEndpointComponent( + new OrderByQueryExecutionContext( + this.clientContext, + this.collectionLink, + this.query, + this.options, + this.partitionedQueryExecutionInfo + ) + ); + } else { + this.endpoint = new ParallelQueryExecutionContext( + this.clientContext, + this.collectionLink, + this.query, + this.options, + this.partitionedQueryExecutionInfo + ); + } + + // If aggregate then add that to the pipeline + const aggregates = PartitionedQueryExecutionContextInfoParser.parseAggregates(partitionedQueryExecutionInfo); + if (Array.isArray(aggregates) && aggregates.length > 0) { + this.endpoint = new AggregateEndpointComponent(this.endpoint, aggregates); + } + + // If top then add that to the pipeline + const top = PartitionedQueryExecutionContextInfoParser.parseTop(partitionedQueryExecutionInfo); + if (typeof top === "number") { + this.endpoint = new TopEndpointComponent(this.endpoint, top); + } + } + + public async nextItem(): Promise> { + return this.endpoint.nextItem(); + } + + public async current(): Promise> { + return this.endpoint.current(); + } + + // Removed callback here beacuse it wouldn't have ever worked... + public hasMoreResults(): boolean { + return this.endpoint.hasMoreResults(); + } + + public async fetchMore(): Promise> { + // if the wrapped endpoint has different implementation for fetchMore use that + // otherwise use the default implementation + if (typeof this.endpoint.fetchMore === "function") { + return this.endpoint.fetchMore(); + } else { + this.fetchBuffer = []; + this.fetchMoreRespHeaders = HeaderUtils.getInitialHeader(); + return this._fetchMoreImplementation(); + } + } + + private async _fetchMoreImplementation(): Promise> { + try { + const { result: item, headers } = await this.endpoint.nextItem(); + HeaderUtils.mergeHeaders(this.fetchMoreRespHeaders, headers); + if (item === undefined) { + // no more results + if (this.fetchBuffer.length === 0) { + return { + result: undefined, + headers: this.fetchMoreRespHeaders + }; + } else { + // Just give what we have + const temp = this.fetchBuffer; + this.fetchBuffer = []; + return { result: temp, headers: this.fetchMoreRespHeaders }; + } + } else { + // append the result + this.fetchBuffer.push(item); + if (this.fetchBuffer.length >= this.pageSize) { + // fetched enough results + const temp = this.fetchBuffer.slice(0, this.pageSize); + this.fetchBuffer = this.fetchBuffer.splice(this.pageSize); + return { result: temp, headers: this.fetchMoreRespHeaders }; + } else { + // recursively fetch more + // TODO: is recursion a good idea? + return this._fetchMoreImplementation(); + } + } + } catch (err) { + HeaderUtils.mergeHeaders(this.fetchMoreRespHeaders, err.headers); + err.headers = this.fetchMoreRespHeaders; + if (err) { + throw err; + } + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryExecutionContext/proxyQueryExecutionContext.ts b/sdk/cosmosdb/cosmos/src/queryExecutionContext/proxyQueryExecutionContext.ts new file mode 100644 index 000000000000..44b7801246a0 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryExecutionContext/proxyQueryExecutionContext.ts @@ -0,0 +1,154 @@ +import { + DefaultQueryExecutionContext, + FetchFunctionCallback, + IExecutionContext, + PartitionedQueryExecutionContextInfo, + PipelinedQueryExecutionContext, + SqlQuerySpec +} from "."; +import { ClientContext } from "../ClientContext"; +import { StatusCodes, SubStatusCodes } from "../common"; +import { Response } from "../request/request"; + +/** @hidden */ +export class ProxyQueryExecutionContext implements IExecutionContext { + private queryExecutionContext: IExecutionContext; + + constructor( + private clientContext: ClientContext, + private query: SqlQuerySpec | string, + private options: any, // TODO: any options + private fetchFunctions: FetchFunctionCallback | FetchFunctionCallback[], + private resourceLink: string | string[] + ) { + this.query = query; + this.fetchFunctions = fetchFunctions; + // clone options + this.options = JSON.parse(JSON.stringify(options || {})); + this.resourceLink = resourceLink; + this.queryExecutionContext = new DefaultQueryExecutionContext( + this.clientContext, + this.query, + this.options, + this.fetchFunctions + ); + } + /** + * Execute a provided function on the next element in the ProxyQueryExecutionContext. + * @memberof ProxyQueryExecutionContext + * @instance + * @param {callback} callback - Function to execute for each element. \ + * the function takes two parameters error, element. + */ + public async nextItem(): Promise> { + try { + const r = await this.queryExecutionContext.nextItem(); + return r; + } catch (err) { + if (this._hasPartitionedExecutionInfo(err)) { + // if this's a partitioned execution info switches the execution context + const partitionedExecutionInfo = this._getParitionedExecutionInfo(err); + this.queryExecutionContext = this._createPipelinedExecutionContext(partitionedExecutionInfo); + try { + // TODO: recusion might be bad... + return this.nextItem(); + } catch (e) { + throw e; + } + } else { + throw err; + } + } + } + + private _createPipelinedExecutionContext(partitionedExecutionInfo: PartitionedQueryExecutionContextInfo) { + if (!this.resourceLink) { + throw new Error("for top/orderby resourceLink is required"); + } + if (Array.isArray(this.resourceLink) && this.resourceLink.length !== 1) { + throw new Error("for top/orderby exactly one collectionLink is required"); + } + + const collectionLink = Array.isArray(this.resourceLink) ? this.resourceLink[0] : this.resourceLink; + + return new PipelinedQueryExecutionContext( + this.clientContext, + collectionLink, + this.query, + this.options, + partitionedExecutionInfo + ); + } + + /** + * Retrieve the current element on the ProxyQueryExecutionContext. + * @memberof ProxyQueryExecutionContext + * @instance + * @param {callback} callback - Function to execute for the current element. \ + * the function takes two parameters error, element. + */ + public async current(): Promise> { + try { + return await this.queryExecutionContext.current(); + } catch (err) { + if (this._hasPartitionedExecutionInfo(err)) { + // if this's a partitioned execution info switches the execution context + const partitionedExecutionInfo = this._getParitionedExecutionInfo(err); + this.queryExecutionContext = this._createPipelinedExecutionContext(partitionedExecutionInfo); + + // TODO: recursion + try { + return this.current(); + } catch (e) { + throw e; + } + } else { + throw err; + } + } + } + + /** + * Determine if there are still remaining resources to process. + * @memberof ProxyQueryExecutionContext + * @instance + * @returns {Boolean} true if there is other elements to process in the ProxyQueryExecutionContext. + */ + public hasMoreResults() { + return this.queryExecutionContext.hasMoreResults(); + } + + public async fetchMore(): Promise> { + try { + return await this.queryExecutionContext.fetchMore(); + } catch (err) { + if (this._hasPartitionedExecutionInfo(err)) { + // if this's a partitioned execution info switches the execution context + const partitionedExecutionInfo = this._getParitionedExecutionInfo(err); + this.queryExecutionContext = this._createPipelinedExecutionContext(partitionedExecutionInfo); + try { + // TODO: maybe should move the others to use this pattern as it avoid the recursion issue. + return this.queryExecutionContext.fetchMore(); + } catch (e) { + throw e; + } + } else { + throw err; + } + } + } + + private _hasPartitionedExecutionInfo(error: any) { + // TODO: any error + return ( + error.code === StatusCodes.BadRequest && + "substatus" in error && + error["substatus"] === SubStatusCodes.CrossPartitionQueryNotServable + ); + } + + private _getParitionedExecutionInfo(error: any) { + // TODO: any error + return JSON.parse(JSON.parse(error.body).additionalErrorInfo); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryIterator.ts b/sdk/cosmosdb/cosmos/src/queryIterator.ts new file mode 100644 index 000000000000..66121c73ac04 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryIterator.ts @@ -0,0 +1,189 @@ +/// +import { ClientContext } from "./ClientContext"; +import { + FetchFunctionCallback, + IExecutionContext, + IHeaders, + ProxyQueryExecutionContext, + SqlQuerySpec +} from "./queryExecutionContext"; +import { FeedOptions } from "./request/FeedOptions"; +import { Response } from "./request/request"; + +/** + * Represents a QueryIterator Object, an implmenetation of feed or query response that enables + * traversal and iterating over the response + * in the Azure Cosmos DB database service. + */ +export class QueryIterator { + private toArrayTempResources: T[]; // TODO + private toArrayLastResHeaders: IHeaders; + private queryExecutionContext: IExecutionContext; + /** + * @hidden + */ + constructor( + private clientContext: ClientContext, + private query: SqlQuerySpec | string, + private options: FeedOptions, + private fetchFunctions: FetchFunctionCallback | FetchFunctionCallback[], + private resourceLink?: string | string[] + ) { + this.query = query; + this.fetchFunctions = fetchFunctions; + this.options = options; + this.resourceLink = resourceLink; + this.queryExecutionContext = this._createQueryExecutionContext(); + } + + /** + * Calls a specified callback for each item returned from the query. + * Runs serially; each callback blocks the next. + * + * @param callback Specified callback. + * First param is the result, + * second param (optional) is the current headers object state, + * third param (optional) is current index. + * No more callbacks will be called if one of them results false. + * + * @returns Promise - you should await or .catch the Promise in case there are any errors + * + * @example Iterate over all databases + * ```typescript + * await client.databases.readAll().forEach((db, headers, index) => { + * console.log(`Got ${db.id} from forEach`); + * }) + * ``` + */ + public async forEach(callback: (result: T, headers?: IHeaders, index?: number) => boolean | void): Promise { + this.reset(); + let index = 0; + while (this.queryExecutionContext.hasMoreResults()) { + const result = await this.queryExecutionContext.nextItem(); + if (result.result === undefined) { + return; + } + if (callback(result.result, result.headers, index) === false) { + return; + } else { + ++index; + } + } + } + + /** + * Gets an async iterator that will yield results until completion. + * + * NOTE: AsyncIterators are a very new feature and you might need to + * use polyfils/etc. in order to use them in your code. + * + * If you're using TypeScript, you can use the following polyfill as long + * as you target ES6 or higher and are running on Node 6 or higher. + * + * ```typescript + * if (!Symbol || !Symbol.asyncIterator) { + * (Symbol as any).asyncIterator = Symbol.for("Symbol.asyncIterator"); + * } + * ``` + * + * @see QueryIterator.forEach for very similar functionality. + * + * @example Iterate over all databases + * ```typescript + * for await(const {result: db} in client.databases.readAll().getAsyncIterator()) { + * console.log(`Got ${db.id} from AsyncIterator`); + * } + * ``` + */ + public async *getAsyncIterator(): AsyncIterable> { + this.reset(); + while (this.queryExecutionContext.hasMoreResults()) { + const result = await this.queryExecutionContext.nextItem(); + if (result.result === undefined) { + return; + } + yield result; + } + } + + /** + * Execute a provided function on the next element in the QueryIterator. + */ + public async nextItem(): Promise> { + return this.queryExecutionContext.nextItem(); + } + + /** + * Retrieve the current element on the QueryIterator. + */ + public async current(): Promise> { + return this.queryExecutionContext.current(); + } + + // TODO: why is has more results deprecated? + /** + * @deprecated Instead check if nextItem() or current() returns undefined. + * + * Determine if there are still remaining resources to processs based on the value of the continuation token or the\ + * elements remaining on the current batch in the QueryIterator. + * @returns {Boolean} true if there is other elements to process in the QueryIterator. + */ + public hasMoreResults(): boolean { + return this.queryExecutionContext.hasMoreResults(); + } + + /** + * Retrieve all the elements of the feed and pass them as an array to a function + */ + public async toArray(): Promise> { + if (arguments.length !== 0) { + throw new Error("toArray takes no arguments"); + } + this.reset(); + this.toArrayTempResources = []; + return this._toArrayImplementation(); + } + + /** + * Retrieve the next batch of the feed and pass them as an array to a function + */ + public async executeNext(): Promise> { + return this.queryExecutionContext.fetchMore(); + } + + /** + * Reset the QueryIterator to the beginning and clear all the resources inside it + */ + public reset() { + this.queryExecutionContext = this._createQueryExecutionContext(); + } + + private async _toArrayImplementation(): Promise> { + while (this.queryExecutionContext.hasMoreResults()) { + const { result, headers } = await this.queryExecutionContext.nextItem(); + // concatinate the results and fetch more + this.toArrayLastResHeaders = headers; + + if (result === undefined) { + // no more results + break; + } + + this.toArrayTempResources.push(result); + } + return { + result: this.toArrayTempResources, + headers: this.toArrayLastResHeaders + }; + } + + private _createQueryExecutionContext() { + return new ProxyQueryExecutionContext( + this.clientContext, + this.query, + this.options, + this.fetchFunctions, + this.resourceLink + ); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryMetrics/clientSideMetrics.ts b/sdk/cosmosdb/cosmos/src/queryMetrics/clientSideMetrics.ts new file mode 100644 index 000000000000..e2a48064a175 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryMetrics/clientSideMetrics.ts @@ -0,0 +1,35 @@ +import { QueryMetricsUtils } from "./queryMetricsUtils"; + +export class ClientSideMetrics { + constructor(public readonly requestCharge: number) {} + + /** + * Adds one or more ClientSideMetrics to a copy of this instance and returns the result. + */ + public add(...clientSideMetricsArray: ClientSideMetrics[]) { + if (arguments == null || arguments.length === 0) { + throw new Error("arguments was null or empty"); + } + + let requestCharge = this.requestCharge; + for (const clientSideMetrics of clientSideMetricsArray) { + if (clientSideMetrics == null) { + throw new Error("clientSideMetrics has null or undefined item(s)"); + } + + requestCharge += clientSideMetrics.requestCharge; + } + + return new ClientSideMetrics(requestCharge); + } + + public static readonly zero = new ClientSideMetrics(0); + + public static createFromArray(...clientSideMetricsArray: ClientSideMetrics[]) { + if (clientSideMetricsArray == null) { + throw new Error("clientSideMetricsArray is null or undefined item(s)"); + } + + return this.zero.add(...clientSideMetricsArray); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryMetrics/index.ts b/sdk/cosmosdb/cosmos/src/queryMetrics/index.ts new file mode 100644 index 000000000000..74bba09cae48 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryMetrics/index.ts @@ -0,0 +1,7 @@ +export { ClientSideMetrics } from "./clientSideMetrics"; +export { QueryMetrics } from "./queryMetrics"; +export { default as QueryMetricsConstants } from "./queryMetricsConstants"; +export { QueryMetricsUtils } from "./queryMetricsUtils"; +export { QueryPreparationTimes } from "./queryPreparationTime"; +export { RuntimeExecutionTimes } from "./runtimeExecutionTimes"; +export { TimeSpan } from "./timeSpan"; diff --git a/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetrics.ts b/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetrics.ts new file mode 100644 index 000000000000..9f65dd415f7e --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetrics.ts @@ -0,0 +1,213 @@ +import { ClientSideMetrics } from "./clientSideMetrics"; +import QueryMetricsConstants from "./queryMetricsConstants"; +import { QueryMetricsUtils } from "./queryMetricsUtils"; +import { QueryPreparationTimes } from "./queryPreparationTime"; +import { RuntimeExecutionTimes } from "./runtimeExecutionTimes"; +import { TimeSpan } from "./timeSpan"; + +export class QueryMetrics { + constructor( + public readonly retrievedDocumentCount: number, + public readonly retrievedDocumentSize: number, + public readonly outputDocumentCount: number, + public readonly outputDocumentSize: number, + public readonly indexHitDocumentCount: number, + public readonly totalQueryExecutionTime: TimeSpan, + public readonly queryPreparationTimes: QueryPreparationTimes, + public readonly indexLookupTime: TimeSpan, + public readonly documentLoadTime: TimeSpan, + public readonly vmExecutionTime: TimeSpan, + public readonly runtimeExecutionTimes: RuntimeExecutionTimes, + public readonly documentWriteTime: TimeSpan, + public readonly clientSideMetrics: ClientSideMetrics + ) {} + + /** + * Gets the IndexHitRatio + * @memberof QueryMetrics + * @instance + * @ignore + */ + public get indexHitRatio() { + return this.retrievedDocumentCount === 0 ? 1 : this.indexHitDocumentCount / this.retrievedDocumentCount; + } + + /** + * returns a new QueryMetrics instance that is the addition of this and the arguments. + */ + public add(queryMetricsArray: QueryMetrics[]) { + if (arguments == null || arguments.length === 0) { + throw new Error("arguments was null or empty"); + } + + let retrievedDocumentCount = 0; + let retrievedDocumentSize = 0; + let outputDocumentCount = 0; + let outputDocumentSize = 0; + let indexHitDocumentCount = 0; + let totalQueryExecutionTime = TimeSpan.zero; + const queryPreparationTimesArray = []; + let indexLookupTime = TimeSpan.zero; + let documentLoadTime = TimeSpan.zero; + let vmExecutionTime = TimeSpan.zero; + const runtimeExecutionTimesArray = []; + let documentWriteTime = TimeSpan.zero; + const clientSideQueryMetricsArray = []; + + queryMetricsArray.push(this); + + for (const queryMetrics of queryMetricsArray) { + if (queryMetrics == null) { + throw new Error("queryMetricsArray has null or undefined item(s)"); + } + + retrievedDocumentCount += queryMetrics.retrievedDocumentCount; + retrievedDocumentSize += queryMetrics.retrievedDocumentSize; + outputDocumentCount += queryMetrics.outputDocumentCount; + outputDocumentSize += queryMetrics.outputDocumentSize; + indexHitDocumentCount += queryMetrics.indexHitDocumentCount; + totalQueryExecutionTime = totalQueryExecutionTime.add(queryMetrics.totalQueryExecutionTime); + queryPreparationTimesArray.push(queryMetrics.queryPreparationTimes); + indexLookupTime = indexLookupTime.add(queryMetrics.indexLookupTime); + documentLoadTime = documentLoadTime.add(queryMetrics.documentLoadTime); + vmExecutionTime = vmExecutionTime.add(queryMetrics.vmExecutionTime); + runtimeExecutionTimesArray.push(queryMetrics.runtimeExecutionTimes); + documentWriteTime = documentWriteTime.add(queryMetrics.documentWriteTime); + clientSideQueryMetricsArray.push(queryMetrics.clientSideMetrics); + } + + return new QueryMetrics( + retrievedDocumentCount, + retrievedDocumentSize, + outputDocumentCount, + outputDocumentSize, + indexHitDocumentCount, + totalQueryExecutionTime, + QueryPreparationTimes.createFromArray(queryPreparationTimesArray), + indexLookupTime, + documentLoadTime, + vmExecutionTime, + RuntimeExecutionTimes.createFromArray(runtimeExecutionTimesArray), + documentWriteTime, + ClientSideMetrics.createFromArray(...clientSideQueryMetricsArray) + ); + } + + /** + * Output the QueryMetrics as a delimited string. + * @memberof QueryMetrics + * @instance + * @ignore + */ + public toDelimitedString() { + return ( + QueryMetricsConstants.RetrievedDocumentCount + + "=" + + this.retrievedDocumentCount + + ";" + + QueryMetricsConstants.RetrievedDocumentSize + + "=" + + this.retrievedDocumentSize + + ";" + + QueryMetricsConstants.OutputDocumentCount + + "=" + + this.outputDocumentCount + + ";" + + QueryMetricsConstants.OutputDocumentSize + + "=" + + this.outputDocumentSize + + ";" + + QueryMetricsConstants.IndexHitRatio + + "=" + + this.indexHitRatio + + ";" + + QueryMetricsConstants.TotalQueryExecutionTimeInMs + + "=" + + this.totalQueryExecutionTime.totalMilliseconds() + + ";" + + this.queryPreparationTimes.toDelimitedString() + + ";" + + QueryMetricsConstants.IndexLookupTimeInMs + + "=" + + this.indexLookupTime.totalMilliseconds() + + ";" + + QueryMetricsConstants.DocumentLoadTimeInMs + + "=" + + this.documentLoadTime.totalMilliseconds() + + ";" + + QueryMetricsConstants.VMExecutionTimeInMs + + "=" + + this.vmExecutionTime.totalMilliseconds() + + ";" + + this.runtimeExecutionTimes.toDelimitedString() + + ";" + + QueryMetricsConstants.DocumentWriteTimeInMs + + "=" + + this.documentWriteTime.totalMilliseconds() + ); + } + + public static readonly zero = new QueryMetrics( + 0, + 0, + 0, + 0, + 0, + TimeSpan.zero, + QueryPreparationTimes.zero, + TimeSpan.zero, + TimeSpan.zero, + TimeSpan.zero, + RuntimeExecutionTimes.zero, + TimeSpan.zero, + ClientSideMetrics.zero + ); + + /** + * Returns a new instance of the QueryMetrics class that is the aggregation of an array of query metrics. + * @memberof QueryMetrics + * @instance + */ + public static createFromArray(queryMetricsArray: QueryMetrics[]) { + if (queryMetricsArray == null) { + throw new Error("queryMetricsArray is null or undefined item(s)"); + } + + return QueryMetrics.zero.add(queryMetricsArray); + } + + /** + * Returns a new instance of the QueryMetrics class this is deserialized from a delimited string. + * @memberof QueryMetrics + * @instance + */ + public static createFromDelimitedString(delimitedString: string, clientSideMetrics?: ClientSideMetrics) { + const metrics = QueryMetricsUtils.parseDelimitedString(delimitedString); + + const indexHitRatio = metrics[QueryMetricsConstants.IndexHitRatio] || 0; + const retrievedDocumentCount = metrics[QueryMetricsConstants.RetrievedDocumentCount] || 0; + const indexHitCount = indexHitRatio * retrievedDocumentCount; + const outputDocumentCount = metrics[QueryMetricsConstants.OutputDocumentCount] || 0; + const outputDocumentSize = metrics[QueryMetricsConstants.OutputDocumentSize] || 0; + const retrievedDocumentSize = metrics[QueryMetricsConstants.RetrievedDocumentSize] || 0; + const totalQueryExecutionTime = QueryMetricsUtils.timeSpanFromMetrics( + metrics, + QueryMetricsConstants.TotalQueryExecutionTimeInMs + ); + return new QueryMetrics( + retrievedDocumentCount, + retrievedDocumentSize, + outputDocumentCount, + outputDocumentSize, + indexHitCount, + totalQueryExecutionTime, + QueryPreparationTimes.createFromDelimitedString(delimitedString), + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.IndexLookupTimeInMs), + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.DocumentLoadTimeInMs), + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.VMExecutionTimeInMs), + RuntimeExecutionTimes.createFromDelimitedString(delimitedString), + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.DocumentWriteTimeInMs), + clientSideMetrics || ClientSideMetrics.zero + ); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetricsConstants.ts b/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetricsConstants.ts new file mode 100644 index 000000000000..8eadeb10a672 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetricsConstants.ts @@ -0,0 +1,61 @@ +export default { + // QueryMetrics + RetrievedDocumentCount: "retrievedDocumentCount", + RetrievedDocumentSize: "retrievedDocumentSize", + OutputDocumentCount: "outputDocumentCount", + OutputDocumentSize: "outputDocumentSize", + IndexHitRatio: "indexUtilizationRatio", + IndexHitDocumentCount: "indexHitDocumentCount", + TotalQueryExecutionTimeInMs: "totalExecutionTimeInMs", + + // QueryPreparationTimes + QueryCompileTimeInMs: "queryCompileTimeInMs", + LogicalPlanBuildTimeInMs: "queryLogicalPlanBuildTimeInMs", + PhysicalPlanBuildTimeInMs: "queryPhysicalPlanBuildTimeInMs", + QueryOptimizationTimeInMs: "queryOptimizationTimeInMs", + + // QueryTimes + IndexLookupTimeInMs: "indexLookupTimeInMs", + DocumentLoadTimeInMs: "documentLoadTimeInMs", + VMExecutionTimeInMs: "VMExecutionTimeInMs", + DocumentWriteTimeInMs: "writeOutputTimeInMs", + + // RuntimeExecutionTimes + QueryEngineTimes: "queryEngineTimes", + SystemFunctionExecuteTimeInMs: "systemFunctionExecuteTimeInMs", + UserDefinedFunctionExecutionTimeInMs: "userFunctionExecuteTimeInMs", + + // QueryMetrics Text + RetrievedDocumentCountText: "Retrieved Document Count", + RetrievedDocumentSizeText: "Retrieved Document Size", + OutputDocumentCountText: "Output Document Count", + OutputDocumentSizeText: "Output Document Size", + IndexUtilizationText: "Index Utilization", + TotalQueryExecutionTimeText: "Total Query Execution Time", + + // QueryPreparationTimes Text + QueryPreparationTimesText: "Query Preparation Times", + QueryCompileTimeText: "Query Compilation Time", + LogicalPlanBuildTimeText: "Logical Plan Build Time", + PhysicalPlanBuildTimeText: "Physical Plan Build Time", + QueryOptimizationTimeText: "Query Optimization Time", + + // QueryTimes Text + QueryEngineTimesText: "Query Engine Times", + IndexLookupTimeText: "Index Lookup Time", + DocumentLoadTimeText: "Document Load Time", + WriteOutputTimeText: "Document Write Time", + + // RuntimeExecutionTimes Text + RuntimeExecutionTimesText: "Runtime Execution Times", + TotalExecutionTimeText: "Query Engine Execution Time", + SystemFunctionExecuteTimeText: "System Function Execution Time", + UserDefinedFunctionExecutionTimeText: "User-defined Function Execution Time", + + // ClientSideQueryMetrics Text + ClientSideQueryMetricsText: "Client Side Metrics", + RetriesText: "Retry Count", + RequestChargeText: "Request Charge", + FetchExecutionRangesText: "Partition Execution Timeline", + SchedulingMetricsText: "Scheduling Metrics" +}; diff --git a/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetricsUtils.ts b/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetricsUtils.ts new file mode 100644 index 000000000000..3aca74aedcb9 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryMetrics/queryMetricsUtils.ts @@ -0,0 +1,39 @@ +import { TimeSpan } from "./timeSpan"; + +export class QueryMetricsUtils { + public static parseDelimitedString(delimitedString: string) { + if (delimitedString == null) { + throw new Error("delimitedString is null or undefined"); + } + + const metrics: { [key: string]: any } = {}; + + const headerAttributes = delimitedString.split(";"); + for (const attribute of headerAttributes) { + const attributeKeyValue = attribute.split("="); + + if (attributeKeyValue.length !== 2) { + throw new Error("recieved a malformed delimited string"); + } + + const attributeKey = attributeKeyValue[0]; + const attributeValue = parseFloat(attributeKeyValue[1]); + + metrics[attributeKey] = attributeValue; + } + + return metrics; + } + + public static timeSpanFromMetrics(metrics: { [key: string]: any } /* TODO: any */, key: string) { + if (key in metrics) { + return TimeSpan.fromMilliseconds(metrics[key]); + } + + return TimeSpan.zero; + } + + public static isNumeric(input: any) { + return !isNaN(parseFloat(input)) && isFinite(input); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryMetrics/queryPreparationTime.ts b/sdk/cosmosdb/cosmos/src/queryMetrics/queryPreparationTime.ts new file mode 100644 index 000000000000..194aa3e873d0 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryMetrics/queryPreparationTime.ts @@ -0,0 +1,88 @@ +import QueryMetricsConstants from "./queryMetricsConstants"; +import { QueryMetricsUtils } from "./queryMetricsUtils"; +import { TimeSpan } from "./timeSpan"; + +export class QueryPreparationTimes { + constructor( + public readonly queryCompilationTime: TimeSpan, + public readonly logicalPlanBuildTime: TimeSpan, + public readonly physicalPlanBuildTime: TimeSpan, + public readonly queryOptimizationTime: TimeSpan + ) {} + + /** + * returns a new QueryPreparationTimes instance that is the addition of this and the arguments. + */ + public add(...queryPreparationTimesArray: QueryPreparationTimes[]) { + if (arguments == null || arguments.length === 0) { + throw new Error("arguments was null or empty"); + } + + let queryCompilationTime = this.queryCompilationTime; + let logicalPlanBuildTime = this.logicalPlanBuildTime; + let physicalPlanBuildTime = this.physicalPlanBuildTime; + let queryOptimizationTime = this.queryOptimizationTime; + + for (const queryPreparationTimes of queryPreparationTimesArray) { + if (queryPreparationTimes == null) { + throw new Error("queryPreparationTimesArray has null or undefined item(s)"); + } + + queryCompilationTime = queryCompilationTime.add(queryPreparationTimes.queryCompilationTime); + logicalPlanBuildTime = logicalPlanBuildTime.add(queryPreparationTimes.logicalPlanBuildTime); + physicalPlanBuildTime = physicalPlanBuildTime.add(queryPreparationTimes.physicalPlanBuildTime); + queryOptimizationTime = queryOptimizationTime.add(queryPreparationTimes.queryOptimizationTime); + } + + return new QueryPreparationTimes( + queryCompilationTime, + logicalPlanBuildTime, + physicalPlanBuildTime, + queryOptimizationTime + ); + } + + /** + * Output the QueryPreparationTimes as a delimited string. + */ + public toDelimitedString() { + return ( + `${QueryMetricsConstants.QueryCompileTimeInMs}=${this.queryCompilationTime.totalMilliseconds()};` + + `${QueryMetricsConstants.LogicalPlanBuildTimeInMs}=${this.logicalPlanBuildTime.totalMilliseconds()};` + + `${QueryMetricsConstants.PhysicalPlanBuildTimeInMs}=${this.physicalPlanBuildTime.totalMilliseconds()};` + + `${QueryMetricsConstants.QueryOptimizationTimeInMs}=${this.queryOptimizationTime.totalMilliseconds()}` + ); + } + + public static readonly zero = new QueryPreparationTimes(TimeSpan.zero, TimeSpan.zero, TimeSpan.zero, TimeSpan.zero); + + /** + * Returns a new instance of the QueryPreparationTimes class that is the + * aggregation of an array of QueryPreparationTimes. + * @memberof QueryMetrics + * @instance + */ + public static createFromArray(queryPreparationTimesArray: QueryPreparationTimes[]) { + if (queryPreparationTimesArray == null) { + throw new Error("queryPreparationTimesArray is null or undefined item(s)"); + } + + return QueryPreparationTimes.zero.add(...queryPreparationTimesArray); + } + + /** + * Returns a new instance of the QueryPreparationTimes class this is deserialized from a delimited string. + * @memberof QueryMetrics + * @instance + */ + public static createFromDelimitedString(delimitedString: string) { + const metrics = QueryMetricsUtils.parseDelimitedString(delimitedString); + + return new QueryPreparationTimes( + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.QueryCompileTimeInMs), + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.LogicalPlanBuildTimeInMs), + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.PhysicalPlanBuildTimeInMs), + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.QueryOptimizationTimeInMs) + ); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryMetrics/runtimeExecutionTimes.ts b/sdk/cosmosdb/cosmos/src/queryMetrics/runtimeExecutionTimes.ts new file mode 100644 index 000000000000..804ac3dfa4f0 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryMetrics/runtimeExecutionTimes.ts @@ -0,0 +1,98 @@ +import QueryMetricsConstants from "./queryMetricsConstants"; +import { QueryMetricsUtils } from "./queryMetricsUtils"; +import { TimeSpan } from "./timeSpan"; + +export class RuntimeExecutionTimes { + constructor( + public readonly queryEngineExecutionTime: TimeSpan, + public readonly systemFunctionExecutionTime: TimeSpan, + public readonly userDefinedFunctionExecutionTime: TimeSpan + ) {} + + /** + * returns a new RuntimeExecutionTimes instance that is the addition of this and the arguments. + */ + public add(...runtimeExecutionTimesArray: RuntimeExecutionTimes[]) { + if (arguments == null || arguments.length === 0) { + throw new Error("arguments was null or empty"); + } + + let queryEngineExecutionTime = this.queryEngineExecutionTime; + let systemFunctionExecutionTime = this.systemFunctionExecutionTime; + let userDefinedFunctionExecutionTime = this.userDefinedFunctionExecutionTime; + + for (const runtimeExecutionTimes of runtimeExecutionTimesArray) { + if (runtimeExecutionTimes == null) { + throw new Error("runtimeExecutionTimes has null or undefined item(s)"); + } + + queryEngineExecutionTime = queryEngineExecutionTime.add(runtimeExecutionTimes.queryEngineExecutionTime); + systemFunctionExecutionTime = systemFunctionExecutionTime.add(runtimeExecutionTimes.systemFunctionExecutionTime); + userDefinedFunctionExecutionTime = userDefinedFunctionExecutionTime.add( + runtimeExecutionTimes.userDefinedFunctionExecutionTime + ); + } + + return new RuntimeExecutionTimes( + queryEngineExecutionTime, + systemFunctionExecutionTime, + userDefinedFunctionExecutionTime + ); + } + + /** + * Output the RuntimeExecutionTimes as a delimited string. + */ + public toDelimitedString() { + // tslint:disable-next-line:max-line-length + return ( + `${ + QueryMetricsConstants.SystemFunctionExecuteTimeInMs + }=${this.systemFunctionExecutionTime.totalMilliseconds()};` + + // tslint:disable-next-line:max-line-length + `${ + QueryMetricsConstants.UserDefinedFunctionExecutionTimeInMs + }=${this.userDefinedFunctionExecutionTime.totalMilliseconds()}` + ); + } + + public static readonly zero = new RuntimeExecutionTimes(TimeSpan.zero, TimeSpan.zero, TimeSpan.zero); + + /** + * Returns a new instance of the RuntimeExecutionTimes class that is + * the aggregation of an array of RuntimeExecutionTimes. + */ + public static createFromArray(runtimeExecutionTimesArray: RuntimeExecutionTimes[]) { + if (runtimeExecutionTimesArray == null) { + throw new Error("runtimeExecutionTimesArray is null or undefined item(s)"); + } + + return RuntimeExecutionTimes.zero.add(...runtimeExecutionTimesArray); + } + + /** + * Returns a new instance of the RuntimeExecutionTimes class this is deserialized from a delimited string. + */ + public static createFromDelimitedString(delimitedString: string) { + const metrics = QueryMetricsUtils.parseDelimitedString(delimitedString); + + const vmExecutionTime = QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.VMExecutionTimeInMs); + const indexLookupTime = QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.IndexLookupTimeInMs); + const documentLoadTime = QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.DocumentLoadTimeInMs); + const documentWriteTime = QueryMetricsUtils.timeSpanFromMetrics( + metrics, + QueryMetricsConstants.DocumentWriteTimeInMs + ); + + let queryEngineExecutionTime = TimeSpan.zero; + queryEngineExecutionTime = queryEngineExecutionTime.add(vmExecutionTime); + queryEngineExecutionTime = queryEngineExecutionTime.subtract(indexLookupTime); + queryEngineExecutionTime = queryEngineExecutionTime.subtract(documentLoadTime); + queryEngineExecutionTime = queryEngineExecutionTime.subtract(documentWriteTime); + return new RuntimeExecutionTimes( + queryEngineExecutionTime, + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.SystemFunctionExecuteTimeInMs), + QueryMetricsUtils.timeSpanFromMetrics(metrics, QueryMetricsConstants.UserDefinedFunctionExecutionTimeInMs) + ); + } +} diff --git a/sdk/cosmosdb/cosmos/src/queryMetrics/timeSpan.ts b/sdk/cosmosdb/cosmos/src/queryMetrics/timeSpan.ts new file mode 100644 index 000000000000..1bf4c2677f17 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/queryMetrics/timeSpan.ts @@ -0,0 +1,273 @@ +// Ported this implementation to javascript: +// https://referencesource.microsoft.com/#mscorlib/system/timespan.cs,83e476c1ae112117 +/** @hidden */ +const ticksPerMillisecond = 10000; +/** @hidden */ +const millisecondsPerTick = 1.0 / ticksPerMillisecond; + +/** @hidden */ +const ticksPerSecond = ticksPerMillisecond * 1000; // 10,000,000 +/** @hidden */ +const secondsPerTick = 1.0 / ticksPerSecond; // 0.0001 + +/** @hidden */ +const ticksPerMinute = ticksPerSecond * 60; // 600,000,000 +/** @hidden */ +const minutesPerTick = 1.0 / ticksPerMinute; // 1.6666666666667e-9 + +/** @hidden */ +const ticksPerHour = ticksPerMinute * 60; // 36,000,000,000 +/** @hidden */ +const hoursPerTick = 1.0 / ticksPerHour; // 2.77777777777777778e-11 + +/** @hidden */ +const ticksPerDay = ticksPerHour * 24; // 864,000,000,000 +/** @hidden */ +const daysPerTick = 1.0 / ticksPerDay; // 1.1574074074074074074e-12 + +/** @hidden */ +const millisPerSecond = 1000; +/** @hidden */ +const millisPerMinute = millisPerSecond * 60; // 60,000 +/** @hidden */ +const millisPerHour = millisPerMinute * 60; // 3,600,000 +/** @hidden */ +const millisPerDay = millisPerHour * 24; // 86,400,000 + +/** @hidden */ +const maxMilliSeconds = Number.MAX_SAFE_INTEGER / ticksPerMillisecond; +/** @hidden */ +const minMilliSeconds = Number.MIN_SAFE_INTEGER / ticksPerMillisecond; + +/** + * Represents a time interval. + * + * @constructor TimeSpan + * @param {number} days - Number of days. + * @param {number} hours - Number of hours. + * @param {number} minutes - Number of minutes. + * @param {number} seconds - Number of seconds. + * @param {number} milliseconds - Number of milliseconds. + * @ignore + */ +export class TimeSpan { + // tslint:disable-next-line:variable-name + protected _ticks: number; + constructor(days: number, hours: number, minutes: number, seconds: number, milliseconds: number) { + // Constructor + if (!Number.isInteger(days)) { + throw new Error("days is not an integer"); + } + + if (!Number.isInteger(hours)) { + throw new Error("hours is not an integer"); + } + + if (!Number.isInteger(minutes)) { + throw new Error("minutes is not an integer"); + } + + if (!Number.isInteger(seconds)) { + throw new Error("seconds is not an integer"); + } + + if (!Number.isInteger(milliseconds)) { + throw new Error("milliseconds is not an integer"); + } + + const totalMilliSeconds = (days * 3600 * 24 + hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds; + if (totalMilliSeconds > maxMilliSeconds || totalMilliSeconds < minMilliSeconds) { + throw new Error("Total number of milliseconds was either too large or too small"); + } + + this._ticks = totalMilliSeconds * ticksPerMillisecond; + } + + /** + * Returns a new TimeSpan object whose value is the sum of the specified TimeSpan object and this instance. + * @param {TimeSpan} ts - The time interval to add. + * @memberof TimeSpan + * @instance + */ + public add(ts: TimeSpan) { + if (TimeSpan.additionDoesOverflow(this._ticks, ts._ticks)) { + throw new Error("Adding the two timestamps causes an overflow."); + } + + const results = this._ticks + ts._ticks; + return TimeSpan.fromTicks(results); + } + + /** + * Returns a new TimeSpan object whose value is the difference of the specified TimeSpan object and this instance. + * @param {TimeSpan} ts - The time interval to subtract. + * @memberof TimeSpan + * @instance + */ + public subtract(ts: TimeSpan) { + if (TimeSpan.subtractionDoesUnderflow(this._ticks, ts._ticks)) { + throw new Error("Subtracting the two timestamps causes an underflow."); + } + + const results = this._ticks - ts._ticks; + return TimeSpan.fromTicks(results); + } + + /** + * Compares this instance to a specified object and returns an integer that indicates whether this + * instance is shorter than, equal to, or longer than the specified object. + * @param {TimeSpan} value - The time interval to add. + * @memberof TimeSpan + * @instance + */ + public compareTo(value: TimeSpan) { + if (value == null) { + return 1; + } + + if (!TimeSpan.isTimeSpan(value)) { + throw new Error("Argument must be a TimeSpan object"); + } + + return TimeSpan.compare(this, value); + } + + /** + * Returns a new TimeSpan object whose value is the absolute value of the current TimeSpan object. + * @memberof TimeSpan + * @instance + */ + public duration() { + return TimeSpan.fromTicks(this._ticks >= 0 ? this._ticks : -this._ticks); + } + + /** + * Returns a value indicating whether this instance is equal to a specified object. + * @memberof TimeSpan + * @param {TimeSpan} value - The time interval to check for equality. + * @instance + */ + public equals(value: TimeSpan) { + if (TimeSpan.isTimeSpan(value)) { + return this._ticks === value._ticks; + } + + return false; + } + + /** + * Returns a new TimeSpan object whose value is the negated value of this instance. + * @memberof TimeSpan + * @param {TimeSpan} value - The time interval to check for equality. + * @instance + */ + public negate() { + return TimeSpan.fromTicks(-this._ticks); + } + + public days() { + return Math.floor(this._ticks / ticksPerDay); + } + + public hours() { + return Math.floor(this._ticks / ticksPerHour); + } + + public milliseconds() { + return Math.floor(this._ticks / ticksPerMillisecond); + } + + public seconds() { + return Math.floor(this._ticks / ticksPerSecond); + } + + public ticks() { + return this._ticks; + } + + public totalDays() { + return this._ticks * daysPerTick; + } + public totalHours() { + return this._ticks * hoursPerTick; + } + + public totalMilliseconds() { + return this._ticks * millisecondsPerTick; + } + + public totalMinutes() { + return this._ticks * minutesPerTick; + } + + public totalSeconds() { + return this._ticks * secondsPerTick; + } + + public static fromTicks(value: number) { + const timeSpan = new TimeSpan(0, 0, 0, 0, 0); + timeSpan._ticks = value; + return timeSpan; + } + + public static readonly zero = new TimeSpan(0, 0, 0, 0, 0); + public static readonly maxValue = TimeSpan.fromTicks(Number.MAX_SAFE_INTEGER); + public static readonly minValue = TimeSpan.fromTicks(Number.MIN_SAFE_INTEGER); + + public static isTimeSpan(timespan: TimeSpan) { + return timespan._ticks; + } + + public static additionDoesOverflow(a: number, b: number) { + const c = a + b; + return a !== c - b || b !== c - a; + } + + public static subtractionDoesUnderflow(a: number, b: number) { + const c = a - b; + return a !== c + b || b !== a - c; + } + + public static compare(t1: TimeSpan, t2: TimeSpan) { + if (t1._ticks > t2._ticks) { + return 1; + } + if (t1._ticks < t2._ticks) { + return -1; + } + return 0; + } + + public static interval(value: number, scale: number) { + if (isNaN(value)) { + throw new Error("value must be a number"); + } + + const milliseconds = value * scale; + if (milliseconds > maxMilliSeconds || milliseconds < minMilliSeconds) { + throw new Error("timespan too long"); + } + + return TimeSpan.fromTicks(Math.floor(milliseconds * ticksPerMillisecond)); + } + + public static fromMilliseconds(value: number) { + return TimeSpan.interval(value, 1); + } + + public static fromSeconds(value: number) { + return TimeSpan.interval(value, millisPerSecond); + } + + public static fromMinutes(value: number) { + return TimeSpan.interval(value, millisPerMinute); + } + + public static fromHours(value: number) { + return TimeSpan.interval(value, millisPerHour); + } + + public static fromDays(value: number) { + return TimeSpan.interval(value, millisPerDay); + } +} diff --git a/sdk/cosmosdb/cosmos/src/range/Range.ts b/sdk/cosmosdb/cosmos/src/range/Range.ts new file mode 100644 index 000000000000..fba13bd3fcc7 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/range/Range.ts @@ -0,0 +1,136 @@ +import { PartitionKey } from "../documents"; + +/** @hidden */ +export type CompareFunction = (x: Point, y: Point) => number; + +/** @hidden */ +export type Point = number | string; + +/** @hidden */ +export class Range { + public readonly low: Point; + public readonly high: Point; + + /** + * Represents a range object used by the RangePartitionResolver in the Azure Cosmos DB database service. + * @class Range + * @param {object} options - The Range constructor options. + * @param {any} options.low - The low value in the range. + * @param {any} options.high - The high value in the range. + */ + constructor(options?: any) { + // TODO: any options + if (options === undefined) { + options = {}; + } + if (options === null) { + throw new Error("Invalid argument: 'options' is null"); + } + if (typeof options !== "object") { + throw new Error("Invalid argument: 'options' is not an object"); + } + if (options.high === undefined) { + options.high = options.low; + } + this.low = options.low; + this.high = options.high; + + Object.freeze(this); + } + + // TODO: private? + public _compare(x: Point, y: Point, compareFunction?: CompareFunction) { + // Same semantics as Array.sort + // http://www.ecma-international.org/ecma-262/6.0/#sec-sortcompare + if (x === undefined && y === undefined) { + return 0; + } + if (x === undefined) { + return 1; + } + if (y === undefined) { + return -1; + } + if (compareFunction !== undefined) { + const v = Number(compareFunction(x, y)); + if (Number.isNaN(v)) { + return 0; + } + return v; + } + const xString = String(x); + const yString = String(y); + if (xString < yString) { + return -1; + } + if (xString > yString) { + return 1; + } + return 0; + } + + // TODO: This is an alias for backwards compatibility. Need to decide if this is public surface area or not + // tslint:disable-next-line:variable-name + public _contains = this.contains; + + public contains(other: Point | Range, compareFunction?: CompareFunction) { + if (Range.isRange(other)) { + return this._containsRange(other as Range, compareFunction); + } else { + return this._containsPoint(other as number, compareFunction); + } + } + + // TODO: private? + public _containsPoint(point: Point, compareFunction?: CompareFunction) { + return ( + this._compare(point, this.low, compareFunction) >= 0 && this._compare(point, this.high, compareFunction) <= 0 + ); + } + + // TODO: private? + public _containsRange(range: Range, compareFunction?: CompareFunction) { + return ( + this._compare(range.low, this.low, compareFunction) >= 0 && + this._compare(range.high, this.high, compareFunction) <= 0 + ); + } + + // TODO: alias for backwards compat + // tslint:disable-next-line:variable-name + public _intersect = this.intersect; + + public intersect(range: Range, compareFunction?: CompareFunction) { + if (range === undefined || range === null) { + throw new Error("Invalid Argument: 'other' is undefined or null"); + } + const maxLow = this._compare(this.low, range.low, compareFunction) >= 0 ? this.low : range.low; + const minHigh = this._compare(this.high, range.high, compareFunction) <= 0 ? this.high : range.high; + return this._compare(maxLow, minHigh, compareFunction) <= 0; + } + + // TODO: alias for backwards compat + // tslint:disable-next-line:variable-name + public _toString = this.toString; + + public toString() { + return String(this.low) + "," + String(this.high); + } + + // TODO: alias for backwards compat + // tslint:disable-next-line:variable-name + public static _isRange = Range.isRange; + + public static isRange(pointOrRange: Point | Range | PartitionKey) { + if (pointOrRange === undefined) { + return false; + } + if (pointOrRange === null) { + return false; + } + if (typeof pointOrRange !== "object") { + return false; + } + return pointOrRange instanceof Range; + } +} diff --git a/sdk/cosmosdb/cosmos/src/range/RangePartitionResolver.ts b/sdk/cosmosdb/cosmos/src/range/RangePartitionResolver.ts new file mode 100644 index 000000000000..fbe00b7ebf5b --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/range/RangePartitionResolver.ts @@ -0,0 +1,153 @@ +import { CompareFunction, Range } from "."; +import { Document, PartitionKey } from "../documents"; + +/** @hidden */ +export type PartitionKeyExtractorFunction = (obj: object) => PartitionKey; +/** @hidden */ +export type PartitionKeyExtractor = string | PartitionKeyExtractorFunction; +/** @hidden */ +export interface PartitionKeyMapItem { + range: Range; + link: string; +} + +/** @hidden */ +export class RangePartitionResolver { + // TODO: should these be public? + public partitionKeyExtractor: PartitionKeyExtractor; + public partitionKeyMap: PartitionKeyMapItem[]; + public compareFunction: CompareFunction; + /** + * RangePartitionResolver implements partitioning using a partition map of ranges of values to a \ + * collection link in the Azure Cosmos DB database service. + * @class RangePartitionResolver + * @param {PartitionKeyExtractor} partitionKeyExtractor - If partitionKeyExtractor is a string, \ + * it should be the name of the property in the document to execute the hashing on. + * If partitionKeyExtractor is a function, \ + * it should be a function to extract the partition key from an object. + * @param {Array} partitionKeyMap - The map from Range to collection link that\ + * is used for partitioning requests. + * @param {function} compareFunction - Optional function that accepts two arguments\ + * x and y and returns a negative value if x < y, zero if x = y, or a positive value if x > y. + */ + constructor( + partitionKeyExtractor: PartitionKeyExtractor, + partitionKeyMap: PartitionKeyMapItem[], + compareFunction?: CompareFunction + ) { + if (partitionKeyExtractor === undefined || partitionKeyExtractor === null) { + throw new Error("partitionKeyExtractor cannot be null or undefined"); + } + if (typeof partitionKeyExtractor !== "string" && typeof partitionKeyExtractor !== "function") { + throw new Error("partitionKeyExtractor must be either a 'string' or a 'function'"); + } + if (partitionKeyMap === undefined || partitionKeyMap === null) { + throw new Error("partitionKeyMap cannot be null or undefined"); + } + if (!Array.isArray(partitionKeyMap)) { + throw new Error("partitionKeyMap has to be an Array"); + } + const allMapEntriesAreValid = partitionKeyMap.every(m => { + if (m === undefined || m === null) { + return false; + } + if (m.range === undefined) { + return false; + } + if (!(m.range instanceof Range)) { + return false; + } + if (m.link === undefined) { + return false; + } + if (typeof m.link !== "string") { + return false; + } + return true; + }); + if (!allMapEntriesAreValid) { + throw new Error("All partitionKeyMap entries have to be a tuple {range: Range, link: string }"); + } + if (compareFunction !== undefined && typeof compareFunction !== "function") { + throw new Error("Invalid argument: 'compareFunction' is not a function"); + } + + this.partitionKeyExtractor = partitionKeyExtractor; + this.partitionKeyMap = partitionKeyMap; + this.compareFunction = compareFunction; + } + + /** + * Extracts the partition key from the specified document using the partitionKeyExtractor + * @memberof RangePartitionResolver + * @instance + * @param {object} document - The document from which to extract the partition key. + * @returns {} + */ + public getPartitionKey(document: Document): PartitionKey { + if (typeof this.partitionKeyExtractor === "string") { + return document[this.partitionKeyExtractor] as number; + } + if (typeof this.partitionKeyExtractor === "function") { + return this.partitionKeyExtractor(document); + } + throw new Error(`Unable to extract partition key from document. \ + Ensure PartitionKeyExtractor is a valid function or property name.`); + } + + /** + * Given a partition key, returns the correct collection link for creating a document using the range partition map. + * @memberof RangePartitionResolver + * @instance + * @param {any} partitionKey - The partition key used to determine the target collection for create + * @returns {string} - The target collection link that will be used for document creation. + */ + public resolveForCreate(partitionKey: PartitionKey) { + const range = new Range({ low: partitionKey }); + const mapEntry = this.getFirstContainingMapEntryOrNull(range); + if (mapEntry !== undefined && mapEntry !== null) { + return mapEntry.link; + } + throw new Error(`Invalid operation: A containing range for \ +'${range.toString()}' doesn't exist in the partition map.`); + } + + /** + * Given a partition key, returns a list of collection links to read from using the range partition map. + * @memberof RangePartitionResolver + * @instance + * @param {any} partitionKey - The partition key used to determine the target collection for query + * @returns {string[]} - The list of target collection links. + */ + public resolveForRead(partitionKey: PartitionKey) { + if (partitionKey === undefined || partitionKey === null) { + return this.partitionKeyMap.map(i => i.link); + } else { + return this._getIntersectingMapEntries(partitionKey).map(i => i.link); + } + } + + // TODO: did this for backwards compat test, probably need to consider making these private + public _getFirstContainingMapEntryOrNull(point: any) { + return this.getFirstContainingMapEntryOrNull(point); + } + public getFirstContainingMapEntryOrNull(point: any) { + // TODO: any Point + const containingMapEntries = this.partitionKeyMap.filter( + p => p.range !== undefined && p.range.contains(point, this.compareFunction) + ); + if (containingMapEntries && containingMapEntries.length > 0) { + return containingMapEntries[0]; + } + return null; + } + + public _getIntersectingMapEntries(partitionKey: PartitionKey) { + const partitionKeys: PartitionKey[] = Array.isArray(partitionKey) ? partitionKey : [partitionKey]; + const ranges: Range[] = partitionKeys.map(p => (Range.isRange(p) ? (p as Range) : new Range({ low: p }))); + + return ranges.reduce((result, range) => { + return result.concat(this.partitionKeyMap.filter(entry => entry.range.intersect(range, this.compareFunction))); + }, []); + } +} diff --git a/sdk/cosmosdb/cosmos/src/range/index.ts b/sdk/cosmosdb/cosmos/src/range/index.ts new file mode 100644 index 000000000000..bb50cf3a68d4 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/range/index.ts @@ -0,0 +1,2 @@ +export * from "./Range"; +export * from "./RangePartitionResolver"; diff --git a/sdk/cosmosdb/cosmos/src/request/CosmosResponse.ts b/sdk/cosmosdb/cosmos/src/request/CosmosResponse.ts new file mode 100644 index 000000000000..f827de5978bf --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/CosmosResponse.ts @@ -0,0 +1,7 @@ +import { IHeaders } from "../queryExecutionContext"; + +export interface CosmosResponse { + body?: T; + headers?: IHeaders; + ref?: U; +} diff --git a/sdk/cosmosdb/cosmos/src/request/ErrorResponse.ts b/sdk/cosmosdb/cosmos/src/request/ErrorResponse.ts new file mode 100644 index 000000000000..e035892f368b --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/ErrorResponse.ts @@ -0,0 +1,11 @@ +import { IHeaders } from ".."; + +export interface ErrorResponse { + code?: number; + substatus?: number; + body?: any; + headers?: IHeaders; + activityId?: string; + retryAfterInMilliseconds?: number; + [key: string]: any; +} diff --git a/sdk/cosmosdb/cosmos/src/request/FeedOptions.ts b/sdk/cosmosdb/cosmos/src/request/FeedOptions.ts new file mode 100644 index 000000000000..9c7a73697e9b --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/FeedOptions.ts @@ -0,0 +1,47 @@ +import { IHeaders } from ".."; + +/** + * The feed options and query methods. + */ +export interface FeedOptions { + /** Opaque token for continuing the enumeration. */ + continuation?: string; + /** + * DisableRUPerMinuteUsage is used to enable/disable Request Units(RUs)/minute capacity to serve + * the request if regular provisioned RUs/second is exhausted. + */ + disableRUPerMinuteUsage?: boolean; + /** + * A value indicating whether users are enabled to send more than one request to execute the query in the Azure Cosmos DB database service. + * + * More than one request is necessary if the query is not scoped to single partition key value. + */ + enableCrossPartitionQuery?: boolean; + /** Allow scan on the queries which couldn't be served as indexing was opted out on the requested paths. */ + enableScanInQuery?: boolean; + /** + * The maximum number of concurrent operations that run client side during parallel query execution in the + * Azure Cosmos DB database service. Negative values make the system automatically decides the number of + * concurrent operations to run. + */ + maxDegreeOfParallelism?: number; + /** Max number of items to be returned in the enumeration operation. */ + maxItemCount?: number; + /** Specifies a partition key definition for a particular path in the Azure Cosmos DB database service. */ + partitionKey?: string; + /** Token for use with Session consistency. */ + sessionToken?: string; + /** (Advanced use case) Initial headers to start with when sending requests to Cosmos */ + initialHeaders?: IHeaders; + /** Indicates a change feed request. Must be set to "Incremental feed", or omitted otherwise. */ + a_im?: string; + /** Conditions Associated with the request. */ + accessCondition?: { + /** Conditional HTTP method header type (IfMatch or IfNoneMatch). */ + type: string; + /** Conditional HTTP method header value (the _etag field from the last version you read). */ + condition: string; + }; + /** Enable returning query metrics in response headers */ + populateQueryMetrics?: boolean; +} diff --git a/sdk/cosmosdb/cosmos/src/request/LocationRouting.ts b/sdk/cosmosdb/cosmos/src/request/LocationRouting.ts new file mode 100644 index 000000000000..2dc81cc96e03 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/LocationRouting.ts @@ -0,0 +1,38 @@ +export class LocationRouting { + private pIgnorePreferredLocation: boolean; + private pLocationIndexToRoute: number; + private pLocationEndpointToRoute: string; + public get ignorePreferredLocation() { + return this.pIgnorePreferredLocation; + } + + public get locationIndexToRoute() { + return this.pLocationIndexToRoute; + } + + public get locationEndpointToRoute() { + return this.pLocationEndpointToRoute; + } + + public routeToLocation(locationEndpoint: string): void; + public routeToLocation(locationIndex: number, ignorePreferredLocation: boolean): void; + public routeToLocation(endpointOrIndex: string | number, ignorePreferredLocation?: boolean) { + if (arguments.length === 2 && typeof endpointOrIndex === "number") { + this.pLocationIndexToRoute = endpointOrIndex; + this.pIgnorePreferredLocation = ignorePreferredLocation; + this.pLocationEndpointToRoute = undefined; + } else if (arguments.length === 1 && typeof endpointOrIndex === "string") { + this.pLocationEndpointToRoute = endpointOrIndex; + this.pLocationIndexToRoute = undefined; + this.pIgnorePreferredLocation = undefined; + } else { + throw new Error("Invalid arguments passed to routeToLocation"); + } + } + + public clearRouteToLocation(): void { + this.pLocationEndpointToRoute = undefined; + this.pLocationIndexToRoute = undefined; + this.pIgnorePreferredLocation = undefined; + } +} diff --git a/sdk/cosmosdb/cosmos/src/request/MediaOptions.ts b/sdk/cosmosdb/cosmos/src/request/MediaOptions.ts new file mode 100644 index 000000000000..87e6298b49a6 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/MediaOptions.ts @@ -0,0 +1,13 @@ +import { IHeaders } from ".."; + +/** + * Options associated with upload media. + */ +export interface MediaOptions { + /** (Advanced use case) Initial headers to start with when sending requests to Cosmos */ + initialHeaders?: IHeaders; + /** HTTP Slug header value. */ + slug?: string; + /** HTTP ContentType header value. */ + contentType?: string; +} diff --git a/sdk/cosmosdb/cosmos/src/request/RequestContext.ts b/sdk/cosmosdb/cosmos/src/request/RequestContext.ts new file mode 100644 index 000000000000..686320355ca8 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/RequestContext.ts @@ -0,0 +1,11 @@ +import { ClientContext } from "../ClientContext"; +import { LocationRouting } from "./LocationRouting"; + +export interface RequestContext { + path?: string; + operationType?: string; + client?: ClientContext; + retryCount?: number; + resourceType?: string; + locationRouting?: LocationRouting; +} diff --git a/sdk/cosmosdb/cosmos/src/request/RequestHandler.ts b/sdk/cosmosdb/cosmos/src/request/RequestHandler.ts new file mode 100644 index 000000000000..db682e8d5cdd --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/RequestHandler.ts @@ -0,0 +1,197 @@ +import { Agent, OutgoingHttpHeaders } from "http"; +import { RequestOptions } from "https"; // TYPES ONLY +import * as querystring from "querystring"; +import { Constants, IHeaders } from ".."; +import { ConnectionPolicy } from "../documents"; +import { GlobalEndpointManager } from "../globalEndpointManager"; +import { RetryUtility } from "../retry"; +import { bodyFromData, createRequestObject, parse, Response } from "./request"; +import { RequestContext } from "./RequestContext"; + +/** @hidden */ +export class RequestHandler { + public constructor( + private globalEndpointManager: GlobalEndpointManager, + private connectionPolicy: ConnectionPolicy, + private requestAgent: Agent + ) {} + public static async createRequestObjectStub( + connectionPolicy: ConnectionPolicy, + requestOptions: RequestOptions, + body: Buffer + ) { + return createRequestObject(connectionPolicy, requestOptions, body); + } + + /** + * Creates the request object, call the passed callback when the response is retrieved. + * @param {object} globalEndpointManager - an instance of GlobalEndpointManager class. + * @param {object} connectionPolicy - an instance of ConnectionPolicy that has the connection configs. + * @param {object} requestAgent - the https agent used for send request + * @param {string} method - the http request method ( 'get', 'post', 'put', .. etc ). + * @param {String} hostname - The base url for the endpoint. + * @param {string} path - the path of the requesed resource. + * @param {Object} data - the request body. It can be either string, buffer, or undefined. + * @param {Object} queryParams - query parameters for the request. + * @param {Object} headers - specific headers for the request. + * @param {function} callback - the callback that will be called when the response is retrieved and processed. + */ + public static async request( + globalEndpointManager: GlobalEndpointManager, + connectionPolicy: ConnectionPolicy, + requestAgent: Agent, + method: string, + hostname: string, + request: RequestContext, + data: string | Buffer, + queryParams: any, // TODO: any query params types + headers: IHeaders + ): Promise> { + // TODO: any + const path = (request as { path: string }).path === undefined ? request : (request as { path: string }).path; + let body: any; // TODO: any + + if (data) { + body = bodyFromData(data); + if (!body) { + return { + result: { + message: "parameter data must be a javascript object, string, or Buffer" + }, + headers: undefined + }; + } + } + + let buffer; + if (body) { + if (Buffer.isBuffer(body)) { + buffer = body; + } else if (typeof body === "string") { + buffer = Buffer.from(body, "utf8"); + } else { + return { + result: { + message: "body must be string or Buffer" + }, + headers: undefined + }; + } + } + + const requestOptions: RequestOptions = parse(hostname); + requestOptions.method = method; + requestOptions.path += path; + requestOptions.headers = headers as OutgoingHttpHeaders; + requestOptions.agent = requestAgent; + requestOptions.secureProtocol = "TLSv1_client_method"; // TODO: Should be a constant + + if (connectionPolicy.DisableSSLVerification === true) { + requestOptions.rejectUnauthorized = false; + } + + if (queryParams) { + requestOptions.path += "?" + querystring.stringify(queryParams); + } + + if (buffer) { + requestOptions.headers[Constants.HttpHeaders.ContentLength] = buffer.length; + return RetryUtility.execute( + globalEndpointManager, + buffer, + this.createRequestObjectStub, + connectionPolicy, + requestOptions, + request + ); + } else { + return RetryUtility.execute( + globalEndpointManager, + null, + this.createRequestObjectStub, + connectionPolicy, + requestOptions, + request + ); + } + } + + /** @ignore */ + public get(urlString: string, request: RequestContext, headers: IHeaders) { + // TODO: any + return RequestHandler.request( + this.globalEndpointManager, + this.connectionPolicy, + this.requestAgent, + "GET", + urlString, + request, + undefined, + "", + headers + ); + } + + /** @ignore */ + public post(urlString: string, request: RequestContext, body: any, headers: IHeaders) { + // TODO: any + return RequestHandler.request( + this.globalEndpointManager, + this.connectionPolicy, + this.requestAgent, + "POST", + urlString, + request, + body, + "", + headers + ); + } + + /** @ignore */ + public put(urlString: string, request: RequestContext, body: any, headers: IHeaders) { + // TODO: any + return RequestHandler.request( + this.globalEndpointManager, + this.connectionPolicy, + this.requestAgent, + "PUT", + urlString, + request, + body, + "", + headers + ); + } + + /** @ignore */ + public head(urlString: string, request: any, headers: IHeaders) { + // TODO: any + return RequestHandler.request( + this.globalEndpointManager, + this.connectionPolicy, + this.requestAgent, + "HEAD", + urlString, + request, + undefined, + "", + headers + ); + } + + /** @ignore */ + public delete(urlString: string, request: RequestContext, headers: IHeaders) { + return RequestHandler.request( + this.globalEndpointManager, + this.connectionPolicy, + this.requestAgent, + "DELETE", + urlString, + request, + undefined, + "", + headers + ); + } +} diff --git a/sdk/cosmosdb/cosmos/src/request/RequestOptions.ts b/sdk/cosmosdb/cosmos/src/request/RequestOptions.ts new file mode 100644 index 000000000000..994329c8389e --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/RequestOptions.ts @@ -0,0 +1,56 @@ +import { IHeaders } from ".."; +import { PartitionKey } from "../documents"; + +/** + * Options that can be specified for a requested issued to the Azure Cosmos DB servers.= + */ +export interface RequestOptions { + /** Conditions Associated with the request. */ + accessCondition?: { + /** Conditional HTTP method header type (IfMatch or IfNoneMatch). */ + type: string; + /** Conditional HTTP method header value (the _etag field from the last version you read). */ + condition: string; + }; + /** Consistency level required by the client. */ + consistencyLevel?: string; + /** + * DisableRUPerMinuteUsage is used to enable/disable Request Units(RUs)/minute capacity + * to serve the request if regular provisioned RUs/second is exhausted. + */ + disableRUPerMinuteUsage?: boolean; + /** Enables or disables logging in JavaScript stored procedures. */ + enableScriptLogging?: boolean; + /** Specifies indexing directives (index, do not index .. etc). */ + indexingDirective?: string; + /** Represents Request Units(RU)/Minute throughput is enabled/disabled for a container. */ + offerEnableRUPerMinuteThroughput?: boolean; + /** The offer throughput provisioned for a container in measurement of Requests-per-Unit. */ + offerThroughput?: number; + /** + * Offer type when creating document containers. + * + * This option is only valid when creating a document container. + */ + offerType?: string; + /** Specifies a partition key definition for a particular path in the Azure Cosmos DB database service. */ + partitionKey?: PartitionKey | PartitionKey[]; + /** Enables/disables getting document container quota related stats for document container read requests. */ + populateQuotaInfo?: boolean; + /** Indicates what is the post trigger to be invoked after the operation. */ + postTriggerInclude?: string | string[]; + /** Indicates what is the pre trigger to be invoked before the operation. */ + preTriggerInclude?: string | string[]; + /** Expiry time (in seconds) for resource token associated with permission (applicable only for requests on permissions). */ + resourceTokenExpirySeconds?: number; + /** Token for use with Session consistency. */ + sessionToken?: string; + /** (Advanced use case) Initial headers to start with when sending requests to Cosmos */ + initialHeaders?: IHeaders; + /** (Advanced use case) The url to connect to. */ + urlConnection?: string; + /** (Advanced use case) Skip getting info on the parititon key from the container. */ + skipGetPartitionKeyDefinition?: boolean; + /** Disable automatic id generation (will cause creates to fail if id isn't on the definition) */ + disableAutomaticIdGeneration?: boolean; +} diff --git a/sdk/cosmosdb/cosmos/src/request/Response.ts b/sdk/cosmosdb/cosmos/src/request/Response.ts new file mode 100644 index 000000000000..c43eea329e51 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/Response.ts @@ -0,0 +1,7 @@ +import { IHeaders } from ".."; + +export interface Response { + headers?: IHeaders; + result?: T; + statusCode?: number; +} diff --git a/sdk/cosmosdb/cosmos/src/request/index.ts b/sdk/cosmosdb/cosmos/src/request/index.ts new file mode 100644 index 000000000000..29235457e015 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/index.ts @@ -0,0 +1,7 @@ +export { ErrorResponse } from "./ErrorResponse"; +export { FeedOptions } from "./FeedOptions"; +export { MediaOptions } from "./MediaOptions"; +export { RequestHandler } from "./RequestHandler"; +export { RequestOptions } from "./RequestOptions"; +export { Response } from "./Response"; +export { CosmosResponse } from "./CosmosResponse"; diff --git a/sdk/cosmosdb/cosmos/src/request/request.ts b/sdk/cosmosdb/cosmos/src/request/request.ts new file mode 100644 index 000000000000..4c7344b6c0a1 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/request/request.ts @@ -0,0 +1,307 @@ +import { ClientRequest, ClientResponse } from "http"; // TYPES ONLY +import * as https from "https"; // TYPES ONLY +import { Socket } from "net"; +import { Stream } from "stream"; +import * as url from "url"; + +import { Constants, Helper } from "../common"; +import { ConnectionPolicy, MediaReadMode } from "../documents"; +import { IHeaders } from "../queryExecutionContext"; + +import { ErrorResponse } from "./ErrorResponse"; +export { ErrorResponse }; // Should refactor this out + +import { FeedOptions, MediaOptions, RequestOptions } from "."; +import { AuthHandler, AuthOptions } from "../auth"; +import { Response } from "./Response"; +export { Response }; // Should refactor this out + +// ---------------------------------------------------------------------------- +// Utility methods +// + +/** @hidden */ +function javaScriptFriendlyJSONStringify(s: object) { + // two line terminators (Line separator and Paragraph separator) are not needed to be escaped in JSON + // but are needed to be escaped in JavaScript. + return JSON.stringify(s) + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029"); +} + +/** @hidden */ +export function bodyFromData(data: Stream | Buffer | string | object) { + if ((data as Stream).pipe) { + return data; + } + if (Buffer.isBuffer(data)) { + return data; + } + if (typeof data === "string") { + return data; + } + if (typeof data === "object") { + return javaScriptFriendlyJSONStringify(data); + } + return undefined; +} + +/** @hidden */ +export function parse(urlString: string) { + return url.parse(urlString); +} + +/** @hidden */ +export function createRequestObject( + connectionPolicy: ConnectionPolicy, + requestOptions: https.RequestOptions, + body: Buffer +): Promise> { + return new Promise>((resolve, reject) => { + function onTimeout() { + httpsRequest.abort(); + } + + const isMedia = requestOptions.path.indexOf("//media") === 0; + + const httpsRequest: ClientRequest = https.request(requestOptions, (response: ClientResponse) => { + // In case of media response, return the stream to the user and the user will need + // to handle reading the stream. + if (isMedia && connectionPolicy.MediaReadMode === MediaReadMode.Streamed) { + return resolve({ + result: response, + headers: response.headers as IHeaders + }); + } + + let data = ""; + + // if the requested data is text (not attachment/media) set the encoding to UTF-8 + if (!isMedia) { + response.setEncoding("utf8"); + } + + response.on("data", chunk => { + data += chunk; + }); + response.on("end", () => { + if (response.statusCode >= 400) { + return reject(getErrorBody(response, data, response.headers as IHeaders)); + } + + let result; + try { + result = isMedia ? data : data.length > 0 ? JSON.parse(data) : undefined; + } catch (exception) { + return reject(exception); + } + + resolve({ result, headers: response.headers as IHeaders, statusCode: response.statusCode }); + }); + }); + + httpsRequest.once("socket", (socket: Socket) => { + if (isMedia) { + socket.setTimeout(connectionPolicy.MediaRequestTimeout); + } else { + socket.setTimeout(connectionPolicy.RequestTimeout); + } + + socket.once("timeout", onTimeout); + + httpsRequest.once("response", () => { + socket.removeListener("timeout", onTimeout); + }); + }); + + httpsRequest.once("error", reject); + + if (body) { + httpsRequest.write(body); + httpsRequest.end(); + } else { + httpsRequest.end(); + } + }); +} + +/** + * Constructs the error body from the response and the data returned from the request. + * @param {object} response - response object returned from the executon of a request. + * @param {object} data - the data body returned from the executon of a request. + * @hidden + */ +function getErrorBody(response: ClientResponse, data: string, headers: IHeaders): ErrorResponse { + const errorBody: ErrorResponse = { + code: response.statusCode, + body: data, + headers + }; + + if (Constants.HttpHeaders.ActivityId in response.headers) { + errorBody.activityId = response.headers[Constants.HttpHeaders.ActivityId] as string; + } + + if (Constants.HttpHeaders.SubStatus in response.headers) { + errorBody.substatus = parseInt(response.headers[Constants.HttpHeaders.SubStatus] as string, 10); + } + + if (Constants.HttpHeaders.RetryAfterInMilliseconds in response.headers) { + errorBody.retryAfterInMilliseconds = parseInt( + response.headers[Constants.HttpHeaders.RetryAfterInMilliseconds] as string, + 10 + ); + } + + return errorBody; +} + +export async function getHeaders( + authOptions: AuthOptions, + defaultHeaders: IHeaders, + verb: string, + path: string, + resourceId: string, + resourceType: string, + options: RequestOptions | FeedOptions | MediaOptions, + partitionKeyRangeId?: string, + useMultipleWriteLocations?: boolean +): Promise { + const headers: IHeaders = { ...defaultHeaders }; + const opts: RequestOptions & FeedOptions & MediaOptions = (options || {}) as any; // TODO: this is dirty + + if (useMultipleWriteLocations) { + headers[Constants.HttpHeaders.ALLOW_MULTIPLE_WRITES] = true; + } + + if (opts.continuation) { + headers[Constants.HttpHeaders.Continuation] = opts.continuation; + } + + if (opts.preTriggerInclude) { + headers[Constants.HttpHeaders.PreTriggerInclude] = + opts.preTriggerInclude.constructor === Array + ? (opts.preTriggerInclude as string[]).join(",") + : (opts.preTriggerInclude as string); + } + + if (opts.postTriggerInclude) { + headers[Constants.HttpHeaders.PostTriggerInclude] = + opts.postTriggerInclude.constructor === Array + ? (opts.postTriggerInclude as string[]).join(",") + : (opts.postTriggerInclude as string); + } + + if (opts.offerType) { + headers[Constants.HttpHeaders.OfferType] = opts.offerType; + } + + if (opts.offerThroughput) { + headers[Constants.HttpHeaders.OfferThroughput] = opts.offerThroughput; + } + + if (opts.maxItemCount) { + headers[Constants.HttpHeaders.PageSize] = opts.maxItemCount; + } + + if (opts.accessCondition) { + if (opts.accessCondition.type === "IfMatch") { + headers[Constants.HttpHeaders.IfMatch] = opts.accessCondition.condition; + } else { + headers[Constants.HttpHeaders.IfNoneMatch] = opts.accessCondition.condition; + } + } + + if (opts.a_im) { + headers[Constants.HttpHeaders.A_IM] = opts.a_im; + } + + if (opts.indexingDirective) { + headers[Constants.HttpHeaders.IndexingDirective] = opts.indexingDirective; + } + + if (opts.consistencyLevel) { + headers[Constants.HttpHeaders.ConsistencyLevel] = opts.consistencyLevel; + } + + if (opts.resourceTokenExpirySeconds) { + headers[Constants.HttpHeaders.ResourceTokenExpiry] = opts.resourceTokenExpirySeconds; + } + + if (opts.sessionToken) { + headers[Constants.HttpHeaders.SessionToken] = opts.sessionToken; + } + + if (opts.enableScanInQuery) { + headers[Constants.HttpHeaders.EnableScanInQuery] = opts.enableScanInQuery; + } + + if (opts.enableCrossPartitionQuery) { + headers[Constants.HttpHeaders.EnableCrossPartitionQuery] = opts.enableCrossPartitionQuery; + } + + if (opts.populateQuotaInfo) { + headers[Constants.HttpHeaders.PopulateQuotaInfo] = opts.populateQuotaInfo; + } + + if (opts.populateQueryMetrics) { + headers[Constants.HttpHeaders.PopulateQueryMetrics] = opts.populateQueryMetrics; + } + + if (opts.maxDegreeOfParallelism !== undefined) { + headers[Constants.HttpHeaders.ParallelizeCrossPartitionQuery] = true; + } + + if (opts.populateQuotaInfo) { + headers[Constants.HttpHeaders.PopulateQuotaInfo] = true; + } + + if (opts.partitionKey !== undefined) { + let partitionKey: string[] | string = opts.partitionKey; + if (partitionKey === null || !Array.isArray(partitionKey)) { + partitionKey = [partitionKey as string]; + } + headers[Constants.HttpHeaders.PartitionKey] = Helper.jsonStringifyAndEscapeNonASCII(partitionKey); + } + + if (authOptions.masterKey || authOptions.key || authOptions.tokenProvider) { + headers[Constants.HttpHeaders.XDate] = new Date().toUTCString(); + } + + if (verb === "post" || verb === "put") { + if (!headers[Constants.HttpHeaders.ContentType]) { + headers[Constants.HttpHeaders.ContentType] = Constants.MediaTypes.Json; + } + } + + if (!headers[Constants.HttpHeaders.Accept]) { + headers[Constants.HttpHeaders.Accept] = Constants.MediaTypes.Json; + } + + if (partitionKeyRangeId !== undefined) { + headers[Constants.HttpHeaders.PartitionKeyRangeID] = partitionKeyRangeId; + } + + if (opts.enableScriptLogging) { + headers[Constants.HttpHeaders.EnableScriptLogging] = opts.enableScriptLogging; + } + + if (opts.offerEnableRUPerMinuteThroughput) { + headers[Constants.HttpHeaders.OfferIsRUPerMinuteThroughputEnabled] = true; + } + + if (opts.disableRUPerMinuteUsage) { + headers[Constants.HttpHeaders.DisableRUPerMinuteUsage] = true; + } + if ( + authOptions.masterKey || + authOptions.key || + authOptions.resourceTokens || + authOptions.tokenProvider || + authOptions.permissionFeed + ) { + const token = await AuthHandler.getAuthorizationHeader(authOptions, verb, path, resourceId, resourceType, headers); + headers[Constants.HttpHeaders.Authorization] = token; + } + return headers; +} diff --git a/sdk/cosmosdb/cosmos/src/retry/IRetryPolicy.ts b/sdk/cosmosdb/cosmos/src/retry/IRetryPolicy.ts new file mode 100644 index 000000000000..335f75e01657 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/IRetryPolicy.ts @@ -0,0 +1,11 @@ +import { ErrorResponse } from "../request"; +import { RetryContext } from "./RetryContext"; + +export interface IRetryPolicy { + retryAfterInMilliseconds: number; + shouldRetry: ( + errorResponse: ErrorResponse, + retryContext?: RetryContext, + locationEndpoint?: string + ) => Promise; +} diff --git a/sdk/cosmosdb/cosmos/src/retry/RetryContext.ts b/sdk/cosmosdb/cosmos/src/retry/RetryContext.ts new file mode 100644 index 000000000000..cbee76b1383b --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/RetryContext.ts @@ -0,0 +1,5 @@ +export interface RetryContext { + retryCount?: number; + retryRequestOnPreferredLocations?: boolean; + clearSessionTokenNotAvailable?: boolean; +} diff --git a/sdk/cosmosdb/cosmos/src/retry/defaultRetryPolicy.ts b/sdk/cosmosdb/cosmos/src/retry/defaultRetryPolicy.ts new file mode 100644 index 000000000000..63029bc98556 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/defaultRetryPolicy.ts @@ -0,0 +1,83 @@ +import { ErrorResponse } from "../request"; + +/** + * This class implements the default connection retry policy for requests. + * @property {int} currentRetryAttemptCount - Current retry attempt count. + * @hidden + */ +export class DefaultRetryPolicy { + private maxRetryAttemptCount: number = 10; + private currentRetryAttemptCount: number = 0; + public retryAfterInMilliseconds: number = 1000; + + // Windows Socket Error Codes + private WindowsInterruptedFunctionCall: number = 10004; + private WindowsFileHandleNotValid: number = 10009; + private WindowsPermissionDenied: number = 10013; + private WindowsBadAddress: number = 10014; + private WindowsInvalidArgumnet: number = 10022; + private WindowsResourceTemporarilyUnavailable: number = 10035; + private WindowsOperationNowInProgress: number = 10036; + private WindowsAddressAlreadyInUse: number = 10048; + private WindowsConnectionResetByPeer: number = 10054; + private WindowsCannotSendAfterSocketShutdown: number = 10058; + private WindowsConnectionTimedOut: number = 10060; + private WindowsConnectionRefused: number = 10061; + private WindowsNameTooLong: number = 10063; + private WindowsHostIsDown: number = 10064; + private WindowsNoRouteTohost: number = 10065; + + // Linux Error Codes + private LinuxConnectionReset = "ECONNRESET"; + + private CONNECTION_ERROR_CODES: any[] = [ + this.WindowsInterruptedFunctionCall, + this.WindowsFileHandleNotValid, + this.WindowsPermissionDenied, + this.WindowsBadAddress, + this.WindowsInvalidArgumnet, + this.WindowsResourceTemporarilyUnavailable, + this.WindowsOperationNowInProgress, + this.WindowsAddressAlreadyInUse, + this.WindowsConnectionResetByPeer, + this.WindowsCannotSendAfterSocketShutdown, + this.WindowsConnectionTimedOut, + this.WindowsConnectionRefused, + this.WindowsNameTooLong, + this.WindowsHostIsDown, + this.WindowsNoRouteTohost, + this.LinuxConnectionReset + ]; + + /** + * @constructor ResourceThrottleRetryPolicy + * @param {string} operationType - The type of operation being performed. + */ + constructor(private operationType: string) {} + /** + * Determines whether the request should be retried or not. + * @param {object} err - Error returned by the request. + * @param {function} callback - The callback function which takes bool argument which + * specifies whether the request will be retried or not. + */ + public async shouldRetry(err: ErrorResponse): Promise { + if (err) { + if (this.currentRetryAttemptCount < this.maxRetryAttemptCount && this.needs_retry(err.code)) { + this.currentRetryAttemptCount++; + return true; + } + } + return false; + } + + private needs_retry(code: number | string) { + if ( + (this.operationType === "read" || this.operationType === "query") && + this.CONNECTION_ERROR_CODES.indexOf(code) !== -1 + ) { + return true; + } else { + return false; + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/retry/endpointDiscoveryRetryPolicy.ts b/sdk/cosmosdb/cosmos/src/retry/endpointDiscoveryRetryPolicy.ts new file mode 100644 index 000000000000..cb18c0c4bab3 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/endpointDiscoveryRetryPolicy.ts @@ -0,0 +1,78 @@ +import { Helper } from "../common"; +import { GlobalEndpointManager } from "../globalEndpointManager"; +import { ErrorResponse } from "../request/request"; +import { RequestContext } from "../request/RequestContext"; +import { IRetryPolicy } from "./IRetryPolicy"; +import { RetryContext } from "./RetryContext"; + +/** + * This class implements the retry policy for endpoint discovery. + * @hidden + */ +export class EndpointDiscoveryRetryPolicy implements IRetryPolicy { + /** Current retry attempt count. */ + public currentRetryAttemptCount: number; + /** Retry interval in milliseconds. */ + public retryAfterInMilliseconds: number; + + /** Max number of retry attempts to perform. */ + private maxRetryAttemptCount: number; + private static readonly maxRetryAttemptCount = 120; // TODO: Constant? + private static readonly retryAfterInMilliseconds = 1000; + + /** + * @constructor EndpointDiscoveryRetryPolicy + * @param {object} globalEndpointManager The GlobalEndpointManager instance. + */ + constructor(private globalEndpointManager: GlobalEndpointManager, private request: RequestContext) { + this.maxRetryAttemptCount = EndpointDiscoveryRetryPolicy.maxRetryAttemptCount; + this.currentRetryAttemptCount = 0; + this.retryAfterInMilliseconds = EndpointDiscoveryRetryPolicy.retryAfterInMilliseconds; + } + + /** + * Determines whether the request should be retried or not. + * @param {object} err - Error returned by the request. + */ + public async shouldRetry( + err: ErrorResponse, + retryContext?: RetryContext, + locationEndpoint?: string + ): Promise { + if (!err) { + return false; + } + + if (!retryContext || !locationEndpoint) { + return false; + } + + if (!this.globalEndpointManager.enableEndpointDiscovery) { + return false; + } + + if (this.currentRetryAttemptCount >= this.maxRetryAttemptCount) { + return false; + } + + this.currentRetryAttemptCount++; + + if (Helper.isReadRequest(this.request)) { + this.globalEndpointManager.markCurrentLocationUnavailableForRead(locationEndpoint); + } else { + this.globalEndpointManager.markCurrentLocationUnavailableForWrite(locationEndpoint); + } + + // Check location index increment + // TODO: Tracing + // console.log("Write region was changed, refreshing the regions list from database account + // and will retry the request."); + await this.globalEndpointManager.refreshEndpointList(); + + retryContext.retryCount = this.currentRetryAttemptCount; + retryContext.clearSessionTokenNotAvailable = false; + retryContext.retryRequestOnPreferredLocations = false; + + return true; + } +} diff --git a/sdk/cosmosdb/cosmos/src/retry/index.ts b/sdk/cosmosdb/cosmos/src/retry/index.ts new file mode 100644 index 000000000000..a68e2696f7b4 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/index.ts @@ -0,0 +1,5 @@ +export * from "./retryOptions"; +export * from "./endpointDiscoveryRetryPolicy"; +export * from "./resourceThrottleRetryPolicy"; +export * from "./sessionRetryPolicy"; +export * from "./retryUtility"; diff --git a/sdk/cosmosdb/cosmos/src/retry/resourceThrottleRetryPolicy.ts b/sdk/cosmosdb/cosmos/src/retry/resourceThrottleRetryPolicy.ts new file mode 100644 index 000000000000..2b539dd56442 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/resourceThrottleRetryPolicy.ts @@ -0,0 +1,60 @@ +import { StatusCodes } from "../common"; +import { ErrorResponse } from "../request/request"; + +/** + * This class implements the resource throttle retry policy for requests. + * @hidden + */ +export class ResourceThrottleRetryPolicy { + /** Current retry attempt count. */ + public currentRetryAttemptCount: number = 0; + /** Cummulative wait time in milliseconds for a request while the retries are happening. */ + public cummulativeWaitTimeinMilliseconds: number = 0; + /** Max wait time in milliseconds to wait for a request while the retries are happening. */ + public retryAfterInMilliseconds: number = 0; + + /** Max number of retries to be performed for a request. */ + private maxWaitTimeInMilliseconds: number; + /** + * @constructor ResourceThrottleRetryPolicy + * @param {int} maxRetryAttemptCount - Max number of retries to be performed for a request. + * @param {int} fixedRetryIntervalInMilliseconds - Fixed retry interval in milliseconds to wait between each \ + * retry ignoring the retryAfter returned as part of the response. + * @param {int} maxWaitTimeInSeconds - Max wait time in seconds to wait for a request while the \ + * retries are happening. + */ + constructor( + private maxRetryAttemptCount: number, + private fixedRetryIntervalInMilliseconds: number, + maxWaitTimeInSeconds: number + ) { + this.maxWaitTimeInMilliseconds = maxWaitTimeInSeconds * 1000; + this.currentRetryAttemptCount = 0; + this.cummulativeWaitTimeinMilliseconds = 0; + } + /** + * Determines whether the request should be retried or not. + * @param {object} err - Error returned by the request. + */ + public async shouldRetry(err: ErrorResponse): Promise { + // TODO: any custom error object + if (err) { + if (this.currentRetryAttemptCount < this.maxRetryAttemptCount) { + this.currentRetryAttemptCount++; + this.retryAfterInMilliseconds = 0; + + if (this.fixedRetryIntervalInMilliseconds) { + this.retryAfterInMilliseconds = this.fixedRetryIntervalInMilliseconds; + } else if (err.retryAfterInMilliseconds) { + this.retryAfterInMilliseconds = err.retryAfterInMilliseconds; + } + + if (this.cummulativeWaitTimeinMilliseconds < this.maxWaitTimeInMilliseconds) { + this.cummulativeWaitTimeinMilliseconds += this.retryAfterInMilliseconds; + return true; + } + } + } + return false; + } +} diff --git a/sdk/cosmosdb/cosmos/src/retry/retryOptions.ts b/sdk/cosmosdb/cosmos/src/retry/retryOptions.ts new file mode 100644 index 000000000000..605fa54d19b5 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/retryOptions.ts @@ -0,0 +1,13 @@ +/** + * Represents the Retry policy assocated with throttled requests in the Azure Cosmos DB database service. + */ +export class RetryOptions { + constructor( + /** Max number of retries to be performed for a request. Default value 9. */ + public readonly MaxRetryAttemptCount: number = 9, + /** Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. */ + public readonly FixedRetryIntervalInMilliseconds: number = 0, + /** Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds. */ + public readonly MaxWaitTimeInSeconds: number = 30 + ) {} +} diff --git a/sdk/cosmosdb/cosmos/src/retry/retryUtility.ts b/sdk/cosmosdb/cosmos/src/retry/retryUtility.ts new file mode 100644 index 000000000000..a6dee96c6797 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/retryUtility.ts @@ -0,0 +1,171 @@ +import { RequestOptions } from "https"; +import * as url from "url"; +import { EndpointDiscoveryRetryPolicy, ResourceThrottleRetryPolicy, SessionRetryPolicy } from "."; +import { Constants, Helper, StatusCodes, SubStatusCodes } from "../common"; +import { ConnectionPolicy } from "../documents"; +import { GlobalEndpointManager } from "../globalEndpointManager"; +import { Response } from "../request"; +import { LocationRouting } from "../request/LocationRouting"; +import { RequestContext } from "../request/RequestContext"; +import { DefaultRetryPolicy } from "./defaultRetryPolicy"; +import { IRetryPolicy } from "./IRetryPolicy"; +import { RetryContext } from "./RetryContext"; + +/** @hidden */ +export type CreateRequestObjectStubFunction = ( + connectionPolicy: ConnectionPolicy, + requestOptions: RequestOptions, + body: Buffer +) => Promise>; // TODO: any response + +/** @hidden */ +export class RetryUtility { + /** + * Executes the retry policy for the created request object. + * @param {object} globalEndpointManager - an instance of GlobalEndpointManager class. + * @param {object} body - request body. A buffer or a string. + * @param {function} createRequestObjectStub - stub function that creates the request object. + * @param {object} connectionPolicy - an instance of ConnectionPolicy that has the connection configs. + * @param {RequestOptions} requestOptions - The request options. + * @param {function} callback - the callback that will be called when the request is finished executing. + */ + public static async execute( + globalEndpointManager: GlobalEndpointManager, + body: Buffer, + createRequestObjectFunc: CreateRequestObjectStubFunction, + connectionPolicy: ConnectionPolicy, + requestOptions: RequestOptions, + request: RequestContext + ): Promise> { + // TODO: any request + const r: RequestContext = typeof request !== "string" ? request : { path: "", operationType: "nonReadOps" }; + + const endpointDiscoveryRetryPolicy = new EndpointDiscoveryRetryPolicy(globalEndpointManager, r); + const resourceThrottleRetryPolicy = new ResourceThrottleRetryPolicy( + connectionPolicy.RetryOptions.MaxRetryAttemptCount, + connectionPolicy.RetryOptions.FixedRetryIntervalInMilliseconds, + connectionPolicy.RetryOptions.MaxWaitTimeInSeconds + ); + const sessionReadRetryPolicy = new SessionRetryPolicy(globalEndpointManager, r, connectionPolicy); + const defaultRetryPolicy = new DefaultRetryPolicy(request.operationType); + + return this.apply( + body, + createRequestObjectFunc, + connectionPolicy, + requestOptions, + endpointDiscoveryRetryPolicy, + resourceThrottleRetryPolicy, + sessionReadRetryPolicy, + defaultRetryPolicy, + globalEndpointManager, + request, + {} + ); + } + + /** + * Applies the retry policy for the created request object. + * @param {object} body - request body. A buffer or a string. + * @param {function} createRequestObjectFunc - function that creates the request object. + * @param {object} connectionPolicy - an instance of ConnectionPolicy that has the connection configs. + * @param {RequestOptions} requestOptions - The request options. + * @param {EndpointDiscoveryRetryPolicy} endpointDiscoveryRetryPolicy - The endpoint discovery retry policy \ + * instance. + * @param {ResourceThrottleRetryPolicy} resourceThrottleRetryPolicy - The resource throttle retry policy instance. + * @param {function} callback - the callback that will be called when the response is retrieved and processed. + */ + public static async apply( + body: Buffer, + createRequestObjectFunc: CreateRequestObjectStubFunction, + connectionPolicy: ConnectionPolicy, + requestOptions: RequestOptions, + endpointDiscoveryRetryPolicy: EndpointDiscoveryRetryPolicy, + resourceThrottleRetryPolicy: ResourceThrottleRetryPolicy, + sessionReadRetryPolicy: SessionRetryPolicy, + defaultRetryPolicy: DefaultRetryPolicy, + globalEndpointManager: GlobalEndpointManager, + request: RequestContext, + retryContext: RetryContext + ): Promise> { + // TODO: any response + const httpsRequest = createRequestObjectFunc(connectionPolicy, requestOptions, body); + if (!request.locationRouting) { + request.locationRouting = new LocationRouting(); + } + request.locationRouting.clearRouteToLocation(); + if (retryContext) { + request.locationRouting.routeToLocation( + retryContext.retryCount || 0, + !retryContext.retryRequestOnPreferredLocations + ); + if (retryContext.clearSessionTokenNotAvailable) { + request.client.clearSessionToken(request.path); + } + } + const locationEndpoint = await globalEndpointManager.resolveServiceEndpoint(request); + requestOptions = this.modifyRequestOptions(requestOptions, url.parse(locationEndpoint)); + request.locationRouting.routeToLocation(locationEndpoint); + try { + const response = await (httpsRequest as Promise>); + response.headers[Constants.ThrottleRetryCount] = resourceThrottleRetryPolicy.currentRetryAttemptCount; + response.headers[Constants.ThrottleRetryWaitTimeInMs] = + resourceThrottleRetryPolicy.cummulativeWaitTimeinMilliseconds; + return response; + } catch (err) { + // TODO: any error + let retryPolicy: IRetryPolicy = null; + const headers = err.headers || {}; + if (err.code === StatusCodes.Forbidden && err.substatus === SubStatusCodes.WriteForbidden) { + retryPolicy = endpointDiscoveryRetryPolicy; + } else if (err.code === StatusCodes.TooManyRequests) { + retryPolicy = resourceThrottleRetryPolicy; + } else if (err.code === StatusCodes.NotFound && err.substatus === SubStatusCodes.ReadSessionNotAvailable) { + retryPolicy = sessionReadRetryPolicy; + } else { + retryPolicy = defaultRetryPolicy; + } + const results = await retryPolicy.shouldRetry(err, retryContext, locationEndpoint); + if (!results) { + headers[Constants.ThrottleRetryCount] = resourceThrottleRetryPolicy.currentRetryAttemptCount; + headers[Constants.ThrottleRetryWaitTimeInMs] = resourceThrottleRetryPolicy.cummulativeWaitTimeinMilliseconds; + err.headers = { ...err.headers, ...headers }; + throw err; + } else { + request.retryCount++; + const newUrl = (results as any)[1]; // TODO: any hack + if (newUrl !== undefined) { + RetryUtility.modifyRequestOptions(requestOptions, url.parse(newUrl)); + } + await Helper.sleep(retryPolicy.retryAfterInMilliseconds); + return this.apply( + body, + createRequestObjectFunc, + connectionPolicy, + requestOptions, + endpointDiscoveryRetryPolicy, + resourceThrottleRetryPolicy, + sessionReadRetryPolicy, + defaultRetryPolicy, + globalEndpointManager, + request, + retryContext + ); + } + } + } + + private static modifyRequestOptions( + oldRequestOptions: RequestOptions | any, // TODO: any hack is bad + newUrl: url.UrlWithStringQuery | any + ) { + // TODO: any hack is bad + const properties = Object.keys(newUrl); + for (const index in properties) { + if (properties[index] !== "path") { + oldRequestOptions[properties[index]] = newUrl[properties[index]]; + } + } + return oldRequestOptions; + } +} diff --git a/sdk/cosmosdb/cosmos/src/retry/sessionRetryPolicy.ts b/sdk/cosmosdb/cosmos/src/retry/sessionRetryPolicy.ts new file mode 100644 index 000000000000..ee1f62da4a16 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/retry/sessionRetryPolicy.ts @@ -0,0 +1,73 @@ +import { Helper } from "../common"; +import { ConnectionPolicy } from "../documents"; +import { GlobalEndpointManager } from "../globalEndpointManager"; +import { ErrorResponse } from "../request/request"; +import { RequestContext } from "../request/RequestContext"; +import { IRetryPolicy } from "./IRetryPolicy"; +import { RetryContext } from "./RetryContext"; + +/** + * This class implements the retry policy for session consistent reads. + * @hidden + */ +export class SessionRetryPolicy implements IRetryPolicy { + /** Current retry attempt count. */ + public currentRetryAttemptCount = 0; + /** Retry interval in milliseconds. */ + public retryAfterInMilliseconds = 0; + + /** + * @constructor SessionReadRetryPolicy + * @param {object} globalEndpointManager - The GlobalEndpointManager instance. + * @property {object} request - The Http request information + */ + constructor( + private globalEndpointManager: GlobalEndpointManager, + private request: RequestContext, + private connectionPolicy: ConnectionPolicy + ) {} + + /** + * Determines whether the request should be retried or not. + * @param {object} err - Error returned by the request. + * @param {function} callback - The callback function which takes bool argument which specifies whether the request\ + * will be retried or not. + */ + public async shouldRetry(err: ErrorResponse, retryContext?: RetryContext): Promise { + if (!err) { + return false; + } + + if (!retryContext) { + return false; + } + + if (!this.connectionPolicy.EnableEndpointDiscovery) { + return false; + } + + if (this.globalEndpointManager.canUseMultipleWriteLocations(this.request)) { + // If we can write to multiple locations, we should against every write endpoint until we succeed + const endpoints = Helper.isReadRequest(this.request) + ? await this.globalEndpointManager.getReadEndpoints() + : await this.globalEndpointManager.getWriteEndpoints(); + if (this.currentRetryAttemptCount > endpoints.length) { + return false; + } else { + retryContext.retryCount = ++this.currentRetryAttemptCount - 1; + retryContext.retryRequestOnPreferredLocations = this.currentRetryAttemptCount > 1; + retryContext.clearSessionTokenNotAvailable = this.currentRetryAttemptCount === endpoints.length; + return true; + } + } else { + if (this.currentRetryAttemptCount > 1) { + return false; + } else { + retryContext.retryCount = ++this.currentRetryAttemptCount - 1; + retryContext.retryRequestOnPreferredLocations = false; // Forces all operations to primary write endpoint + retryContext.clearSessionTokenNotAvailable = true; + return true; + } + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/routing/CollectionRoutingMapFactory.ts b/sdk/cosmosdb/cosmos/src/routing/CollectionRoutingMapFactory.ts new file mode 100644 index 000000000000..62438f1d6cae --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/routing/CollectionRoutingMapFactory.ts @@ -0,0 +1,82 @@ +import { InMemoryCollectionRoutingMap } from "."; +import { Constants } from "../common"; + +function compareRanges(a: any, b: any) { + const aVal = a[0][Constants.PartitionKeyRange.MinInclusive]; + const bVal = b[0][Constants.PartitionKeyRange.MinInclusive]; + if (aVal > bVal) { + return 1; + } + if (aVal < bVal) { + return -1; + } + return 0; +} + +/** @hidden */ +export class CollectionRoutingMapFactory { + public static createCompleteRoutingMap(partitionKeyRangeInfoTuppleList: any[], collectionUniqueId: string) { + const rangeById: any = {}; // TODO: any + const rangeByInfo: any = {}; // TODO: any + + let sortedRanges = []; + + // the for loop doesn't invoke any async callback + for (const r of partitionKeyRangeInfoTuppleList) { + rangeById[r[0][Constants.PartitionKeyRange.Id]] = r; + rangeByInfo[r[1]] = r[0]; + sortedRanges.push(r); + } + + sortedRanges = sortedRanges.sort(compareRanges); + const partitionKeyOrderedRange = sortedRanges.map(r => r[0]); + const orderedPartitionInfo = sortedRanges.map(r => r[1]); + + if (!this._isCompleteSetOfRange(partitionKeyOrderedRange)) { + return undefined; + } + return new InMemoryCollectionRoutingMap( + rangeById, + rangeByInfo, + partitionKeyOrderedRange, + orderedPartitionInfo, + collectionUniqueId + ); + } + + private static _isCompleteSetOfRange(partitionKeyOrderedRange: any) { + // TODO: any + let isComplete = false; + if (partitionKeyOrderedRange.length > 0) { + const firstRange = partitionKeyOrderedRange[0]; + const lastRange = partitionKeyOrderedRange[partitionKeyOrderedRange.length - 1]; + isComplete = + firstRange[Constants.PartitionKeyRange.MinInclusive] === + Constants.EffectiveParitionKeyConstants.MinimumInclusiveEffectivePartitionKey; + isComplete = + isComplete && + lastRange[Constants.PartitionKeyRange.MaxExclusive] === + Constants.EffectiveParitionKeyConstants.MaximumExclusiveEffectivePartitionKey; + + for (let i = 1; i < partitionKeyOrderedRange.length; i++) { + const previousRange = partitionKeyOrderedRange[i - 1]; + const currentRange = partitionKeyOrderedRange[i]; + isComplete = + isComplete && + previousRange[Constants.PartitionKeyRange.MaxExclusive] === + currentRange[Constants.PartitionKeyRange.MinInclusive]; + + if (!isComplete) { + if ( + previousRange[Constants.PartitionKeyRange.MaxExclusive] > + currentRange[Constants.PartitionKeyRange.MinInclusive] + ) { + throw Error("Ranges overlap"); + } + break; + } + } + } + return isComplete; + } +} diff --git a/sdk/cosmosdb/cosmos/src/routing/QueryRange.ts b/sdk/cosmosdb/cosmos/src/routing/QueryRange.ts new file mode 100644 index 000000000000..7684cb294102 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/routing/QueryRange.ts @@ -0,0 +1,79 @@ +import { Constants } from "../common"; + +/** @hidden */ +export class QueryRange { + public min: string; + public max: string; + public isMinInclusive: boolean; + public isMaxInclusive: boolean; + + /** + * Represents a QueryRange. + * @constructor QueryRange + * @param {string} rangeMin - min + * @param {string} rangeMin - max + * @param {boolean} isMinInclusive - isMinInclusive + * @param {boolean} isMaxInclusive - isMaxInclusive + * @ignore + */ + constructor(rangeMin: string, rangeMax: string, isMinInclusive: boolean, isMaxInclusive: boolean) { + this.min = rangeMin; + this.max = rangeMax; + this.isMinInclusive = isMinInclusive; + this.isMaxInclusive = isMaxInclusive; + } + public overlaps(other: QueryRange) { + // tslint:disable-next-line:no-this-assignment + const range1 = this; + const range2 = other; + if (range1 === undefined || range2 === undefined) { + return false; + } + if (range1.isEmpty() || range2.isEmpty()) { + return false; + } + + if (range1.min <= range2.max || range2.min <= range1.max) { + if ( + (range1.min === range2.max && !(range1.isMinInclusive && range2.isMaxInclusive)) || + (range2.min === range1.max && !(range2.isMinInclusive && range1.isMaxInclusive)) + ) { + return false; + } + return true; + } + return false; + } + + public isEmpty() { + return !(this.isMinInclusive && this.isMaxInclusive) && this.min === this.max; + } + /** + * Parse a QueryRange from a partitionKeyRange + * @returns QueryRange + * @ignore + */ + public static parsePartitionKeyRange(partitionKeyRange: any) { + // TODO: paritionkeyrange + return new QueryRange( + partitionKeyRange[Constants.PartitionKeyRange.MinInclusive], + partitionKeyRange[Constants.PartitionKeyRange.MaxExclusive], + true, + false + ); + } + /** + * Parse a QueryRange from a dictionary + * @returns QueryRange + * @ignore + */ + public static parseFromDict(queryRangeDict: any) { + // TODO: queryRangeDictionary + return new QueryRange( + queryRangeDict.min, + queryRangeDict.max, + queryRangeDict.isMinInclusive, + queryRangeDict.isMaxInclusive + ); + } +} diff --git a/sdk/cosmosdb/cosmos/src/routing/inMemoryCollectionRoutingMap.ts b/sdk/cosmosdb/cosmos/src/routing/inMemoryCollectionRoutingMap.ts new file mode 100644 index 000000000000..89f24b57e19a --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/routing/inMemoryCollectionRoutingMap.ts @@ -0,0 +1,139 @@ +import * as bs from "binary-search-bounds"; // TODO: missing types +import { Constants } from "../common"; +import { Range } from "../range"; +import { QueryRange } from "./QueryRange"; + +/** @hidden */ +export class InMemoryCollectionRoutingMap { + private rangeById: Range[]; + private rangeByInfo: string; + private orderedPartitionKeyRanges: any[]; + private orderedRanges: QueryRange[]; + // TODO: chrande made this public, even though it is implementation detail for a test + public orderedPartitionInfo: any; + private collectionUniqueId: any; + + /** + * Represents a InMemoryCollectionRoutingMap Object, + * Stores partition key ranges in an efficient way with some additional information and provides + * convenience methods for working with set of ranges. + */ + constructor( + rangeById: Range[], + rangeByInfo: string, + orderedPartitionKeyRanges: any[], + orderedPartitionInfo: any, + collectionUniqueId: string + ) { + this.rangeById = rangeById; + this.rangeByInfo = rangeByInfo; + this.orderedPartitionKeyRanges = orderedPartitionKeyRanges; + this.orderedRanges = orderedPartitionKeyRanges.map(pkr => { + return new QueryRange( + pkr[Constants.PartitionKeyRange.MinInclusive], + pkr[Constants.PartitionKeyRange.MaxExclusive], + true, + false + ); + }); + this.orderedPartitionInfo = orderedPartitionInfo; + this.collectionUniqueId = collectionUniqueId; + } + public getOrderedParitionKeyRanges() { + return this.orderedPartitionKeyRanges; + } + + public getRangeByEffectivePartitionKey(effectivePartitionKeyValue: string) { + if (Constants.EffectiveParitionKeyConstants.MinimumInclusiveEffectivePartitionKey === effectivePartitionKeyValue) { + return this.orderedPartitionKeyRanges[0]; + } + + if (Constants.EffectiveParitionKeyConstants.MaximumExclusiveEffectivePartitionKey === effectivePartitionKeyValue) { + return undefined; + } + + const sortedLow = this.orderedRanges.map(r => { + return { v: r.min, b: !r.isMinInclusive }; + }); + + const index = bs.le( + sortedLow, + { v: effectivePartitionKeyValue, b: true }, + InMemoryCollectionRoutingMap._vbCompareFunction + ); + // that's an error + if (index < 0) { + throw new Error("error in collection routing map, queried partition key is less than the start range."); + } + + return this.orderedPartitionKeyRanges[index]; + } + + private static _vbCompareFunction(x: any, y: any) { + // TODO: What is x & y? A bs type? + if (x.v > y.v) { + return 1; + } + if (x.v < y.v) { + return -1; + } + if (x.b > y.b) { + return 1; + } + if (x.b < y.b) { + return -1; + } + return 0; + } + + public getOverlappingRanges(providedQueryRanges: QueryRange | QueryRange[]) { + const pqr: QueryRange[] = Array.isArray(providedQueryRanges) ? providedQueryRanges : [providedQueryRanges]; + const minToPartitionRange: any = {}; // TODO: any + const sortedLow = this.orderedRanges.map(r => { + return { v: r.min, b: !r.isMinInclusive }; + }); + const sortedHigh = this.orderedRanges.map(r => { + return { v: r.max, b: r.isMaxInclusive }; + }); + + // this for loop doesn't invoke any async callback + for (const queryRange of pqr) { + if (queryRange.isEmpty()) { + continue; + } + const minIndex = bs.le( + sortedLow, + { v: queryRange.min, b: !queryRange.isMinInclusive }, + InMemoryCollectionRoutingMap._vbCompareFunction + ); + + if (minIndex < 0) { + throw new Error("error in collection routing map, queried value is less than the start range."); + } + + const maxIndex = bs.ge( + sortedHigh, + { v: queryRange.max, b: queryRange.isMaxInclusive }, + InMemoryCollectionRoutingMap._vbCompareFunction + ); + if (maxIndex > sortedHigh.length) { + throw new Error("error in collection routing map, queried value is greater than the end range."); + } + + // the for loop doesn't invoke any async callback + for (let j = minIndex; j < maxIndex + 1; j++) { + if (queryRange.overlaps(this.orderedRanges[j])) { + minToPartitionRange[ + this.orderedPartitionKeyRanges[j][Constants.PartitionKeyRange.MinInclusive] + ] = this.orderedPartitionKeyRanges[j]; + } + } + } + + const overlappingPartitionKeyRanges = Object.keys(minToPartitionRange).map(k => minToPartitionRange[k]); + + return overlappingPartitionKeyRanges.sort(r => { + return r[Constants.PartitionKeyRange.MinInclusive]; + }); + } +} diff --git a/sdk/cosmosdb/cosmos/src/routing/index.ts b/sdk/cosmosdb/cosmos/src/routing/index.ts new file mode 100644 index 000000000000..66e33c4dd252 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/routing/index.ts @@ -0,0 +1,5 @@ +export * from "./QueryRange"; +export * from "./CollectionRoutingMapFactory"; +export * from "./inMemoryCollectionRoutingMap"; +export * from "./partitionKeyRangeCache"; +export * from "./smartRoutingMapProvider"; diff --git a/sdk/cosmosdb/cosmos/src/routing/partitionKeyRangeCache.ts b/sdk/cosmosdb/cosmos/src/routing/partitionKeyRangeCache.ts new file mode 100644 index 000000000000..ddd99065d834 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/routing/partitionKeyRangeCache.ts @@ -0,0 +1,75 @@ +import semaphore from "semaphore"; +import { CollectionRoutingMapFactory, InMemoryCollectionRoutingMap, QueryRange } from "."; +import { ClientContext } from "../ClientContext"; +import { Helper } from "../common"; + +/** @hidden */ +export class PartitionKeyRangeCache { + private collectionRoutingMapByCollectionId: { + [key: string]: InMemoryCollectionRoutingMap; + }; + private sem: semaphore.Semaphore; + + constructor(private clientContext: ClientContext) { + this.collectionRoutingMapByCollectionId = {}; + this.sem = semaphore(1); + } + /** + * Finds or Instantiates the requested Collection Routing Map and invokes callback + * @param {callback} callback - Function to execute for the collection routing map. + * the function takes two parameters error, collectionRoutingMap. + * @param {string} collectionLink - Requested collectionLink + * @ignore + */ + public async onCollectionRoutingMap(collectionLink: string): Promise { + const collectionId = Helper.getIdFromLink(collectionLink); + + let collectionRoutingMap = this.collectionRoutingMapByCollectionId[collectionId]; + if (collectionRoutingMap === undefined) { + // attempt to consturct collection routing map + collectionRoutingMap = await new Promise((resolve, reject) => { + const semaphorizedFuncCollectionMapInstantiator = async () => { + let crm: InMemoryCollectionRoutingMap = this.collectionRoutingMapByCollectionId[collectionId]; + if (crm === undefined) { + try { + const { result: resources } = await this.clientContext.queryPartitionKeyRanges(collectionLink).toArray(); + + crm = CollectionRoutingMapFactory.createCompleteRoutingMap(resources.map(r => [r, true]), collectionId); + + this.collectionRoutingMapByCollectionId[collectionId] = crm; + this.sem.leave(); + resolve(crm); + } catch (err) { + this.sem.leave(); + reject(err); + } + } else { + // sanity gaurd + this.sem.leave(); + // TODO: it looks like this code should never be reached... + // return resolve(collectionRoutingMap.getOverlappingRanges(partitionKeyRanges)); + reject(new Error("Not yet implemented")); + } + }; + + // We want only one attempt to construct collectionRoutingMap + // so we pass the consturction in the semaphore take + this.sem.take(semaphorizedFuncCollectionMapInstantiator); + }); + } + return collectionRoutingMap; + } + + /** + * Given the query ranges and a collection, invokes the callback on the list of overlapping partition key ranges + * @param {callback} callback - Function execute on the overlapping partition key ranges result, + * takes two parameters error, partition key ranges + * @param collectionLink + * @param queryRanges + * @ignore + */ + public async getOverlappingRanges(collectionLink: string, queryRanges: QueryRange) { + const crm = await this.onCollectionRoutingMap(collectionLink); + return crm.getOverlappingRanges(queryRanges); + } +} diff --git a/sdk/cosmosdb/cosmos/src/routing/smartRoutingMapProvider.ts b/sdk/cosmosdb/cosmos/src/routing/smartRoutingMapProvider.ts new file mode 100644 index 000000000000..9785f8212488 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/routing/smartRoutingMapProvider.ts @@ -0,0 +1,138 @@ +import { PartitionKeyRangeCache, QueryRange } from "."; +import { ClientContext } from "../ClientContext"; +import { Constants } from "../common"; + +/** @hidden */ +export const PARITIONKEYRANGE = Constants.PartitionKeyRange; + +/** @hidden */ +export class SmartRoutingMapProvider { + private partitionKeyRangeCache: PartitionKeyRangeCache; + + constructor(clientContext: ClientContext) { + this.partitionKeyRangeCache = new PartitionKeyRangeCache(clientContext); + } + private static _secondRangeIsAfterFirstRange(range1: QueryRange, range2: QueryRange) { + if (typeof range1.max === "undefined") { + throw new Error("range1 must have max"); + } + + if (typeof range2.min === "undefined") { + throw new Error("range2 must have min"); + } + + if (range1.max > range2.min) { + // r.min < #previous_r.max + return false; + } else { + if (range1.max === range2.min && range1.isMaxInclusive && range2.isMinInclusive) { + // the inclusive ending endpoint of previous_r is the same as the inclusive beginning endpoint of r + // they share a point + return false; + } + return true; + } + } + + private static _isSortedAndNonOverlapping(ranges: QueryRange[]) { + for (let idx = 1; idx < ranges.length; idx++) { + const previousR = ranges[idx - 1]; + const r = ranges[idx]; + if (!this._secondRangeIsAfterFirstRange(previousR, r)) { + return false; + } + } + return true; + } + + private static _stringMax(a: string, b: string) { + return a >= b ? a : b; + } + + private static _stringCompare(a: string, b: string) { + return a === b ? 0 : a > b ? 1 : -1; + } + + private static _subtractRange(r: QueryRange, partitionKeyRange: any) { + const left = this._stringMax(partitionKeyRange[PARITIONKEYRANGE.MaxExclusive], r.min); + const leftInclusive = this._stringCompare(left, r.min) === 0 ? r.isMinInclusive : false; + return new QueryRange(left, r.max, leftInclusive, r.isMaxInclusive); + } + + /** + * Given the sorted ranges and a collection, invokes the callback on the list of overlapping partition key ranges + * @param {callback} callback - Function execute on the overlapping partition key ranges result, + * takes two parameters error, partition key ranges + * @param collectionLink + * @param sortedRanges + * @ignore + */ + public async getOverlappingRanges(collectionLink: string, sortedRanges: QueryRange[]): Promise { + // validate if the list is non- overlapping and sorted TODO: any PartitionKeyRanges + if (!SmartRoutingMapProvider._isSortedAndNonOverlapping(sortedRanges)) { + throw new Error("the list of ranges is not a non-overlapping sorted ranges"); + } + + let partitionKeyRanges: any[] = []; // TODO: any ParitionKeyRanges + + if (sortedRanges.length === 0) { + return partitionKeyRanges; + } + + const collectionRoutingMap = await this.partitionKeyRangeCache.onCollectionRoutingMap(collectionLink); + + let index = 0; + let currentProvidedRange = sortedRanges[index]; + while (true) { + if (currentProvidedRange.isEmpty()) { + // skip and go to the next item + if (++index >= sortedRanges.length) { + return partitionKeyRanges; + } + currentProvidedRange = sortedRanges[index]; + continue; + } + + let queryRange; + if (partitionKeyRanges.length > 0) { + queryRange = SmartRoutingMapProvider._subtractRange( + currentProvidedRange, + partitionKeyRanges[partitionKeyRanges.length - 1] + ); + } else { + queryRange = currentProvidedRange; + } + + const overlappingRanges = collectionRoutingMap.getOverlappingRanges(queryRange); + if (overlappingRanges.length <= 0) { + throw new Error(`error: returned overlapping ranges for queryRange ${queryRange} is empty`); + } + partitionKeyRanges = partitionKeyRanges.concat(overlappingRanges); + + const lastKnownTargetRange = QueryRange.parsePartitionKeyRange(partitionKeyRanges[partitionKeyRanges.length - 1]); + if (!lastKnownTargetRange) { + throw new Error("expected lastKnowTargetRange to be truthy"); + } + // the overlapping ranges must contain the requested range + + if (SmartRoutingMapProvider._stringCompare(currentProvidedRange.max, lastKnownTargetRange.max) > 0) { + throw new Error(`error: returned overlapping ranges ${overlappingRanges} \ + does not contain the requested range ${queryRange}`); + } + + // the current range is contained in partitionKeyRanges just move forward + if (++index >= sortedRanges.length) { + return partitionKeyRanges; + } + currentProvidedRange = sortedRanges[index]; + + while (SmartRoutingMapProvider._stringCompare(currentProvidedRange.max, lastKnownTargetRange.max) <= 0) { + // the current range is covered too.just move forward + if (++index >= sortedRanges.length) { + return partitionKeyRanges; + } + currentProvidedRange = sortedRanges[index]; + } + } + } +} diff --git a/sdk/cosmosdb/cosmos/src/session/SessionContext.ts b/sdk/cosmosdb/cosmos/src/session/SessionContext.ts new file mode 100644 index 000000000000..aa660ef66ce9 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/session/SessionContext.ts @@ -0,0 +1,7 @@ +export interface SessionContext { + resourceId?: string; + resourceAddress?: string; + resourceType?: string; // TODO: enum + isNameBased?: boolean; + operationType?: string; // TODO: enum +} diff --git a/sdk/cosmosdb/cosmos/src/session/VectorSessionToken.ts b/sdk/cosmosdb/cosmos/src/session/VectorSessionToken.ts new file mode 100644 index 000000000000..fcdbc51ff9f1 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/session/VectorSessionToken.ts @@ -0,0 +1,152 @@ +/** + * Models vector clock bases session token. Session token has the following format: + * {Version}#{GlobalLSN}#{RegionId1}={LocalLsn1}#{RegionId2}={LocalLsn2}....#{RegionIdN}={LocalLsnN} + * 'Version' captures the configuration number of the partition which returned this session token. + * 'Version' is incremented everytime topology of the partition is updated (say due to Add/Remove/Failover). + * + * The choice of separators '#' and '=' is important. Separators ';' and ',' are used to delimit + * per-partitionKeyRange session token + * @hidden + * @private + * + */ +export class VectorSessionToken { + private static readonly SEGMENT_SEPARATOR = "#"; + private static readonly REGION_PROGRESS_SEPARATOR = "="; + + constructor( + private readonly version: number, + private readonly globalLsn: number, + private readonly localLsnByregion: Map, + private readonly sessionToken?: string + ) { + if (!this.sessionToken) { + const regionAndLocalLsn = []; + for (const [key, value] of this.localLsnByregion.entries()) { + regionAndLocalLsn.push(`${key}${VectorSessionToken.REGION_PROGRESS_SEPARATOR}${value}`); + } + const regionProgress = regionAndLocalLsn.join(VectorSessionToken.SEGMENT_SEPARATOR); + if (regionProgress === "") { + this.sessionToken = `${this.version}${VectorSessionToken.SEGMENT_SEPARATOR}${this.globalLsn}`; + } else { + this.sessionToken = `${this.version}${VectorSessionToken.SEGMENT_SEPARATOR}${this.globalLsn}${ + VectorSessionToken.SEGMENT_SEPARATOR + }${regionProgress}`; + } + } + } + + public static create(sessionToken: string): VectorSessionToken { + if (!sessionToken) { + return null; + } + + const [versionStr, globalLsnStr, ...regionSegments] = sessionToken.split(VectorSessionToken.SEGMENT_SEPARATOR); + + const version = parseInt(versionStr, 10); + const globalLsn = parseFloat(globalLsnStr); + + if (typeof version !== "number" || typeof globalLsn !== "number") { + return null; + } + + const lsnByRegion = new Map(); + for (const regionSegment of regionSegments) { + const [regionIdStr, localLsnStr] = regionSegment.split(VectorSessionToken.REGION_PROGRESS_SEPARATOR); + + if (!regionIdStr || !localLsnStr) { + return null; + } + + const regionId = parseInt(regionIdStr, 10); + let localLsn: string; + try { + localLsn = localLsnStr; + } catch (err) { + // TODO: log error + return null; + } + if (typeof regionId !== "number") { + return null; + } + + lsnByRegion.set(regionId, localLsn); + } + + return new VectorSessionToken(version, globalLsn, lsnByRegion, sessionToken); + } + + public equals(other: VectorSessionToken): boolean { + return !other + ? false + : this.version === other.version && + this.globalLsn === other.globalLsn && + this.areRegionProgressEqual(other.localLsnByregion); + } + + public merge(other: VectorSessionToken): VectorSessionToken { + if (other == null) { + throw new Error("other (Vector Session Token) must not be null"); + } + + if (this.version === other.version && this.localLsnByregion.size !== other.localLsnByregion.size) { + throw new Error(`Compared session tokens ${this.sessionToken} and ${other.sessionToken} have unexpected regions`); + } + + const [higherVersionSessionToken, lowerVersionSessionToken]: [VectorSessionToken, VectorSessionToken] = + this.version < other.version ? [other, this] : [this, other]; + + const highestLocalLsnByRegion = new Map(); + + for (const [regionId, highLocalLsn] of higherVersionSessionToken.localLsnByregion.entries()) { + const lowLocalLsn = lowerVersionSessionToken.localLsnByregion.get(regionId); + if (lowLocalLsn) { + highestLocalLsnByRegion.set(regionId, max(highLocalLsn, lowLocalLsn)); + } else if (this.version === other.version) { + throw new Error( + `Compared session tokens have unexpected regions. Session 1: ${this.sessionToken} - Session 2: ${ + this.sessionToken + }` + ); + } else { + highestLocalLsnByRegion.set(regionId, highLocalLsn); + } + } + + return new VectorSessionToken( + Math.max(this.version, other.version), + Math.max(this.globalLsn, other.globalLsn), + highestLocalLsnByRegion + ); + } + + public toString() { + return this.sessionToken; + } + + private areRegionProgressEqual(other: Map): boolean { + if (this.localLsnByregion.size !== other.size) { + return false; + } + + for (const [regionId, localLsn] of this.localLsnByregion.entries()) { + const otherLocalLsn = other.get(regionId); + + if (localLsn !== otherLocalLsn) { + return false; + } + } + return true; + } +} + +function max(int1: string, int2: string) { + // NOTE: This only works for positive numbers + if (int1.length === int2.length) { + return int1 > int2 ? int1 : int2; + } else if (int1.length > int2.length) { + return int1; + } else { + return int2; + } +} diff --git a/sdk/cosmosdb/cosmos/src/session/sessionContainer.ts b/sdk/cosmosdb/cosmos/src/session/sessionContainer.ts new file mode 100644 index 000000000000..4442dea7fd7f --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/session/sessionContainer.ts @@ -0,0 +1,157 @@ +import { Constants, Helper } from "../common"; +import { IHeaders } from "../queryExecutionContext"; +import { SessionContext } from "./SessionContext"; +import { VectorSessionToken } from "./VectorSessionToken"; + +/** @hidden */ +export class SessionContainer { + private static readonly EMPTY_SESSION_TOKEN = ""; + private static readonly SESSION_TOKEN_SEPARATOR = ","; + private static readonly SESSION_TOKEN_PARTITION_SPLITTER = ":"; + constructor( + private collectionNameToCollectionResourceId = new Map(), + private collectionResourceIdToSessionTokens = new Map>() + ) {} + + public get(request: SessionContext) { + if (!request) { + throw new Error("request cannot be null"); + } + const collectionName = Helper.getContainerLink(Helper.trimSlashes(request.resourceAddress)); + const rangeIdToTokenMap = this.getPartitionKeyRangeIdToTokenMap(collectionName); + return SessionContainer.getCombinedSessionTokenString(rangeIdToTokenMap); + } + + public remove(request: SessionContext) { + let collectionResourceId: string; + const resourceAddress = Helper.trimSlashes(request.resourceAddress); + const collectionName = Helper.getContainerLink(resourceAddress); + if (collectionName) { + collectionResourceId = this.collectionNameToCollectionResourceId.get(collectionName); + this.collectionNameToCollectionResourceId.delete(collectionName); + } + if (collectionResourceId !== undefined) { + this.collectionResourceIdToSessionTokens.delete(collectionResourceId); + } + } + + public set(request: SessionContext, resHeaders: IHeaders) { + // TODO: we check the master logic a few different places. Might not need it. + if (!resHeaders || SessionContainer.isReadingFromMaster(request.resourceType, request.operationType)) { + return; + } + + const sessionTokenString = resHeaders[Constants.HttpHeaders.SessionToken]; + if (!sessionTokenString) { + return; + } + + const containerName = this.getContainerName(request, resHeaders); + + const ownerId = !request.isNameBased + ? request.resourceId + : resHeaders[Constants.HttpHeaders.OwnerId] || request.resourceId; + + if (!ownerId) { + return; + } + + if (containerName && this.validateOwnerID(ownerId)) { + if (!this.collectionResourceIdToSessionTokens.has(ownerId)) { + this.collectionResourceIdToSessionTokens.set(ownerId, new Map()); + } + + if (!this.collectionNameToCollectionResourceId.has(containerName)) { + this.collectionNameToCollectionResourceId.set(containerName, ownerId); + } + + const containerSessionContainer = this.collectionResourceIdToSessionTokens.get(ownerId); + SessionContainer.compareAndSetToken(sessionTokenString, containerSessionContainer); + } + } + + private validateOwnerID(ownerId: string) { + const ownerIdBuffer = Buffer.from(ownerId, "base64"); + // If ownerId contains exactly 8 bytes it represents a unique database+collection identifier. Otherwise it represents another resource + // The first 4 bytes are the database. The last 4 bytes are the collection. + if (ownerIdBuffer.length === 8) { + return true; + } + return false; + } + + private getPartitionKeyRangeIdToTokenMap(collectionName: string): Map { + let rangeIdToTokenMap: Map = null; + if (collectionName && this.collectionNameToCollectionResourceId.has(collectionName)) { + rangeIdToTokenMap = this.collectionResourceIdToSessionTokens.get( + this.collectionNameToCollectionResourceId.get(collectionName) + ); + } + + return rangeIdToTokenMap; + } + + private static getCombinedSessionTokenString(tokens: Map) { + if (!tokens || tokens.size === 0) { + return SessionContainer.EMPTY_SESSION_TOKEN; + } + + let result = ""; + for (const [range, token] of tokens.entries()) { + result += + range + + SessionContainer.SESSION_TOKEN_PARTITION_SPLITTER + + token.toString() + + SessionContainer.SESSION_TOKEN_SEPARATOR; + } + return result.slice(0, -1); + } + + private static compareAndSetToken(newTokenString: string, containerSessionTokens: Map) { + if (!newTokenString) { + return; + } + + const partitionsParts = newTokenString.split(SessionContainer.SESSION_TOKEN_SEPARATOR); + for (const partitionPart of partitionsParts) { + const newTokenParts = partitionPart.split(SessionContainer.SESSION_TOKEN_PARTITION_SPLITTER); + if (newTokenParts.length !== 2) { + return; + } + + const range = newTokenParts[0]; + const newToken = VectorSessionToken.create(newTokenParts[1]); + const tokenForRange = !containerSessionTokens.get(range) + ? newToken + : containerSessionTokens.get(range).merge(newToken); + containerSessionTokens.set(range, tokenForRange); + } + } + + // TODO: have a assert if the type doesn't mastch known types + private static isReadingFromMaster(resourceType: string, operationType: string): boolean { + if ( + resourceType === Constants.Path.OffersPathSegment || + resourceType === Constants.Path.DatabasesPathSegment || + resourceType === Constants.Path.UsersPathSegment || + resourceType === Constants.Path.PermissionsPathSegment || + resourceType === Constants.Path.TopologyPathSegment || + resourceType === Constants.Path.DatabaseAccountPathSegment || + resourceType === Constants.Path.PartitionKeyRangesPathSegment || + (resourceType === Constants.Path.CollectionsPathSegment && operationType === Constants.OperationTypes.Query) + ) { + return true; + } + + return false; + } + + private getContainerName(request: SessionContext, headers: IHeaders) { + let ownerFullName = headers[Constants.HttpHeaders.OwnerFullName]; + if (!ownerFullName) { + ownerFullName = Helper.trimSlashes(request.resourceAddress); + } + + return Helper.getContainerLink(ownerFullName as string); + } +} diff --git a/sdk/cosmosdb/cosmos/src/test/common/BaselineTest.PathParser.ts b/sdk/cosmosdb/cosmos/src/test/common/BaselineTest.PathParser.ts new file mode 100644 index 000000000000..5c9435a0fe91 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/common/BaselineTest.PathParser.ts @@ -0,0 +1,98 @@ +export default [ + { + path: "/", + parts: [] + }, + { + path: "/*", + parts: ["*"] + }, + { + path: '/"Key1"/*', + parts: ["Key1", "*"] + }, + { + path: '/"Key1"/"StringValue"/*', + parts: ["Key1", "StringValue", "*"] + }, + { + path: "/'Key1'/'StringValue'/*", + parts: ["Key1", "StringValue", "*"] + }, + { + path: "/'Ke\\\"\\\"y1'/'Strin\\\"gValue'/*", + parts: ['Ke\\"\\"y1', 'Strin\\"gValue', "*"] + }, + { + path: '/\'Ke\\"\\"y1\'/"Strin\'gValue"/*', + parts: ['Ke\\"\\"y1', "Strin'gValue", "*"] + }, + { + path: "/'Key1'/'StringValue'/*", + parts: ["Key1", "StringValue", "*"] + }, + { + path: '/"Key1"/"Key2"/*', + parts: ["Key1", "Key2", "*"] + }, + { + path: '/"Key1"/"Key2"/"Key3"/*', + parts: ["Key1", "Key2", "Key3", "*"] + }, + { + path: '/"A"/"B"/"R"/[]/"Address"/[]/*', + parts: ["A", "B", "R", "[]", "Address", "[]", "*"] + }, + { + path: '/"A"/"B"/"R"/[]/"Address"/[]/*', + parts: ["A", "B", "R", "[]", "Address", "[]", "*"] + }, + { + path: '/"A"/"B"/"R"/[]/"Address"/*', + parts: ["A", "B", "R", "[]", "Address", "*"] + }, + { + path: '/"Key1"/"Key2"/?', + parts: ["Key1", "Key2", "?"] + }, + { + path: '/"Key1"/"Key2"/*', + parts: ["Key1", "Key2", "*"] + }, + { + path: '/"123"/"StringValue"/*', + parts: ["123", "StringValue", "*"] + }, + { + path: "/'!@#$%^&*()_+='/'StringValue'/*", + parts: ["!@#$%^&*()_+=", "StringValue", "*"] + }, + { + path: '/"_ts"/?', + parts: ["_ts", "?"] + }, + { + path: '/[]/"City"/*', + parts: ["[]", "City", "*"] + }, + { + path: "/[]/*", + parts: ["[]", "*"] + }, + { + path: '/[]/"fine!"/*', + parts: ["[]", "fine!", "*"] + }, + { + path: + '/"this is a long key with speicial characters (*)(*)__)((*&*(&*&\'*(&)()(*_)()(_(_)*!@#$%^ and numbers 132654890"/*', + parts: [ + "this is a long key with speicial characters (*)(*)__)((*&*(&*&'*(&)()(*_)()(_(_)*!@#$%^ and numbers 132654890", + "*" + ] + }, + { + path: "/ Key 1 / Key 2 ", + parts: ["Key 1", "Key 2"] + } +]; diff --git a/sdk/cosmosdb/cosmos/src/test/common/MockClientContext.ts b/sdk/cosmosdb/cosmos/src/test/common/MockClientContext.ts new file mode 100644 index 000000000000..12d8e1481e45 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/common/MockClientContext.ts @@ -0,0 +1,13 @@ +import { MockedQueryIterator } from "./MockQueryIterator"; + +/** @hidden */ +export class MockedClientContext { + constructor(private partitionKeyRanges: any, private collectionId: any) {} + public readPartitionKeyRanges(collectionLink: any) { + return new MockedQueryIterator(this.partitionKeyRanges); + } + + public queryPartitionKeyRanges(collectionLink: any) { + return new MockedQueryIterator(this.partitionKeyRanges); + } +} diff --git a/sdk/cosmosdb/cosmos/src/test/common/MockQueryIterator.ts b/sdk/cosmosdb/cosmos/src/test/common/MockQueryIterator.ts new file mode 100644 index 000000000000..69ff985c9572 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/common/MockQueryIterator.ts @@ -0,0 +1,7 @@ +/** @hidden */ +export class MockedQueryIterator { + constructor(private results: any) {} + public async toArray() { + return { result: this.results }; + } +} diff --git a/sdk/cosmosdb/cosmos/src/test/common/TestData.ts b/sdk/cosmosdb/cosmos/src/test/common/TestData.ts new file mode 100644 index 000000000000..57af3d0bf014 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/common/TestData.ts @@ -0,0 +1,42 @@ +/** @hidden */ +export class TestData { + public numberOfDocuments: number; + public field: string; + public numberOfDocsWithSamePartitionKey: number; + public numberOfDocumentsWithNumbericId: number; + public sum: number; + public docs: any[]; + constructor(public partitionKey: string, public uniquePartitionKey: string) { + this.numberOfDocuments = 800; + this.field = "field"; + const docs = []; + + const values = [null, false, true, "abc", "cdfg", "opqrs", "ttttttt", "xyz", "oo", "ppp"]; + for (const value of values) { + const d: any = {}; + d[partitionKey] = value; + docs.push(d); + } + + this.numberOfDocsWithSamePartitionKey = 400; + for (let i = 0; i < this.numberOfDocsWithSamePartitionKey; ++i) { + const d: any = {}; + d[partitionKey] = uniquePartitionKey; + d["resourceId"] = i.toString(); + d[this.field] = i + 1; + docs.push(d); + } + + this.numberOfDocumentsWithNumbericId = + this.numberOfDocuments - values.length - this.numberOfDocsWithSamePartitionKey; + for (let i = 0; i < this.numberOfDocumentsWithNumbericId; ++i) { + const d: any = {}; + d[partitionKey] = i + 1; + docs.push(d); + } + + this.sum = (this.numberOfDocumentsWithNumbericId * (this.numberOfDocumentsWithNumbericId + 1)) / 2.0; + + this.docs = docs; + } +} diff --git a/sdk/cosmosdb/cosmos/src/test/common/TestHelpers.ts b/sdk/cosmosdb/cosmos/src/test/common/TestHelpers.ts new file mode 100644 index 000000000000..c6158f3d1c56 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/common/TestHelpers.ts @@ -0,0 +1,328 @@ +import assert from "assert"; +import { Container, CosmosClient, Database, DatabaseDefinition, Item, RequestOptions, Response } from "../.."; +import { + ContainerDefinition, + ItemDefinition, + ItemResponse, + PermissionResponse, + Resource, + TriggerResponse, + User, + UserDefinedFunctionResponse +} from "../../client"; +import { StoredProcedureResponse } from "../../client/StoredProcedure/StoredProcedureResponse"; +import { UserResponse } from "../../client/User/UserResponse"; +import { endpoint, masterKey } from "./_testConfig"; + +const defaultClient = new CosmosClient({ endpoint, auth: { masterKey } }); + +export function addEntropy(name: string): string { + return name + getEntropy(); +} + +export function getEntropy(): string { + return `${Math.floor(Math.random() * 10000)}`; +} + +export async function removeAllDatabases(client: CosmosClient = defaultClient) { + try { + const { result: databases } = await client.databases.readAll().toArray(); + const length = databases.length; + + if (length === 0) { + return; + } + + const count = 0; + await Promise.all( + databases.map>>(async (database: DatabaseDefinition) => + client.database(database.id).delete() + ) + ); + } catch (err) { + // TODO: remove console logging for errors and add ts-lint flag back + console.log("An error occured", err); + assert.fail(err); + throw err; + } +} + +export async function getTestDatabase(testName: string, client: CosmosClient = defaultClient) { + const entropy = Math.floor(Math.random() * 10000); + const id = `${testName.replace(" ", "").substring(0, 30)}${entropy}`; + await client.databases.create({ id }); + return client.database(id); +} + +export async function getTestContainer( + testName: string, + client: CosmosClient = defaultClient, + containerDef?: ContainerDefinition, + options?: RequestOptions +) { + const db = await getTestDatabase(testName, client); + const entropy = Math.floor(Math.random() * 10000); + const id = `${testName.replace(" ", "").substring(0, 30)}${entropy}`; + await db.containers.create({ ...containerDef, ...{ id } }, options); + return db.container(id); +} + +export async function bulkInsertItems( + container: Container, + documents: any[] +): Promise> { + const returnedDocuments = []; + for (const doc of documents) { + try { + const { body: document } = await container.items.create(doc); + returnedDocuments.push(document); + } catch (err) { + throw err; + } + } + return returnedDocuments; +} + +export async function bulkReadItems(container: Container, documents: any[], partitionKey: string) { + for (const document of documents) { + try { + const options = + partitionKey && document.hasOwnProperty(partitionKey) + ? { partitionKey: document[partitionKey] } + : { partitionKey: {} }; + + // TODO: should we block or do all requests in parallel? + const { body: doc } = await container.item(document.id).read(options); + + assert.equal(JSON.stringify(doc), JSON.stringify(document)); + } catch (err) { + throw err; + } + } +} + +export async function bulkReplaceItems(container: Container, documents: any[]): Promise { + const returnedDocuments: any[] = []; + for (const document of documents) { + try { + const { body: doc } = await container.item(document.id).replace(document); + const expectedModifiedDocument = JSON.parse(JSON.stringify(document)); + delete expectedModifiedDocument._etag; + delete expectedModifiedDocument._ts; + const actualModifiedDocument = JSON.parse(JSON.stringify(doc)); + delete actualModifiedDocument._etag; + delete actualModifiedDocument._ts; + assert.equal(JSON.stringify(actualModifiedDocument), JSON.stringify(expectedModifiedDocument)); + returnedDocuments.push(doc); + } catch (err) { + throw err; + } + } + return returnedDocuments; +} + +export async function bulkDeleteItems( + container: Container, + documents: any[], + partitionKeyPropertyName: string +): Promise { + for (const document of documents) { + try { + const options = + partitionKeyPropertyName && document.hasOwnProperty(partitionKeyPropertyName) + ? { partitionKey: document[partitionKeyPropertyName] } + : { partitionKey: {} }; + + await container.item(document.id).delete(options); + } catch (err) { + throw err; + } + } +} + +export async function bulkQueryItemsWithPartitionKey( + container: Container, + documents: any[], + partitionKeyPropertyName: any +): Promise { + for (const document of documents) { + try { + if (!document.hasOwnProperty(partitionKeyPropertyName)) { + continue; + } + + const querySpec = { + query: "SELECT * FROM root r WHERE r." + partitionKeyPropertyName + "=@key", + parameters: [ + { + name: "@key", + value: document[partitionKeyPropertyName] + } + ] + }; + + const { result: results } = await container.items.query(querySpec).toArray(); + assert.equal(results.length, 1, "Expected exactly 1 document"); + assert.equal(JSON.stringify(results[0]), JSON.stringify(document)); + } catch (err) { + throw err; + } + } +} + +// Item +export async function createOrUpsertItem( + container: Container, + body: any, + options: RequestOptions, + isUpsertTest: boolean +): Promise> { + if (isUpsertTest) { + return container.items.upsert(body, options); + } else { + return container.items.create(body, options); + } +} + +export async function replaceOrUpsertItem( + container: Container, + body: any, + options: RequestOptions, + isUpsertTest: boolean +): Promise> { + if (isUpsertTest) { + return container.items.upsert(body, options); + } else { + return container.item(body.id).replace(body, options); + } +} + +// User +export function createOrUpsertUser( + database: Database, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return database.users.upsert(body, options); + } else { + return database.users.create(body, options); + } +} +export function replaceOrUpsertUser( + database: Database, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return database.users.upsert(body, options); + } else { + return database.user(body.id).replace(body, options); + } +} + +export function createOrUpsertPermission( + user: User, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return user.permissions.upsert(body, options); + } else { + return user.permissions.create(body, options); + } +} + +export function replaceOrUpsertPermission( + user: User, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return user.permissions.upsert(body, options); + } else { + return user.permission(body.id).replace(body, options); + } +} + +// Trigger +export function createOrUpsertTrigger( + container: Container, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return container.triggers.upsert(body, options); + } else { + return container.triggers.create(body, options); + } +} +export function replaceOrUpsertTrigger( + container: Container, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return container.triggers.upsert(body, options); + } else { + return container.trigger(body.id).replace(body, options); + } +} + +// User Defined Function +export function createOrUpsertUserDefinedFunction( + container: Container, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return container.userDefinedFunctions.upsert(body, options); + } else { + return container.userDefinedFunctions.create(body, options); + } +} +export function replaceOrUpsertUserDefinedFunction( + container: Container, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return container.userDefinedFunctions.upsert(body, options); + } else { + return container.userDefinedFunction(body.id).replace(body, options); + } +} + +// Stored Procedure +export function createOrUpsertStoredProcedure( + container: Container, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return container.storedProcedures.upsert(body, options); + } else { + return container.storedProcedures.create(body, options); + } +} +export function replaceOrUpsertStoredProcedure( + container: Container, + body: any, + options: any, + isUpsertTest: boolean +): Promise { + if (isUpsertTest) { + return container.storedProcedures.upsert(body, options); + } else { + return container.storedProcedure(body.id).replace(body, options); + } +} diff --git a/sdk/cosmosdb/cosmos/src/test/common/_testConfig.ts b/sdk/cosmosdb/cosmos/src/test/common/_testConfig.ts new file mode 100644 index 000000000000..014468392586 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/common/_testConfig.ts @@ -0,0 +1,11 @@ +// [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine")] +const masterKey = + process.env.ACCOUNT_KEY || "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; +const endpoint = process.env.ACCOUNT_HOST || "https://localhost:8081"; + +// This is needed to disable SSL verification for the tests running against emulator. +if (endpoint.includes("https://localhost")) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +} + +export { masterKey, endpoint }; diff --git a/sdk/cosmosdb/cosmos/src/test/common/setup.ts b/sdk/cosmosdb/cosmos/src/test/common/setup.ts new file mode 100644 index 000000000000..3a3e44cade2f --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/common/setup.ts @@ -0,0 +1,11 @@ +process.on("unhandledRejection", error => { + if (error.body) { + try { + error.body = JSON.parse(error.body); + } catch (err) { + /* NO OP */ + } + } + console.error(new Error("Unhandled exception found")); + console.error(JSON.stringify(error, null, " ")); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/authorization.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/authorization.spec.ts new file mode 100644 index 000000000000..7bb4ffd25b01 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/authorization.spec.ts @@ -0,0 +1,241 @@ +import assert from "assert"; +import { CosmosClient, DocumentBase } from "../.."; +import { PermissionDefinition } from "../../client"; +import { endpoint, masterKey } from "../common/_testConfig"; +import { createOrUpsertPermission, getTestContainer, getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + await removeAllDatabases(); + }); + + describe("Validate Authorization", function() { + it("should handle all the key options", async function() { + const clientOptionsKey = new CosmosClient({ endpoint, key: masterKey }); + assert( + undefined !== (await clientOptionsKey.databases.readAll().toArray()), + "Should be able to fetch list of databases" + ); + + const clientOptionsAuthKey = new CosmosClient({ endpoint, auth: { key: masterKey } }); + assert( + undefined !== (await clientOptionsAuthKey.databases.readAll().toArray()), + "Should be able to fetch list of databases" + ); + + const clientOptionsAuthMasterKey = new CosmosClient({ endpoint, auth: { masterKey } }); + assert( + undefined !== (await clientOptionsAuthMasterKey.databases.readAll().toArray()), + "Should be able to fetch list of databases" + ); + }); + + const setupEntities = async function(isUpsertTest: boolean) { + // create database + const database = await getTestDatabase("Validate Authorization database"); + // create container1 + + const { body: container1 } = await database.containers.create({ id: "Validate Authorization container" }); + // create document1 + const { body: document1 } = await database + .container(container1.id) + .items.create({ id: "coll1doc1", foo: "bar", key: "value" }); + // create document 2 + const { body: document2 } = await database + .container(container1.id) + .items.create({ id: "coll1doc2", foo: "bar2", key: "value2" }); + + // create container 2 + const { body: container2 } = await database.containers.create({ id: "sample container2" }); + + // create user1 + const { body: user1 } = await database.users.create({ id: "user1" }); + let permission = { + id: "permission On Coll1", + permissionMode: DocumentBase.PermissionMode.Read, + resource: (container1 as any)._self + }; // TODO: any rid stuff + // create permission for container1 + const { body: permissionOnColl1 } = await createOrUpsertPermission( + database.user(user1.id), + permission, + undefined, + isUpsertTest + ); + assert((permissionOnColl1 as any)._token !== undefined, "permission token is invalid"); + permission = { + id: "permission On Doc1", + permissionMode: DocumentBase.PermissionMode.All, + resource: (document2 as any)._self // TODO: any rid + }; + // create permission for document 2 + const { body: permissionOnDoc2 } = await createOrUpsertPermission( + database.user(user1.id), + permission, + undefined, + isUpsertTest + ); + assert((permissionOnDoc2 as any)._token !== undefined, "permission token is invalid"); // TODO: any rid + + // create user 2 + const { body: user2 } = await database.users.create({ id: "user2" }); + permission = { + id: "permission On coll2", + permissionMode: DocumentBase.PermissionMode.All, + resource: (container2 as any)._self // TODO: any rid + }; + // create permission on container 2 + const { body: permissionOnColl2 } = await createOrUpsertPermission( + database.user(user2.id), + permission, + undefined, + isUpsertTest + ); + const entities = { + database, + coll1: container1, + coll2: container2, + doc1: document1, + doc2: document2, + user1, + user2, + permissionOnColl1, + permissionOnDoc2, + permissionOnColl2 + }; + + return entities; + }; + + const authorizationCRUDTest = async function(isUpsertTest: boolean) { + try { + const badclient = new CosmosClient({ endpoint, auth: undefined }); + const { result: databases } = await badclient.databases.readAll().toArray(); + assert.fail("Must fail"); + } catch (err) { + assert(err !== undefined, "error should not be undefined"); + const unauthorizedErrorCode = 401; + assert.equal(err.code, unauthorizedErrorCode, "error code should be equal to 401"); + } + + // setup entities + // TODO: should move this out of this test and into before/etc. + const entities = await setupEntities(isUpsertTest); + const resourceTokens: any = {}; + resourceTokens[entities.coll1.id] = (entities.permissionOnColl1 as any)._token; + resourceTokens[entities.doc1.id] = (entities.permissionOnColl1 as any)._token; + + const col1Client = new CosmosClient({ endpoint, auth: { resourceTokens } }); + + // 1. Success-- Use Col1 Permission to Read + const { body: successColl1 } = await col1Client + .database(entities.database.id) + .container(entities.coll1.id) + .read(); + assert(successColl1 !== undefined, "error reading container"); + + // 2. Failure-- Use Col1 Permission to delete + try { + await col1Client + .database(entities.database.id) + .container(entities.coll1.id) + .delete(); + assert.fail("must fail if no permission"); + } catch (err) { + assert(err !== undefined, "expected to fail, no permission to delete"); + assert.equal(err.code, 403, "Must return a code for not authorized"); + } + + // 3. Success-- Use Col1 Permission to Read All Docs + const { result: successDocuments } = await col1Client + .database(entities.database.id) + .container(entities.coll1.id) + .items.readAll() + .toArray(); + assert(successDocuments !== undefined, "error reading documents"); + assert.equal(successDocuments.length, 2, "Expected 2 Documents to be succesfully read"); + + // 4. Success-- Use Col1 Permission to Read Col1Doc1 + const { body: successDoc } = await col1Client + .database(entities.database.id) + .container(entities.coll1.id) + .item(entities.doc1.id) + .read(); + assert(successDoc !== undefined, "error reading document"); + assert.equal(successDoc.id, entities.doc1.id, "Expected to read children using parent permissions"); + + // TODO: Permission Feed uses RID right now + /* + const col2Client = new CosmosClient({ + endpoint, + auth: { permissionFeed: [entities.permissionOnColl2] }, + }); + const doc = { id: "new doc", CustomProperty1: "BBBBBB", customProperty2: 1000 }; + const col2Container = await col2Client.databaseDatabase(entities.db.id) + .containerContainer(entities.coll2.id); + const { result: successDoc2 } = await createOrUpsertItem( + col2Container, doc, undefined, isUpsertTest); + assert(successDoc2 !== undefined, "error creating document"); + assert.equal(successDoc2.CustomProperty1, doc.CustomProperty1, + "document should have been created successfully"); + */ + }; + + const authorizationCRUDOverMultiplePartitionsTest = async function() { + // create database + // create container + const partitionKey = "key"; + const containerDefinition = { + id: "coll1", + partitionKey: { paths: ["/" + partitionKey], kind: DocumentBase.PartitionKind.Hash } + }; + const container = await getTestContainer("authorization CRUD multiple partitons", undefined, containerDefinition); + // create user + const { body: userDef } = await container.database.users.create({ id: "user1" }); + const user = container.database.user(userDef.id); + + const key = 1; + const permissionDefinition: PermissionDefinition = { + id: "permission1", + permissionMode: DocumentBase.PermissionMode.All, + resource: container.url, + resourcePartitionKey: [key] + }; + + // create permission + const { body: permission } = await user.permissions.create(permissionDefinition); + assert((permission as any)._token !== undefined, "permission token is invalid"); + const resourceTokens: any = {}; + resourceTokens[container.id] = (permission as any)._token; + + const restrictedClient = new CosmosClient({ endpoint, auth: { resourceTokens } }); + await restrictedClient + .database(container.database.id) + .container(container.id) + .items.create({ id: "document1", key: 1 }); + try { + await restrictedClient + .database(container.database.id) + .container(container.id) + .items.create({ id: "document2", key: 2 }); + assert.fail("Must throw unauthorized on read"); + } catch (err) { + const unauthorizedErrorCode = 403; + assert.equal(err.code, unauthorizedErrorCode); + } + }; + + it("nativeApi Should do authorization successfully name based", async function() { + await authorizationCRUDTest(false); + }); + + it("nativeApi Should do authorization successfully name based with upsert", async function() { + await authorizationCRUDTest(true); + }); + + it("nativeApi Should do authorization over multiple partitions successfully name based", async function() { + await authorizationCRUDOverMultiplePartitionsTest(); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/client.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/client.spec.ts new file mode 100644 index 000000000000..d940b0fdd879 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/client.spec.ts @@ -0,0 +1,48 @@ +import assert from "assert"; +import { Agent } from "http"; +import { CosmosClient, DocumentBase } from "../.."; +import { endpoint, masterKey } from "../common/_testConfig"; +import { getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 20000); + + describe("Validate client request timeout", function() { + it("nativeApi Client Should throw exception", async function() { + const connectionPolicy = new DocumentBase.ConnectionPolicy(); + // making timeout 5 ms to make sure it will throw + // (create database request takes 10ms-15ms to finish on emulator) + connectionPolicy.RequestTimeout = 1; + const client = new CosmosClient({ endpoint, auth: { masterKey }, connectionPolicy }); + // create database + try { + await getTestDatabase("request timeout", client); + assert.fail("Must throw when trying to connect to database"); + } catch (err) { + assert.equal(err.code, "ECONNRESET", "client should throw exception"); + } + }); + }); + + describe("Constructor", function() { + it("Should work with a non-class based Connection Policy", function() { + const client = new CosmosClient({ + endpoint: "https://faaaaaake.com", + auth: { masterKey: "" }, + connectionPolicy: { + RequestTimeout: 10000 + } + }); + assert.ok(client !== undefined, "client shouldn't be undefined if it succeeded"); + }); + + it("Accepts node Agent", function() { + const client = new CosmosClient({ + endpoint: "https://faaaaaake.com", + auth: { masterKey: "" }, + agent: new Agent() + }); + assert.ok(client !== undefined, "client shouldn't be undefined if it succeeded"); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/container.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/container.spec.ts new file mode 100644 index 000000000000..2047b7264138 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/container.spec.ts @@ -0,0 +1,439 @@ +import assert from "assert"; +import { Constants, DocumentBase } from "../.."; +import { ContainerDefinition, Database } from "../../client"; +import { DataType, Index, IndexedPath, IndexingMode, IndexingPolicy, IndexKind } from "../../documents"; +import { getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + await removeAllDatabases(); + }); + + describe("Validate Container CRUD", function() { + const containerCRUDTest = async function(hasPartitionKey: boolean) { + try { + // create database + const database = await getTestDatabase("Validate Container CRUD"); + + // create a container + const containerDefinition: ContainerDefinition = { + id: "sample container", + indexingPolicy: { indexingMode: IndexingMode.consistent } + }; + + if (hasPartitionKey) { + containerDefinition.partitionKey = { paths: ["/id"], kind: DocumentBase.PartitionKind.Hash }; + } + + const { body: containerDef } = await database.containers.create(containerDefinition); + const container = database.container(containerDef.id); + assert.equal(containerDefinition.id, containerDef.id); + assert.equal("consistent", containerDef.indexingPolicy.indexingMode); + if (containerDef.partitionKey) { + assert.equal(containerDef.partitionKey.kind, containerDefinition.partitionKey.kind); + assert.deepEqual(containerDef.partitionKey.paths, containerDefinition.partitionKey.paths); + } + // read containers after creation + const { result: containers } = await database.containers.readAll().toArray(); + + assert.equal(containers.length, 1, "create should increase the number of containers"); + // query containers + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: containerDefinition.id + } + ] + }; + const { result: results } = await database.containers.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + const { result: ranges } = await container.readPartitionKeyRanges().toArray(); + assert(ranges.length > 0, "container should have at least 1 partition"); + + // Replacing indexing policy is allowed. + containerDef.indexingPolicy.indexingMode = IndexingMode.lazy; + const { body: replacedContainer } = await container.replace(containerDef); + assert.equal("lazy", replacedContainer.indexingPolicy.indexingMode); + + // Replacing partition key is not allowed. + try { + containerDef.partitionKey = { paths: ["/key"], kind: DocumentBase.PartitionKind.Hash }; + await container.replace(containerDef); + assert.fail("Replacing paritionkey must throw"); + } catch (err) { + const badRequestErrorCode = 400; + assert.equal(err.code, badRequestErrorCode, "response should return error code " + badRequestErrorCode); + } finally { + containerDef.partitionKey = containerDefinition.partitionKey; // Resume partition key + } + // Replacing id is not allowed. + try { + containerDef.id = "try_to_replace_id"; + await container.replace(containerDef); + assert.fail("Replacing container id must throw"); + } catch (err) { + const notFoundErrorCode = 400; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + + // read container + containerDef.id = containerDefinition.id; // Resume Id. + const { body: readcontainer } = await container.read(); + assert.equal(containerDefinition.id, readcontainer.id); + + // delete container + await container.delete(); + + // read container after deletion + try { + await container.read(); + assert.fail("Must fail to read container after delete"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + } catch (err) { + throw err; + } + }; + + const badPartitionKeyDefinitionTest = async function(isNameBased: boolean) { + try { + // create database + const database = await getTestDatabase("container CRUD bad partition key"); + + // create a container + const badPartitionKeyDefinition: any = { + paths: "/id", // This is invalid. Must be an array. + kind: DocumentBase.PartitionKind.Hash + }; + + const containerDefinition: ContainerDefinition = { + id: "sample container", + indexingPolicy: { indexingMode: IndexingMode.consistent }, + partitionKey: badPartitionKeyDefinition // This is invalid, forced using type coersion + }; + + try { + await database.containers.create(containerDefinition); + } catch (err) { + assert.equal(err.code, 400); + } + } catch (err) { + throw err; + } + }; + + it("nativeApi Should do container CRUD operations successfully name based", async function() { + try { + await containerCRUDTest(false); + } catch (err) { + throw err; + } + }); + + it("nativeApi Should do elastic container CRUD operations successfully name based", async function() { + try { + await containerCRUDTest(true); + } catch (err) { + throw err; + } + }); + + it("nativeApi container with bad partition key definition name based", async function() { + try { + await badPartitionKeyDefinitionTest(true); + } catch (err) { + throw err; + } + }); + + it("nativeApi container with bad partition key definition name based", async function() { + try { + await badPartitionKeyDefinitionTest(false); + } catch (err) { + throw err; + } + }); + }); + + describe("Validate container indexing policy", function() { + it("nativeApi Should create container with correct indexing policy name based", async function() { + // create database + const database = await getTestDatabase("container test database"); + + // create container + const { body: containerDef } = await database.containers.create({ id: "container test container" }); + const container = database.container(containerDef.id); + + assert.equal( + containerDef.indexingPolicy.indexingMode, + DocumentBase.IndexingMode.consistent, + "default indexing mode should be consistent" + ); + await container.delete(); + + const lazyContainerDefinition: ContainerDefinition = { + id: "lazy container", + indexingPolicy: { indexingMode: DocumentBase.IndexingMode.lazy } + }; + + const { body: lazyContainerDef } = await database.containers.create(lazyContainerDefinition); + const lazyContainer = database.container(lazyContainerDef.id); + + assert.equal( + lazyContainerDef.indexingPolicy.indexingMode, + DocumentBase.IndexingMode.lazy, + "indexing mode should be lazy" + ); + + await lazyContainer.delete(); + + const uniqueKeysContainerDefinition: ContainerDefinition = { + id: "uniqueKeysContainer", + uniqueKeyPolicy: { uniqueKeys: [{ paths: ["/foo"] }] } + }; + + const { body: uniqueKeysContainerDef } = await database.containers.create(uniqueKeysContainerDefinition); + const uniqueKeysContainer = database.container(uniqueKeysContainerDef.id); + + assert.equal(uniqueKeysContainerDef.uniqueKeyPolicy.uniqueKeys[0].paths, "/foo"); + + await uniqueKeysContainer.delete(); + + const consistentcontainerDefinition: ContainerDefinition = { + id: "lazy container", + indexingPolicy: { indexingMode: "consistent" } // tests the type flexibility + }; + const { body: consistentContainerDef } = await database.containers.create(consistentcontainerDefinition); + const consistentContainer = database.container(consistentContainerDef.id); + assert.equal( + containerDef.indexingPolicy.indexingMode, + DocumentBase.IndexingMode.consistent, + "indexing mode should be consistent" + ); + await consistentContainer.delete(); + + const containerDefinition: ContainerDefinition = { + id: "containerWithIndexingPolicy", + indexingPolicy: { + automatic: true, + indexingMode: DocumentBase.IndexingMode.consistent, + includedPaths: [ + { + path: "/", + indexes: [ + { + kind: DocumentBase.IndexKind.Hash, + dataType: DocumentBase.DataType.Number, + precision: 2 + } + ] + } + ], + excludedPaths: [ + { + path: '/"systemMetadata"/*' + } + ] + } + }; + + const { body: containerWithIndexingPolicyDef } = await database.containers.create(containerDefinition); + + // Two included paths. + assert.equal( + 1, + containerWithIndexingPolicyDef.indexingPolicy.includedPaths.length, + "Unexpected includedPaths length" + ); + // The first included path is what we created. + assert.equal("/", containerWithIndexingPolicyDef.indexingPolicy.includedPaths[0].path); + // Backend adds a default index + assert(containerWithIndexingPolicyDef.indexingPolicy.includedPaths[0].indexes.length > 1); + assert.equal( + DocumentBase.IndexKind.Range, + containerWithIndexingPolicyDef.indexingPolicy.includedPaths[0].indexes[0].kind + ); + // The second included path is a timestamp index created by the server. + + // And two excluded paths. + assert.equal( + 2, + containerWithIndexingPolicyDef.indexingPolicy.excludedPaths.length, + "Unexpected excludedPaths length" + ); + assert.equal('/"systemMetadata"/*', containerWithIndexingPolicyDef.indexingPolicy.excludedPaths[0].path); + }); + + const checkDefaultIndexingPolicyPaths = function(indexingPolicy: IndexingPolicy) { + assert.equal(1, indexingPolicy["excludedPaths"].length); + assert.equal(1, indexingPolicy["includedPaths"].length); + + let rootIncludedPath: IndexedPath = null; + if (indexingPolicy["includedPaths"][0]["path"] === "/*") { + rootIncludedPath = indexingPolicy["includedPaths"][0]; + } + + assert(rootIncludedPath); // root path should exist. + + // In the root path, there should be two indexes. One for Strings and one for Numbers. + assert.equal(2, rootIncludedPath["indexes"].length); + }; + + const defaultIndexingPolicyTest = async function() { + try { + // create database + const database = await getTestDatabase("container test database"); + + // create container with no indexing policy specified. + const containerDefinition01: ContainerDefinition = { id: "TestCreateDefaultPolicy01" }; + const { body: containerNoIndexPolicyDef } = await database.containers.create(containerDefinition01); + checkDefaultIndexingPolicyPaths(containerNoIndexPolicyDef["indexingPolicy"]); + + // create container with partial policy specified. + const containerDefinition02: ContainerDefinition = { + id: "TestCreateDefaultPolicy02", + indexingPolicy: { + indexingMode: IndexingMode.lazy, + automatic: true + } + }; + + const { body: containerWithPartialPolicyDef } = await database.containers.create(containerDefinition02); + checkDefaultIndexingPolicyPaths((containerWithPartialPolicyDef as any)["indexingPolicy"]); + + // create container with default policy. + const containerDefinition03 = { + id: "TestCreateDefaultPolicy03", + indexingPolicy: {} + }; + const { body: containerDefaultPolicy } = await database.containers.create(containerDefinition03); + checkDefaultIndexingPolicyPaths((containerDefaultPolicy as any)["indexingPolicy"]); + + // create container with indexing policy missing indexes. + const containerDefinition04 = { + id: "TestCreateDefaultPolicy04", + indexingPolicy: { + includedPaths: [ + { + path: "/*" + } + ] + } + }; + const { body: containerMissingIndexes } = await database.containers.create(containerDefinition04); + checkDefaultIndexingPolicyPaths((containerMissingIndexes as any)["indexingPolicy"]); + + // create container with indexing policy missing precision. + const containerDefinition05 = { + id: "TestCreateDefaultPolicy05", + indexingPolicy: { + includedPaths: [ + { + path: "/*", + indexes: [ + { + kind: IndexKind.Hash, + dataType: DataType.String + }, + { + kind: IndexKind.Range, + dataType: DataType.Number + } + ] + } + ] + } + }; + const { body: containerMissingPrecision } = await database.containers.create(containerDefinition05); + checkDefaultIndexingPolicyPaths((containerMissingPrecision as any)["indexingPolicy"]); + } catch (err) { + throw err; + } + }; + + it("nativeApi Should create container with default indexing policy name based", async function() { + try { + await defaultIndexingPolicyTest(); + } catch (err) { + throw err; + } + }); + }); + + describe("Validate response headers", function() { + const createThenReadcontainer = async function(database: Database, body: ContainerDefinition) { + try { + const { body: createdcontainer, headers } = await database.containers.create(body); + const response = await database.container(createdcontainer.id).read(); + return response; + } catch (err) { + throw err; + } + }; + + const indexProgressHeadersTest = async function() { + try { + const database = await getTestDatabase("Validate response headers"); + const { headers: headers1 } = await createThenReadcontainer(database, { id: "consistent_coll" }); + assert.notEqual(headers1[Constants.HttpHeaders.IndexTransformationProgress], undefined); + assert.equal(headers1[Constants.HttpHeaders.LazyIndexingProgress], undefined); + + const lazyContainerDefinition = { + id: "lazy_coll", + indexingPolicy: { indexingMode: DocumentBase.IndexingMode.lazy } + }; + const { headers: headers2 } = await createThenReadcontainer(database, lazyContainerDefinition); + assert.notEqual(headers2[Constants.HttpHeaders.IndexTransformationProgress], undefined); + assert.notEqual(headers2[Constants.HttpHeaders.LazyIndexingProgress], undefined); + + const noneContainerDefinition = { + id: "none_coll", + indexingPolicy: { indexingMode: DocumentBase.IndexingMode.none, automatic: false } + }; + const { headers: headers3 } = await createThenReadcontainer(database, noneContainerDefinition); + assert.notEqual(headers3[Constants.HttpHeaders.IndexTransformationProgress], undefined); + assert.equal(headers3[Constants.HttpHeaders.LazyIndexingProgress], undefined); + } catch (err) { + throw err; + } + }; + + it("nativeApi Validate index progress headers name based", async function() { + try { + await indexProgressHeadersTest(); + } catch (err) { + throw err; + } + }); + }); +}); + +describe("containers.createIfNotExists", function() { + let database: Database; + before(async function() { + // create database + database = await getTestDatabase("containers.createIfNotExists"); + }); + + it("should handle container does not exist", async function() { + const def: ContainerDefinition = { id: "does not exist" }; + const { container } = await database.containers.createIfNotExists(def); + const { body: readDef } = await container.read(); + assert.equal(def.id, readDef.id); + }); + + it("should handle container exists", async function() { + const def: ContainerDefinition = { id: "does exist" }; + await database.containers.create(def); + + const { container } = await database.containers.createIfNotExists(def); + const { body: readDef } = await container.read(); + assert.equal(def.id, readDef.id); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/database.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/database.spec.ts new file mode 100644 index 000000000000..b1ed9bc736de --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/database.spec.ts @@ -0,0 +1,131 @@ +import assert from "assert"; +import { CosmosClient, DatabaseDefinition } from "../.."; +import { endpoint, masterKey } from "../common/_testConfig"; +import { addEntropy, removeAllDatabases } from "../common/TestHelpers"; + +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + await removeAllDatabases(); + }); + + describe("Validate Database CRUD", async function() { + const databaseCRUDTest = async function() { + // read databases + const { result: databases } = await client.databases.readAll().toArray(); + assert.equal(databases.constructor, Array, "Value should be an array"); + + // create a database + const beforeCreateDatabasesCount = databases.length; + const databaseDefinition = { id: "database test database" }; + const { body: db } = await client.databases.create(databaseDefinition); + assert.equal(db.id, databaseDefinition.id); + + // read databases after creation + const { result: databases2 } = await client.databases.readAll().toArray(); + assert.equal(databases2.length, beforeCreateDatabasesCount + 1, "create should increase the number of databases"); + // query databases + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: databaseDefinition.id + } + ] + }; + const { result: results } = await client.databases.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + // delete database + await client.database(db.id).delete(); + try { + // read database after deletion + await client.database(db.id).read(); + assert.fail("Read database on non-existent database should fail"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }; + + it("nativeApi Should do database CRUD operations successfully name based", async function() { + await databaseCRUDTest(); + }); + + describe("databases.createIfNotExists", function() { + it("should handle does not exist", async function() { + const def: DatabaseDefinition = { id: addEntropy("does not exist") }; + const { database } = await client.databases.createIfNotExists(def); + const { body: readDef } = await database.read(); + assert.equal(def.id, readDef.id); + }); + + it("should handle does exist", async function() { + const def: DatabaseDefinition = { id: addEntropy("does exist") }; + // Set up + await client.databases.create(def); + + // Now call createIfNotExists on existing db + const { database } = await client.databases.createIfNotExists(def); + const { body: readDef } = await database.read(); + assert.equal(def.id, readDef.id); + }); + }); + }); + + // TODO: These are unit tests, not e2e tests like above, so maybe should seperate these. + describe("Validate Id validation", function() { + it("nativeApi Should fail on ends with a space", async function() { + // Id shoudn't end with a space. + try { + await client.databases.create({ id: "id_ends_with_space " }); + assert.fail("Must throw if id ends with a space"); + } catch (err) { + assert.equal("Id ends with a space.", err.message); + } + }); + + it("nativeAPI Should fail on contains '/'", async function() { + // Id shoudn't contain "/". + try { + await client.databases.create({ id: "id_with_illegal/_char" }); + assert.fail("Must throw if id has illegal characters"); + } catch (err) { + assert.equal("Id contains illegal chars.", err.message); + } + }); + + it("nativeAPI Should fail on contains '\\'", async function() { + // Id shoudn't contain "\\". + try { + await client.databases.create({ id: "id_with_illegal\\_char" }); + assert.fail("Must throw if id contains illegal characters"); + } catch (err) { + assert.equal("Id contains illegal chars.", err.message); + } + }); + + it("nativeAPI Should fail on contains '?'", async function() { + // Id shoudn't contain "?". + try { + await client.databases.create({ id: "id_with_illegal?_?char" }); + assert.fail("Must throw if id contains illegal characters"); + } catch (err) { + assert.equal("Id contains illegal chars.", err.message); + } + }); + + it("nativeAPI should fail on contains '#'", async function() { + // Id shoudn't contain "#". + try { + await client.databases.create({ id: "id_with_illegal#_char" }); + assert.fail("Must throw if id contains illegal characters"); + } catch (err) { + assert.equal("Id contains illegal chars.", err.message); + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/databaseaccount.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/databaseaccount.spec.ts new file mode 100644 index 000000000000..fea091b1c7ef --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/databaseaccount.spec.ts @@ -0,0 +1,29 @@ +import assert from "assert"; +import { CosmosClient } from "../.."; +import { endpoint, masterKey } from "../common/_testConfig"; +import { removeAllDatabases } from "../common/TestHelpers"; + +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + await removeAllDatabases(); + }); + + describe("validate database account functionality", function() { + const databaseAccountTest = async function() { + const { body: databaseAccount, headers } = await client.getDatabaseAccount(); + assert.equal(databaseAccount.DatabasesLink, "/dbs/"); + assert.equal(databaseAccount.MediaLink, "/media/"); + assert.equal(databaseAccount.MaxMediaStorageUsageInMB, headers["x-ms-max-media-storage-usage-mb"]); // TODO: should use constants here + assert.equal(databaseAccount.CurrentMediaStorageUsageInMB, headers["x-ms-media-storage-usage-mb"]); + assert(databaseAccount.ConsistencyPolicy !== undefined); + }; + + it("nativeApi Should get database account successfully name based", async function() { + await databaseAccountTest(); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/item.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/item.spec.ts new file mode 100644 index 000000000000..75654fcddd3a --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/item.spec.ts @@ -0,0 +1,195 @@ +import assert from "assert"; +import { Container, DocumentBase } from "../.."; +import { ItemDefinition } from "../../client"; +import { + bulkDeleteItems, + bulkInsertItems, + bulkQueryItemsWithPartitionKey, + bulkReadItems, + bulkReplaceItems, + createOrUpsertItem, + getTestDatabase, + removeAllDatabases, + replaceOrUpsertItem +} from "../common/TestHelpers"; + +/** + * @ignore + * @hidden + */ +interface TestItem { + id?: string; + name?: string; + foo?: string; + key?: string; + replace?: string; +} + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + await removeAllDatabases(); + }); + + describe("Validate Document CRUD", function() { + const documentCRUDTest = async function(isUpsertTest: boolean) { + // create database + const database = await getTestDatabase("sample 中文 database"); + // create container + const { body: containerdef } = await database.containers.create({ id: "sample container" }); + const container: Container = database.container(containerdef.id); + + // read items + const { result: items } = await container.items.readAll().toArray(); + assert(Array.isArray(items), "Value should be an array"); + + // create an item + const beforeCreateDocumentsCount = items.length; + const itemDefinition: TestItem = { + name: "sample document", + foo: "bar", + key: "value", + replace: "new property" + }; + try { + await createOrUpsertItem(container, itemDefinition, { disableAutomaticIdGeneration: true }, isUpsertTest); + assert.fail("id generation disabled must throw with invalid id"); + } catch (err) { + assert(err !== undefined, "should throw an error because automatic id generation is disabled"); + } + const { body: document } = await createOrUpsertItem(container, itemDefinition, undefined, isUpsertTest); + assert.equal(document.name, itemDefinition.name); + assert(document.id !== undefined); + // read documents after creation + const { result: documents2 } = await container.items.readAll().toArray(); + assert.equal(documents2.length, beforeCreateDocumentsCount + 1, "create should increase the number of documents"); + // query documents + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: document.id + } + ] + }; + const { result: results } = await container.items.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + const { result: results2 } = await container.items.query(querySpec, { enableScanInQuery: true }).toArray(); + assert(results2.length > 0, "number of results for the query should be > 0"); + + // replace document + document.name = "replaced document"; + document.foo = "not bar"; + const { body: replacedDocument } = await replaceOrUpsertItem(container, document, undefined, isUpsertTest); + assert.equal(replacedDocument.name, "replaced document", "document name property should change"); + assert.equal(replacedDocument.foo, "not bar", "property should have changed"); + assert.equal(document.id, replacedDocument.id, "document id should stay the same"); + // read document + const { body: document2 } = await container.item(replacedDocument.id).read(); + assert.equal(replacedDocument.id, document2.id); + // delete document + const { body: res } = await container.item(replacedDocument.id).delete(); + + // read documents after deletion + try { + const { body: document3 } = await container.item(replacedDocument.id).read(); + assert.fail("must throw if document doesn't exist"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }; + + const documentCRUDMultiplePartitionsTest = async function() { + // create database + const database = await getTestDatabase("db1"); + const partitionKey = "key"; + + // create container + const containerDefinition = { + id: "coll1", + partitionKey: { paths: ["/" + partitionKey], kind: DocumentBase.PartitionKind.Hash } + }; + + const { body: containerdef } = await database.containers.create(containerDefinition, { offerThroughput: 12000 }); + const container = database.container(containerdef.id); + + const documents = [ + { id: "document1" }, + { id: "document2", key: null, prop: 1 }, + { id: "document3", key: false, prop: 1 }, + { id: "document4", key: true, prop: 1 }, + { id: "document5", key: 1, prop: 1 }, + { id: "document6", key: "A", prop: 1 } + ]; + + let returnedDocuments = await bulkInsertItems(container, documents); + + assert.equal(returnedDocuments.length, documents.length); + returnedDocuments.sort(function(doc1, doc2) { + return doc1.id.localeCompare(doc2.id); + }); + await bulkReadItems(container, returnedDocuments, partitionKey); + const { result: successDocuments } = await container.items.readAll().toArray(); + assert(successDocuments !== undefined, "error reading documents"); + assert.equal( + successDocuments.length, + returnedDocuments.length, + "Expected " + returnedDocuments.length + " documents to be succesfully read" + ); + successDocuments.sort(function(doc1, doc2) { + return doc1.id.localeCompare(doc2.id); + }); + assert.equal( + JSON.stringify(successDocuments), + JSON.stringify(returnedDocuments), + "Unexpected documents are returned" + ); + + returnedDocuments.forEach(function(document) { + ++document.prop; + }); + const newReturnedDocuments = await bulkReplaceItems(container, returnedDocuments); + returnedDocuments = newReturnedDocuments; + await bulkQueryItemsWithPartitionKey(container, returnedDocuments, partitionKey); + const querySpec = { + query: "SELECT * FROM Root" + }; + try { + const { result: badUpdate } = await container.items.query(querySpec, { enableScanInQuery: true }).toArray(); + assert.fail("Must fail"); + } catch (err) { + const badRequestErrorCode = 400; + assert.equal(err.code, badRequestErrorCode, "response should return error code " + badRequestErrorCode); + } + const { result: results } = await container.items + .query(querySpec, { enableScanInQuery: true, enableCrossPartitionQuery: true }) + .toArray(); + assert(results !== undefined, "error querying documents"); + results.sort(function(doc1, doc2) { + return doc1.id.localeCompare(doc2.id); + }); + assert.equal( + results.length, + returnedDocuments.length, + "Expected " + returnedDocuments.length + " documents to be succesfully queried" + ); + assert.equal(JSON.stringify(results), JSON.stringify(returnedDocuments), "Unexpected query results"); + + await bulkDeleteItems(container, returnedDocuments, partitionKey); + }; + + it("nativeApi Should do document CRUD operations successfully name based", async function() { + await documentCRUDTest(false); + }); + + it("nativeApi Should do document CRUD operations successfully name based with upsert", async function() { + await documentCRUDTest(true); + }); + + it("nativeApi Should do document CRUD operations over multiple partitions", async function() { + await documentCRUDMultiplePartitionsTest(); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/offer.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/offer.spec.ts new file mode 100644 index 000000000000..2d7a546e9938 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/offer.spec.ts @@ -0,0 +1,119 @@ +import assert from "assert"; +import { Constants, CosmosClient } from "../.."; +import { endpoint, masterKey } from "../common/_testConfig"; +import { getEntropy, getTestContainer, removeAllDatabases } from "../common/TestHelpers"; + +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +const validateOfferResponseBody = function(offer: any) { + assert(offer.id, "Id cannot be null"); + assert(offer._rid, "Resource Id (Rid) cannot be null"); + assert(offer._self, "Self Link cannot be null"); + assert(offer.resource, "Resource Link cannot be null"); + assert(offer._self.indexOf(offer.id) !== -1, "Offer id not contained in offer self link."); +}; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + + beforeEach(async function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + await removeAllDatabases(); + }); + + describe("Validate Offer CRUD", function() { + it("nativeApi Should do offer read and query operations successfully name based single partition collection", async function() { + const mbInBytes = 1024 * 1024; + const offerThroughput = 400; + const container = await getTestContainer("Validate Offer CRUD"); + + const { headers } = await container.read({ populateQuotaInfo: true }); + + // Validate the collection size quota + assert.notEqual(headers[Constants.HttpHeaders.MaxResourceQuota], null); + assert.notEqual(headers[Constants.HttpHeaders.MaxResourceQuota], ""); + const collectionSize: number = Number( + (headers[Constants.HttpHeaders.MaxResourceQuota] as string).split(";").reduce((map: any, obj: string) => { + const items = obj.split("="); + map[items[0]] = items[1]; + return map; + }, {})[Constants.Quota.CollectionSize] + ); + assert.equal(collectionSize, 10 * mbInBytes, "Collection size is unexpected"); + + const { result: offers } = await client.offers.readAll().toArray(); + assert.equal(offers.length, 1); + const expectedOffer = offers[0]; + assert.equal( + expectedOffer.content.offerThroughput, + offerThroughput, + "Expected offerThroughput to be " + offerThroughput + ); + validateOfferResponseBody(expectedOffer); + + // Read the offer + const { body: readOffer } = await client.offer(expectedOffer.id).read(); + validateOfferResponseBody(readOffer); + // Check if the read offer is what we expected. + assert.equal(expectedOffer.id, readOffer.id); + assert.equal(expectedOffer._rid, readOffer._rid); + assert.equal(expectedOffer._self, readOffer._self); + assert.equal(expectedOffer.resource, readOffer.resource); + + // Query for offer. + const querySpec = { + query: "select * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: expectedOffer.id + } + ] + }; + const { result: offers2 } = await client.offers.query(querySpec).toArray(); + assert.equal(offers2.length, 1); + const oneOffer = offers2[0]; + validateOfferResponseBody(oneOffer); + // Now delete the collection. + await container.delete(); + // read offer after deleting collection. + try { + await client.offer(expectedOffer.id).read(); + assert.fail("Must throw after delete"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }); + + it("nativeApi Should do offer replace operations successfully name based", async function() { + const container = await getTestContainer("Validate Offer CRUD"); + const { result: offers } = await client.offers.readAll().toArray(); + assert.equal(offers.length, 1); + const expectedOffer = offers[0]; + validateOfferResponseBody(expectedOffer); + // Replace the offer. + const offerToReplace = Object.assign({}, expectedOffer); + const oldThroughput = offerToReplace.content.offerThroughput; + offerToReplace.content.offerThroughput = oldThroughput + 100; + const { body: replacedOffer } = await client.offer(offerToReplace.id).replace(offerToReplace); + validateOfferResponseBody(replacedOffer); + // Check if the replaced offer is what we expect. + assert.equal(replacedOffer.id, offerToReplace.id); + assert.equal(replacedOffer._rid, offerToReplace._rid); + assert.equal(replacedOffer._self, offerToReplace._self); + assert.equal(replacedOffer.resource, offerToReplace.resource); + assert.equal(replacedOffer.content.offerThroughput, offerToReplace.content.offerThroughput); + // Replace an offer with a bad id. + try { + const offerBadId = Object.assign({}, offerToReplace); + offerBadId._rid = "NotAllowed"; + await client.offer(offerBadId._self).replace(offerBadId); + assert.fail("Must throw after replace with bad id"); + } catch (err) { + const badRequestErrorCode = 400; + assert.equal(err.code, badRequestErrorCode); + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/offer.spec.ts.ignore b/sdk/cosmosdb/cosmos/src/test/functional/offer.spec.ts.ignore new file mode 100644 index 000000000000..6c118eca4f64 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/offer.spec.ts.ignore @@ -0,0 +1,301 @@ +/* Offer is going to be moved to the resources themselves, not a first class top level object */ + + + +import assert from "assert"; +import { Base, Constants, CosmosClient } from "../../"; +import testConfig from "./../common/_testConfig"; +import { TestHelpers } from "./../common/TestHelpers"; +import { OfferDefinition } from "../../client"; + +const endpoint = testConfig.host; +const masterKey = testConfig.masterKey; +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +describe("NodeJS CRUD Tests", function () { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + // remove all databases from the endpoint before each test + beforeEach(async function () { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + await TestHelpers.removeAllDatabases(); + }); + + describe("Validate Offer CRUD", function () { + const validateOfferResponseBody = function ( + offer: any, expectedCollLink: string, expectedOfferType: string) { + assert(offer.id, "Id cannot be null"); + assert(offer._rid, "Resource Id (Rid) cannot be null"); + assert(offer._self, "Self Link cannot be null"); + assert(offer.resource, "Resource Link cannot be null"); + assert(offer._self.indexOf(offer.id) !== -1, "Offer id not contained in offer self link."); + assert.equal(expectedCollLink.replace(/^\/|\/$/g, ""), offer.resource.replace(/^\/|\/$/g, "")); + if (expectedOfferType) { + assert.equal(expectedOfferType, offer.offerType); + } + }; + + const offerReadAndQueryTest = async function ( + isPartitionedCollection: boolean, offerThroughput: number, expectedCollectionSize: number) { + + const collectionRequestOptions = { offerThroughput }; + let collectionDefinition: any = ""; + if (isPartitionedCollection) { + collectionDefinition = { + id: Base.generateGuidId(), + indexingPolicy: { + includedPaths: [ + { + path: "/", + indexes: [ + { + kind: "Range", + dataType: "Number", + }, + { + kind: "Range", + dataType: "String", + }, + ], + }, + ], + }, + partitionKey: { + paths: [ + "/id", + ], + kind: "Hash", + }, + }; + } else { + collectionDefinition = { id: "sample collection" }; + } + const container = await TestHelpers.getTestContainer( + client, "Validate Offer CRUD", collectionDefinition, collectionRequestOptions); + + const { result: createdContainerDef, headers } = await container.read({ populateQuotaInfo: true }); + + // Validate the collection size quota + assert.notEqual(headers[Constants.HttpHeaders.MaxResourceQuota], null); + assert.notEqual(headers[Constants.HttpHeaders.MaxResourceQuota], ""); + const collectionSize: number = Number((headers[Constants.HttpHeaders.MaxResourceQuota] as string).split(";") + .reduce((map: any, obj: string) => { + const items = obj.split("="); + map[items[0]] = items[1]; + return map; + }, {})[Constants.Quota.CollectionSize]); + assert.equal(collectionSize, expectedCollectionSize, "Collection size is unexpected"); + + const { result: offers } = await client.offers.read().toArray(); + assert.equal(offers.length, 1); + const expectedOffer = offers[0]; + assert.equal(expectedOffer.content.offerThroughput, collectionRequestOptions.offerThroughput, + "Expected offerThroughput to be " + collectionRequestOptions.offerThroughput); + validateOfferResponseBody(expectedOffer, createdContainerDef._self, undefined); + + // Read the offer + const { result: readOffer } = await client.readOffer(expectedOffer._self); + validateOfferResponseBody(readOffer, createdContainerDef._self, undefined); + // Check if the read offer is what we expected. + assert.equal(expectedOffer.id, readOffer.id); + assert.equal(expectedOffer._rid, readOffer._rid); + assert.equal(expectedOffer._self, readOffer._self); + assert.equal(expectedOffer.resource, readOffer.resource); + // Read offer with a bad offer link. + try { + const badLink = expectedOffer._self.substring(0, expectedOffer._self.length - 1) + "x/"; + await client.readOffer(badLink); + assert.fail("Must throw after read with bad offer"); + } catch (err) { + const notFoundErrorCode = 400; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + // Query for offer. + const querySpec = { + query: "select * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: expectedOffer.id, + }, + ], + }; + const { result: offers2 } = await client.queryOffers(querySpec).toArray(); + assert.equal(offers2.length, 1); + const oneOffer = offers2[0]; + validateOfferResponseBody(oneOffer, createdContainerDef._self, undefined); + // Now delete the collection. + await client.deleteCollection( + TestHelpers.getCollectionLink(isNameBased, db, createdContainerDef)); + // read offer after deleting collection. + try { + await client.readOffer(expectedOffer._self); + assert.fail("Must throw after delete"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }; + + const mbInBytes = 1024 * 1024; + const offerThroughputSinglePartitionCollection = 5000; + const minOfferThroughputPCollectionWithMultiPartitions = 2000; + const maxOfferThroughputPCollectionWithSinglePartition = minOfferThroughputPCollectionWithMultiPartitions - 100; + + it.skip("nativeApi Should do offer read and query operations successfully name based single partition collection", async function () { + try { + await offerReadAndQueryTest(true, false, offerThroughputSinglePartitionCollection, mbInBytes); + } catch (err) { + throw err; + } + }); + + it.skip("nativeApi Should do offer read and query operations successfully rid based single partition collection", async function () { + try { + await offerReadAndQueryTest(false, false, offerThroughputSinglePartitionCollection, mbInBytes); + } catch (err) { + throw err; + } + }); + + it.skip("nativeApi Should do offer read and query operations successfully w/ name based p-Collection w/ 1 partition", async function () { + try { + await offerReadAndQueryTest(true, true, maxOfferThroughputPCollectionWithSinglePartition, mbInBytes); + } catch (err) { + throw err; + } + }); + + it.skip("nativeApi Should do offer read and query operations successfully w/ rid based p-Collection w/ 1 partition", async function () { + try { + await offerReadAndQueryTest(false, true, maxOfferThroughputPCollectionWithSinglePartition, mbInBytes); + } catch (err) { + throw err; + } + }); + + it.skip("nativeApi Should do offer read and query operations successfully w/ name based p-Collection w/ multi partitions", async function () { + try { + await offerReadAndQueryTest(true, true, minOfferThroughputPCollectionWithMultiPartitions, 5 * mbInBytes); + } catch (err) { + throw err; + } + }); + + it.skip("nativeApi Should do offer read and query operations successfully w/ rid based p-Collection w/ multi partitions", async function () { + try { + await offerReadAndQueryTest(false, true, minOfferThroughputPCollectionWithMultiPartitions, 5 * mbInBytes); + } catch (err) { + throw err; + } + }); + + const offerReplaceTest = async function (isNameBased: boolean) { + try { + const client = new CosmosClient(endpoint, { masterKey }); + // create database + const { result: db } = await client.createDatabase({ id: "sample database" }); + // create collection + const { result: collection } = await client.createCollection( + TestHelpers.getDatabaseLink(isNameBased, db), { id: "sample collection" }); + const { result: offers } = await client.readOffers().toArray(); + assert.equal(offers.length, 1); + const expectedOffer = offers[0]; + validateOfferResponseBody(expectedOffer, collection._self, undefined); + // Replace the offer. + const offerToReplace = Base.extend({}, expectedOffer); + const oldThroughput = offerToReplace.content.offerThroughput; + offerToReplace.content.offerThroughput = oldThroughput + 100; + const { result: replacedOffer } = await client.replaceOffer(offerToReplace._self, offerToReplace); + validateOfferResponseBody(replacedOffer, collection._self, undefined); + // Check if the replaced offer is what we expect. + assert.equal(replacedOffer.id, offerToReplace.id); + assert.equal(replacedOffer._rid, offerToReplace._rid); + assert.equal(replacedOffer._self, offerToReplace._self); + assert.equal(replacedOffer.resource, offerToReplace.resource); + assert.equal(replacedOffer.content.offerThroughput, offerToReplace.content.offerThroughput); + // Replace an offer with a bad id. + try { + const offerBadId = Base.extend({}, offerToReplace); + offerBadId._rid = "NotAllowed"; + await client.replaceOffer(offerBadId._self, offerBadId); + assert.fail("Must throw after replace with bad id"); + } catch (err) { + const badRequestErrorCode = 400; + assert.equal(err.code, badRequestErrorCode); + } + // Replace an offer with a bad rid. + try { + const offerBadRid = Base.extend({}, offerToReplace); + offerBadRid._rid = "InvalidRid"; + await client.replaceOffer(offerBadRid._self, offerBadRid); + assert.fail("Must throw after replace with bad rid"); + } catch (err) { + const badRequestErrorCode = 400; + assert.equal(err.code, badRequestErrorCode); + } + // Replace an offer with null id and rid. + try { + const offerNullId = Base.extend({}, offerToReplace); + offerNullId.id = undefined; + offerNullId._rid = undefined; + await client.replaceOffer(offerNullId._self, offerNullId); + assert.fail("Must throw after repalce with null id and rid"); + } catch (err) { + const badRequestErrorCode = 400; + assert.equal(err.code, badRequestErrorCode); + } + } catch (err) { + throw err; + } + }; + + it("nativeApi Should do offer replace operations successfully name based", async function () { + try { + await offerReplaceTest(true); + } catch (err) { + throw err; + } + }); + + it("nativeApi Should do offer replace operations successfully rid based", async function () { + try { + await offerReplaceTest(false); + } catch (err) { + throw err; + } + }); + + const createCollectionWithOfferTypeTest = async function (isNameBased: boolean) { + try { + const client = new CosmosClient(endpoint, { masterKey }); + // create database + const { result: db } = await client.createDatabase({ id: "sample database" }); + // create collection + const { result: collection } = await client.createCollection( + TestHelpers.getDatabaseLink(isNameBased, db), { id: "sample collection" }, { offerType: "S2" }); + const { result: offers } = await client.readOffers().toArray(); + assert.equal(offers.length, 1); + const expectedOffer = offers[0]; + assert.equal(expectedOffer.offerType, "S2"); + } catch (err) { + throw err; + } + }; + + it("nativeApi Should create collection with specified offer type successfully name based", async function () { + try { + await createCollectionWithOfferTypeTest(true); + } catch (err) { + throw err; + } + }); + + it("nativeApi Should create collection with specified offer type successfully rid based", async function () { + try { + await createCollectionWithOfferTypeTest(false); + } catch (err) { + throw err; + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/permission.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/permission.spec.ts new file mode 100644 index 000000000000..1e6239949477 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/permission.spec.ts @@ -0,0 +1,239 @@ +import assert from "assert"; +import { DocumentBase } from "../.."; +import { PermissionDefinition } from "../../client"; +import { + createOrUpsertPermission, + getTestContainer, + removeAllDatabases, + replaceOrUpsertPermission +} from "../common/TestHelpers"; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + await removeAllDatabases(); + }); + describe("Validate Permission CRUD", function() { + const permissionCRUDTest = async function(isUpsertTest: boolean) { + try { + // create container & database + const container = await getTestContainer("Validate Permission Crud"); + + // create user + const { body: userDef } = await container.database.users.create({ id: "new user" }); + const user = container.database.user(userDef.id); + // list permissions + const { result: permissions } = await user.permissions.readAll().toArray(); + assert.equal(permissions.constructor, Array, "Value should be an array"); + const beforeCreateCount = permissions.length; + const permissionDef: PermissionDefinition = { + id: "new permission", + permissionMode: DocumentBase.PermissionMode.Read, + resource: container.url + }; + + // create permission + const { body: createdPermission } = await createOrUpsertPermission( + user, + permissionDef, + undefined, + isUpsertTest + ); + let permission = user.permission(createdPermission.id); + assert.equal(createdPermission.id, "new permission", "permission name error"); + + // list permissions after creation + const { result: permissionsAfterCreation } = await user.permissions.readAll().toArray(); + assert.equal(permissionsAfterCreation.length, beforeCreateCount + 1); + + // query permissions + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: permissionDef.id + } + ] + }; + const { result: results } = await user.permissions.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + permissionDef.permissionMode = DocumentBase.PermissionMode.All; + const { body: replacedPermission } = await replaceOrUpsertPermission( + user, + permissionDef, + undefined, + isUpsertTest + ); + assert.equal( + replacedPermission.permissionMode, + DocumentBase.PermissionMode.All, + "permission mode should change" + ); + assert.equal(permissionDef.id, replacedPermission.id, "permission id should stay the same"); + + // to change the id of an existing resourcewe have to use replace + permissionDef.id = "replaced permission"; + const { body: replacedPermission2 } = await permission.replace(permissionDef); + assert.equal(replacedPermission2.id, "replaced permission", "permission name should change"); + assert.equal(permissionDef.id, replacedPermission2.id, "permission id should stay the same"); + permission = user.permission(replacedPermission2.id); + + // read permission + const { body: permissionAfterReplace } = await permission.read(); + assert.equal(permissionAfterReplace.id, permissionDef.id); + + // delete permission + const { body: res } = await permission.delete(); + + // read permission after deletion + try { + await permission.read(); + assert.fail("Must fail to read permission after deletion"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + } catch (err) { + throw err; + } + }; + + const permissionCRUDOverMultiplePartitionsTest = async function(isUpsertTest: boolean) { + try { + // create database + // create container + const partitionKey = "id"; + const containerDefinition = { + id: "coll1", + partitionKey: { paths: ["/" + partitionKey], kind: DocumentBase.PartitionKind.Hash } + }; + const container = await getTestContainer( + "permission CRUD over multiple partitions", + undefined, + containerDefinition + ); + + // create user + const { body: userDef } = await container.database.users.create({ id: "new user" }); + const user = container.database.user(userDef.id); + + // list permissions + const { result: permissions } = await user.permissions.readAll().toArray(); + assert(Array.isArray(permissions), "Value should be an array"); + const beforeCreateCount = permissions.length; + const permissionDefinition = { + id: "new permission", + permissionMode: DocumentBase.PermissionMode.Read, + resource: container.url, + resourcePartitionKey: [1] + }; + + // create permission + const response = await createOrUpsertPermission(user, permissionDefinition, undefined, isUpsertTest); + const permissionDef = response.body; + let permission = user.permission(permissionDef.id); + assert.equal(permissionDef.id, permissionDefinition.id, "permission name error"); + assert.equal( + JSON.stringify(permissionDef.resourcePartitionKey), + JSON.stringify(permissionDefinition.resourcePartitionKey), + "permission resource partition key error" + ); + + // list permissions after creation + const { result: permissionsAfterCreation } = await user.permissions.readAll().toArray(); + assert.equal(permissionsAfterCreation.length, beforeCreateCount + 1); + + // query permissions + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: permissionDef.id + } + ] + }; + const { result: results } = await user.permissions.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + // Replace permission + permissionDef.permissionMode = DocumentBase.PermissionMode.All; + const { body: replacedPermission } = await replaceOrUpsertPermission( + user, + permissionDef, + undefined, + isUpsertTest + ); + assert.equal( + replacedPermission.permissionMode, + DocumentBase.PermissionMode.All, + "permission mode should change" + ); + assert.equal(replacedPermission.id, permissionDef.id, "permission id should stay the same"); + assert.equal( + JSON.stringify(replacedPermission.resourcePartitionKey), + JSON.stringify(permissionDef.resourcePartitionKey), + "permission resource partition key error" + ); + + // to change the id of an existing resourcewe have to use replace + permissionDef.id = "replaced permission"; + const { body: replacedPermission2 } = await permission.replace(permissionDef); + assert.equal(replacedPermission2.id, permissionDef.id); + permission = user.permission(replacedPermission2.id); + + // read permission + const { body: permissionAfterReplace } = await permission.read(); + assert.equal(permissionAfterReplace.id, replacedPermission2.id); + + // delete permission + const { body: res } = await permission.delete(); + + // read permission after deletion + try { + await permission.read(); + assert.fail("Must throw on read after delete"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + } catch (err) { + throw err; + } + }; + + it("nativeApi Should do Permission CRUD operations successfully name based", async function() { + try { + await permissionCRUDTest(false); + } catch (err) { + throw err; + } + }); + + it("nativeApi Should do Permission CRUD operations successfully name based with upsert", async function() { + try { + await permissionCRUDTest(true); + } catch (err) { + throw err; + } + }); + + it("nativeApi Should do Permission CRUD operations over multiple partitions successfully name based", async function() { + try { + await permissionCRUDOverMultiplePartitionsTest(false); + } catch (err) { + throw err; + } + }); + + it("nativeApi Should do Permission CRUD operations over multiple partitions successfully with upsert", async function() { + try { + await permissionCRUDOverMultiplePartitionsTest(true); + } catch (err) { + throw err; + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/query.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/query.spec.ts new file mode 100644 index 000000000000..7b94b8ae4b09 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/query.spec.ts @@ -0,0 +1,224 @@ +import assert from "assert"; +import { Constants, CosmosClient, DocumentBase } from "../.."; +import { Container } from "../../client"; +import { endpoint, masterKey } from "../common/_testConfig"; +import { bulkInsertItems, getTestContainer, getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +// TODO: This is required for Node 6 and above, so just putting it in here. +// Might want to decide on only supporting async iterators once Node supports them officially. +if (!Symbol || !Symbol.asyncIterator) { + (Symbol as any).asyncIterator = Symbol.for("Symbol.asyncIterator"); +} + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + before(async function() { + await removeAllDatabases(); + }); + + describe("Validate Queries CRUD", function() { + const queriesCRUDTest = async function() { + try { + // create a database + const database = await getTestDatabase("query test database"); + // query databases + const querySpec0 = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: database.id + } + ] + }; + const { result: results } = await client.databases.query(querySpec0).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + const querySpec1 = { + query: "SELECT * FROM root r WHERE r.id='" + database.id + "'" + }; + const { result: results2 } = await client.databases.query(querySpec1).toArray(); + assert(results2.length > 0, "number of results for the query should be > 0"); + const querySpec2 = "SELECT * FROM root r WHERE r.id='" + database.id + "'"; + const { result: results3 } = await client.databases.query(querySpec2).toArray(); + assert(results3.length > 0, "number of results for the query should be > 0"); + } catch (err) { + throw err; + } + }; + + it("nativeApi Should do queries CRUD operations successfully name based", async function() { + try { + await queriesCRUDTest(); + } catch (err) { + throw err; + } + }); + }); + + describe("Validate QueryIterator Functionality For Multiple Partition container", function() { + const documentDefinitions = [ + { id: "document1" }, + { id: "document2", key: null, prop: 1 }, + { id: "document3", key: false, prop: 1 }, + { id: "document4", key: true, prop: 1 }, + { id: "document5", key: 1, prop: 1 }, + { id: "document6", key: "A", prop: 1 } + ]; + + let container: Container; + + // creates a new database, creates a new collecton, bulk inserts documents to the container + beforeEach(async function() { + const partitionKey = "key"; + const containerDefinition = { + id: "coll1", + partitionKey: { + paths: ["/" + partitionKey], + kind: DocumentBase.PartitionKind.Hash + } + }; + + const containerOptions = { offerThroughput: 12000 }; + container = await getTestContainer("query CRUD database 中文", client, containerDefinition, containerOptions); + await bulkInsertItems(container, documentDefinitions); + }); + + it("nativeApi validate QueryIterator nextItem on Multiple Partition Colleciton", async function() { + // obtain an instance of queryIterator + const queryIterator = container.items.readAll(); + let cnt = 0; + while (queryIterator.hasMoreResults()) { + const { result } = await queryIterator.nextItem(); + if (result === undefined) { + break; + } + cnt++; + } + assert.equal(cnt, documentDefinitions.length); + }); + }); + + describe("Validate QueryIterator Functionality", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 30000); + let resources: { container: Container; doc1: any; doc2: any; doc3: any }; + beforeEach(async function() { + const container = await getTestContainer("Validate QueryIterator Functionality", client); + const { body: doc1 } = await container.items.create({ id: "doc1", prop1: "value1" }); + const { body: doc2 } = await container.items.create({ id: "doc2", prop1: "value2" }); + const { body: doc3 } = await container.items.create({ id: "doc3", prop1: "value3" }); + resources = { container, doc1, doc2, doc3 }; + }); + + const queryIteratorToArrayTest = async function() { + const queryIterator = resources.container.items.readAll({ maxItemCount: 2 }); + const { result: docs } = await queryIterator.toArray(); + assert.equal(docs.length, 3, "queryIterator should return all documents using continuation"); + assert.equal(docs[0].id, resources.doc1.id); + assert.equal(docs[1].id, resources.doc2.id); + assert.equal(docs[2].id, resources.doc3.id); + }; + + const queryIteratorAsyncIteratorTest = async function() { + const queryIterator = resources.container.items.readAll({ maxItemCount: 2 }); + let counter = 0; + for await (const { result: doc } of queryIterator.getAsyncIterator()) { + counter++; + if (counter === 1) { + assert.equal(doc.id, resources.doc1.id, "first document should be doc1"); + } else if (counter === 2) { + assert.equal(doc.id, resources.doc2.id, "second document should be doc2"); + } else if (counter === 3) { + assert.equal(doc.id, resources.doc3.id, "third document should be doc3"); + } + } + assert(counter === 3, "iterator should have run 3 times"); + }; + + const queryIteratorForEachTest = async function() { + const queryIterator = resources.container.items.readAll({ maxItemCount: 2 }); + let counter = 0; + await queryIterator.forEach((item, headers, index) => { + counter++; + if (index === 0) { + assert.equal(item.id, resources.doc1.id, "first document should be doc1"); + } else if (index === 1) { + assert.equal(item.id, resources.doc2.id, "second document should be doc2"); + } else if (index === 2) { + assert.equal(item.id, resources.doc3.id, "third document should be doc3"); + } + }); + assert(counter === 3, "iterator should have run 3 times"); + }; + + const queryIteratorNextAndMoreTest = async function() { + const queryIterator = resources.container.items.readAll({ maxItemCount: 2 }); + assert.equal(queryIterator.hasMoreResults(), true); + const { result: doc2 } = await queryIterator.nextItem(); + assert.equal(doc2.id, resources.doc1.id, "call queryIterator.nextItem after reset should return first document"); + const { result: doc1 } = await queryIterator.current(); + assert.equal(doc1.id, resources.doc1.id, "call queryIterator.current after reset should return first document"); + assert.equal(queryIterator.hasMoreResults(), true); + const { result: doc4 } = await queryIterator.nextItem(); + assert.equal(doc4.id, resources.doc2.id, "call queryIterator.nextItem again should return second document"); + const { result: doc3 } = await queryIterator.current(); + assert.equal(doc3.id, resources.doc2.id, "call queryIterator.current should return second document"); + assert.equal(queryIterator.hasMoreResults(), true); + const { result: doc6 } = await queryIterator.nextItem(); + assert.equal(doc6.id, resources.doc3.id, "call queryIterator.nextItem again should return third document"); + const { result: doc5 } = await queryIterator.current(); + assert.equal(doc5.id, resources.doc3.id, "call queryIterator.current should return third document"); + const { result: doc7 } = await queryIterator.nextItem(); + assert.equal(doc7, undefined, "queryIterator should return undefined if there is no elements"); + }; + + const queryIteratorExecuteNextTest = async function() { + let queryIterator = resources.container.items.readAll({ maxItemCount: 2 }); + const { result: docs, headers } = await queryIterator.executeNext(); + + assert(headers !== undefined, "executeNext should pass headers as the third parameter to the callback"); + assert(headers[Constants.HttpHeaders.RequestCharge] > 0, "RequestCharge has to be non-zero"); + assert.equal(docs.length, 2, "first batch size should be 2"); + assert.equal(docs[0].id, resources.doc1.id, "first batch first document should be doc1"); + assert.equal(docs[1].id, resources.doc2.id, "batch first second document should be doc2"); + const { result: docs2 } = await queryIterator.executeNext(); + assert.equal(docs2.length, 1, "second batch size is unexpected"); + assert.equal(docs2[0].id, resources.doc3.id, "second batch element should be doc3"); + + // validate Iterator.executeNext with continuation token + queryIterator = resources.container.items.readAll({ + maxItemCount: 2, + continuation: headers[Constants.HttpHeaders.Continuation] as string + }); + const { result: docsWithContinuation, headers: headersWithContinuation } = await queryIterator.executeNext(); + assert( + headersWithContinuation !== undefined, + "executeNext should pass headers as the third parameter to the callback" + ); + assert(headersWithContinuation[Constants.HttpHeaders.RequestCharge] > 0, "RequestCharge has to be non-zero"); + assert.equal(docsWithContinuation.length, 1, "second batch size with continuation token is unexpected"); + assert.equal(docsWithContinuation[0].id, resources.doc3.id, "second batch element should be doc3"); + }; + + it("nativeApi validate QueryIterator iterator toArray name based", async function() { + await queryIteratorToArrayTest(); + }); + + it("validate queryIterator asyncIterator", async function() { + await queryIteratorAsyncIteratorTest(); + }); + + it("validate queryIterator forEach", async function() { + await queryIteratorForEachTest(); + }); + + it("nativeApi validate queryIterator nextItem and hasMoreResults name based", async function() { + await queryIteratorNextAndMoreTest(); + }); + + it("nativeApi validate queryIterator iterator executeNext name based", async function() { + await queryIteratorExecuteNextTest(); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/spatial.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/spatial.spec.ts new file mode 100644 index 000000000000..fdc6c603dbeb --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/spatial.spec.ts @@ -0,0 +1,83 @@ +import assert from "assert"; +import { Database, DocumentBase } from "../.."; +import { createOrUpsertItem, getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + await removeAllDatabases(); + }); + + describe("Validate spatial index", function() { + const spatialIndexTest = async function(isUpsertTest: boolean) { + try { + // create database + const database: Database = await getTestDatabase("validate spatial index"); + + // create container using an indexing policy with spatial index. + const indexingPolicy = { + includedPaths: [ + { + path: '/"Location"/?', + indexes: [ + { + kind: DocumentBase.IndexKind.Spatial, + dataType: DocumentBase.DataType.Point + } + ] + }, + { + path: "/" + } + ] + }; + const entropy = Math.floor(Math.random() * 10000); + const { body: containerDef } = await database.containers.create({ + id: `sample container${entropy}`, + indexingPolicy + }); + const container = database.container(containerDef.id); + + const location1 = { + id: "location1", + Location: { + type: "Point", + coordinates: [20.0, 20.0] + } + }; + await createOrUpsertItem(container, location1, undefined, isUpsertTest); + const location2 = { + id: "location2", + Location: { + type: "Point", + coordinates: [100.0, 100.0] + } + }; + await createOrUpsertItem(container, location2, undefined, isUpsertTest); + const query = + "SELECT * FROM root WHERE (ST_DISTANCE(root.Location, {type: 'Point', coordinates: [20.1, 20]}) < 20000) "; + const { result: results } = await container.items.query(query).toArray(); + assert.equal(1, results.length); + assert.equal("location1", results[0].id); + } catch (err) { + throw err; + } + }; + + it("nativeApi Should support spatial index name based", async function() { + try { + await spatialIndexTest(false); + } catch (err) { + throw err; + } + }); + + it("nativeApi Should support spatial index name based with upsert", async function() { + try { + await spatialIndexTest(true); + } catch (err) { + throw err; + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/sproc.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/sproc.spec.ts new file mode 100644 index 000000000000..36c00aa388db --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/sproc.spec.ts @@ -0,0 +1,386 @@ +import assert from "assert"; +import { Constants, CosmosClient, DocumentBase } from "../.."; +import { Container, StoredProcedureDefinition } from "../../client"; +import { bulkInsertItems, getTestContainer, getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +// Used for sproc +declare var getContext: any; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + await removeAllDatabases(); + }); + describe("Validate sproc CRUD", function() { + let container: Container; + beforeEach(async function() { + container = await getTestContainer(this.test.fullTitle()); + }); + + it("nativeApi Should do sproc CRUD operations successfully with create/replace", async function() { + // read sprocs + const { result: sprocs } = await container.storedProcedures.readAll().toArray(); + assert.equal(sprocs.constructor, Array, "Value should be an array"); + + // create a sproc + const beforeCreateSprocsCount = sprocs.length; + const sprocDefinition: StoredProcedureDefinition = { + id: "sample sproc", + body: "function () { const x = 10; }" + }; + + const { body: sproc } = await container.storedProcedures.create(sprocDefinition); + + assert.equal(sproc.id, sprocDefinition.id); + assert.equal(sproc.body, "function () { const x = 10; }"); + + // read sprocs after creation + const { result: sprocsAfterCreation } = await container.storedProcedures.readAll().toArray(); + assert.equal( + sprocsAfterCreation.length, + beforeCreateSprocsCount + 1, + "create should increase the number of sprocs" + ); + + // query sprocs + const querySpec = { + query: "SELECT * FROM root r" + }; + const { result: queriedSprocs } = await container.storedProcedures.query(querySpec).toArray(); + assert(queriedSprocs.length > 0, "number of sprocs for the query should be > 0"); + + // replace sproc + // prettier-ignore + sproc.body = function() { const x = 20; }; + const { body: replacedSproc } = await container.storedProcedure(sproc.id).replace(sproc); + + assert.equal(replacedSproc.id, sproc.id); + assert.equal(replacedSproc.body, "function () { const x = 20; }"); + + // read sproc + const { body: sprocAfterReplace } = await container.storedProcedure(replacedSproc.id).read(); + assert.equal(replacedSproc.id, sprocAfterReplace.id); + + // delete sproc + await container.storedProcedure(replacedSproc.id).delete(); + + // read sprocs after deletion + try { + await container.storedProcedure(replacedSproc.id).read(); + assert.fail("Must fail to read sproc after deletion"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }); + + it("nativeApi Should do sproc CRUD operations successfully name based with upsert", async function() { + // read sprocs + const { result: sprocs } = await container.storedProcedures.readAll().toArray(); + assert.equal(sprocs.constructor, Array, "Value should be an array"); + + // create a sproc + const beforeCreateSprocsCount = sprocs.length; + const sprocDefinition: StoredProcedureDefinition = { + id: "sample sproc", + // prettier-ignore + body: function() { const x = 10; } // tslint:disable-line:object-literal-shorthand + }; + + const { body: sproc } = await container.storedProcedures.upsert(sprocDefinition); + + assert.equal(sproc.id, sprocDefinition.id); + assert.equal(sproc.body, "function () { const x = 10; }"); + + // read sprocs after creation + const { result: sprocsAfterCreation } = await container.storedProcedures.readAll().toArray(); + assert.equal( + sprocsAfterCreation.length, + beforeCreateSprocsCount + 1, + "create should increase the number of sprocs" + ); + + // query sprocs + const querySpec = { + query: "SELECT * FROM root r" + }; + const { result: queriedSprocs } = await container.storedProcedures.query(querySpec).toArray(); + assert(queriedSprocs.length > 0, "number of sprocs for the query should be > 0"); + + // replace sproc + // prettier-ignore + sproc.body = function() { const x = 20; }; + const { body: replacedSproc } = await container.storedProcedures.upsert(sproc); + + assert.equal(replacedSproc.id, sproc.id); + assert.equal(replacedSproc.body, "function () { const x = 20; }"); + + // read sproc + const { body: sprocAfterReplace } = await container.storedProcedure(replacedSproc.id).read(); + assert.equal(replacedSproc.id, sprocAfterReplace.id); + + // delete sproc + await container.storedProcedure(replacedSproc.id).delete(); + + // read sprocs after deletion + try { + await container.storedProcedure(replacedSproc.id).read(); + assert.fail("Must fail to read sproc after deletion"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }); + }); + + describe("Validate stored procedure functionality", function() { + let container: Container; + beforeEach(async function() { + container = await getTestContainer(this.test.fullTitle()); + }); + + it("nativeApi should do stored procedure operations successfully with create/replace", async function() { + // tslint:disable:no-var-keyword + // tslint:disable:prefer-const + // tslint:disable:curly + // tslint:disable:no-string-throw + // tslint:disable:object-literal-shorthand + const sproc1: StoredProcedureDefinition = { + id: "storedProcedure1", + body: function() { + for (var i = 0; i < 1000; i++) { + const item = getContext() + .getResponse() + .getBody(); + if (i > 0 && item !== i - 1) throw "body mismatch"; + getContext() + .getResponse() + .setBody(i); + } + } + }; + + const sproc2: StoredProcedureDefinition = { + id: "storedProcedure2", + body: function() { + for (var i = 0; i < 10; i++) + getContext() + .getResponse() + .appendValue("Body", i); + } + }; + + const sproc3: StoredProcedureDefinition = { + id: "storedProcedure3", + // TODO: I put any in here, but not sure how this will work... + body: function(input: any) { + getContext() + .getResponse() + .setBody("a" + input.temp); + } + }; + + // tslint:enable:no-var-keyword + // tslint:enable:prefer-const + // tslint:enable:curly + // tslint:enable:no-string-throw + // tslint:enable:object-literal-shorthand + + const { body: retrievedSproc } = await container.storedProcedures.create(sproc1); + const { body: result } = await container.storedProcedure(retrievedSproc.id).execute(); + assert.equal(result, 999); + + const { body: retrievedSproc2 } = await container.storedProcedures.create(sproc2); + const { body: result2 } = await container.storedProcedure(retrievedSproc2.id).execute(); + assert.equal(result2, 123456789); + const { body: retrievedSproc3 } = await container.storedProcedures.create(sproc3); + const { body: result3 } = await container.storedProcedure(retrievedSproc3.id).execute([{ temp: "so" }]); + assert.equal(result3, "aso"); + }); + + it("nativeApi Should do stored procedure operations successfully with upsert", async function() { + // tslint:disable:no-var-keyword + // tslint:disable:prefer-const + // tslint:disable:curly + // tslint:disable:no-string-throw + // tslint:disable:object-literal-shorthand + const sproc1: StoredProcedureDefinition = { + id: "storedProcedure1", + body: function() { + for (var i = 0; i < 1000; i++) { + const item = getContext() + .getResponse() + .getBody(); + if (i > 0 && item !== i - 1) throw "body mismatch"; + getContext() + .getResponse() + .setBody(i); + } + } + }; + + const sproc2: StoredProcedureDefinition = { + id: "storedProcedure2", + body: function() { + for (var i = 0; i < 10; i++) + getContext() + .getResponse() + .appendValue("Body", i); + } + }; + + const sproc3: StoredProcedureDefinition = { + id: "storedProcedure3", + // TODO: I put any in here, but not sure how this will work... + body: function(input: any) { + getContext() + .getResponse() + .setBody("a" + input.temp); + } + }; + + // tslint:enable:no-var-keyword + // tslint:enable:prefer-const + // tslint:enable:curly + // tslint:enable:no-string-throw + // tslint:enable:object-literal-shorthand + + const { body: retrievedSproc } = await container.storedProcedures.upsert(sproc1); + const { body: result } = await container.storedProcedure(retrievedSproc.id).execute(); + assert.equal(result, 999); + + const { body: retrievedSproc2 } = await container.storedProcedures.upsert(sproc2); + const { body: result2 } = await container.storedProcedure(retrievedSproc2.id).execute(); + assert.equal(result2, 123456789); + const { body: retrievedSproc3 } = await container.storedProcedures.upsert(sproc3); + const { body: result3 } = await container.storedProcedure(retrievedSproc3.id).execute([{ temp: "so" }]); + assert.equal(result3, "aso"); + }); + }); + + it("nativeApi Should execute stored procedure with partition key successfully name based", async function() { + const database = await getTestDatabase("sproc test database"); + // create container + const partitionKey = "key"; + + const containerDefinition = { + id: "coll1", + partitionKey: { paths: ["/" + partitionKey], kind: DocumentBase.PartitionKind.Hash } + }; + + const { body: containerResult } = await database.containers.create(containerDefinition, { offerThroughput: 12000 }); + const container = await database.container(containerResult.id); + + // tslint:disable:no-var-keyword + // tslint:disable:prefer-const + // tslint:disable:curly + // tslint:disable:no-string-throw + // tslint:disable:no-shadowed-variable + // tslint:disable:object-literal-shorthand + const querySproc = { + id: "querySproc", + body: function() { + var context = getContext(); + var container = context.getCollection(); + var response = context.getResponse(); + + // query for players + var query = "SELECT r.id, r.key, r.prop FROM r"; + var accept = container.queryDocuments(container.getSelfLink(), query, {}, function( + err: any, + documents: any, + responseOptions: any + ) { + if (err) throw new Error("Error" + err.message); + response.setBody(documents); + }); + + if (!accept) throw "Unable to read player details, abort "; + } + }; + // tslint:enable:no-var-keyword + // tslint:enable:prefer-const + // tslint:enable:curly + // tslint:enable:no-string-throw + // tslint:enable:no-shadowed-variable + // tslint:enable:object-literal-shorthand + + const documents = [ + { id: "document1" }, + { id: "document2", key: null, prop: 1 }, + { id: "document3", key: false, prop: 1 }, + { id: "document4", key: true, prop: 1 }, + { id: "document5", key: 1, prop: 1 }, + { id: "document6", key: "A", prop: 1 } + ]; + + const returnedDocuments = await bulkInsertItems(container, documents); + const { body: sproc } = await container.storedProcedures.create(querySproc); + const { body: result } = await container.storedProcedure(sproc.id).execute([], { partitionKey: null }); + assert(result !== undefined); + assert.equal(result.length, 1); + assert.equal(JSON.stringify(result[0]), JSON.stringify(documents[1])); + + const { body: result2 } = await container.storedProcedure(sproc.id).execute(null, { partitionKey: 1 }); + assert(result2 !== undefined); + assert.equal(result2.length, 1); + assert.equal(JSON.stringify(result2[0]), JSON.stringify(documents[4])); + }); + + it("nativeApi Should enable/disable script logging while executing stored procedure", async function() { + // create database + const database = await getTestDatabase("sproc test database"); + // create container + const { body: containerResult } = await database.containers.create({ id: "sample container" }); + + const container = await database.container(containerResult.id); + + // tslint:disable:curly + // tslint:disable:no-string-throw + // tslint:disable:no-shadowed-variable + // tslint:disable:one-line + // tslint:disable:object-literal-shorthand + const sproc1 = { + id: "storedProcedure", + body: function() { + const mytext = "x"; + const myval = 1; + try { + console.log("The value of %s is %s.", mytext, myval); + getContext() + .getResponse() + .setBody("Success!"); + } catch (err) { + getContext() + .getResponse() + .setBody("inline err: [" + err.number + "] " + err); + } + } + }; + + // tslint:enable:curly + // tslint:enable:no-string-throw + // tslint:enable:no-shadowed-variable + // tslint:enable:one-line + // tslint:enable:object-literal-shorthand + + const { body: retrievedSproc } = await container.storedProcedures.create(sproc1); + const { body: result1, headers: headers1 } = await container.storedProcedure(retrievedSproc.id).execute(); + assert.equal(result1, "Success!"); + assert.equal(headers1[Constants.HttpHeaders.ScriptLogResults], undefined); + + let requestOptions = { enableScriptLogging: true }; + const { body: result2, headers: headers2 } = await container + .storedProcedure(retrievedSproc.id) + .execute([], requestOptions); + assert.equal(result2, "Success!"); + assert.equal(headers2[Constants.HttpHeaders.ScriptLogResults], encodeURIComponent("The value of x is 1.")); + + requestOptions = { enableScriptLogging: false }; + const { body: result3, headers: headers3 } = await container + .storedProcedure(retrievedSproc.id) + .execute([], requestOptions); + assert.equal(result3, "Success!"); + assert.equal(headers3[Constants.HttpHeaders.ScriptLogResults], undefined); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/trigger.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/trigger.spec.ts new file mode 100644 index 000000000000..e3102f0c8ffa --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/trigger.spec.ts @@ -0,0 +1,303 @@ +import assert from "assert"; +import { DocumentBase } from "../.."; +import { Container, TriggerDefinition } from "../../client"; +import { getTestContainer, removeAllDatabases } from "../common/TestHelpers"; + +const notFoundErrorCode = 404; + +// Mock for trigger function bodies +declare var getContext: any; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + let container: Container; + + beforeEach(async function() { + await removeAllDatabases(); + container = await getTestContainer("trigger container"); + }); + + describe("Validate Trigger CRUD", function() { + it("nativeApi Should do trigger CRUD operations successfully name based", async function() { + // read triggers + const { result: triggers } = await container.triggers.readAll().toArray(); + assert.equal(Array.isArray(triggers), true); + + // create a trigger + const beforeCreateTriggersCount = triggers.length; + // tslint:disable:no-var-keyword + // tslint:disable:prefer-const + const triggerDefinition: TriggerDefinition = { + id: "sample trigger", + body: "serverScript() { var x = 10; }", + triggerType: DocumentBase.TriggerType.Pre, + triggerOperation: DocumentBase.TriggerOperation.All + }; + // tslint:enable:no-var-keyword + // tslint:enable:prefer-const + + const { body: trigger } = await container.triggers.create(triggerDefinition); + + assert.equal(trigger.id, triggerDefinition.id); + assert.equal(trigger.body, "serverScript() { var x = 10; }"); + + // read triggers after creation + const { result: triggersAfterCreation } = await container.triggers.readAll().toArray(); + assert.equal( + triggersAfterCreation.length, + beforeCreateTriggersCount + 1, + "create should increase the number of triggers" + ); + + // query triggers + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: triggerDefinition.id + } + ] + }; + const { result: results } = await container.triggers.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + // replace trigger + // prettier-ignore + trigger.body = function() { const x = 20; }; + const { body: replacedTrigger } = await container.trigger(trigger.id).replace(trigger); + + assert.equal(replacedTrigger.id, trigger.id); + assert.equal(replacedTrigger.body, "function () { const x = 20; }"); + + // read trigger + const { body: triggerAfterReplace } = await container.trigger(replacedTrigger.id).read(); + assert.equal(replacedTrigger.id, triggerAfterReplace.id); + + // delete trigger + await await container.trigger(replacedTrigger.id).delete(); + + // read triggers after deletion + try { + await container.trigger(replacedTrigger.id).read(); + assert.fail("Must fail to read after deletion"); + } catch (err) { + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }); + + it("nativeApi Should do trigger CRUD operations successfully name based with upsert", async function() { + // read triggers + const { result: triggers } = await container.triggers.readAll().toArray(); + assert.equal(Array.isArray(triggers), true); + + // create a trigger + const beforeCreateTriggersCount = triggers.length; + // tslint:disable:no-var-keyword + // tslint:disable:prefer-const + const triggerDefinition: TriggerDefinition = { + id: "sample trigger", + body: "serverScript() { var x = 10; }", + triggerType: DocumentBase.TriggerType.Pre, + triggerOperation: DocumentBase.TriggerOperation.All + }; + // tslint:enable:no-var-keyword + // tslint:enable:prefer-const + + const { body: trigger } = await container.triggers.upsert(triggerDefinition); + + assert.equal(trigger.id, triggerDefinition.id); + assert.equal(trigger.body, "serverScript() { var x = 10; }"); + + // read triggers after creation + const { result: triggersAfterCreation } = await container.triggers.readAll().toArray(); + assert.equal( + triggersAfterCreation.length, + beforeCreateTriggersCount + 1, + "create should increase the number of triggers" + ); + + // query triggers + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: triggerDefinition.id + } + ] + }; + const { result: results } = await container.triggers.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + // replace trigger + // prettier-ignore + trigger.body = function() { const x = 20; }; + const { body: replacedTrigger } = await container.triggers.upsert(trigger); + + assert.equal(replacedTrigger.id, trigger.id); + assert.equal(replacedTrigger.body, "function () { const x = 20; }"); + + // read trigger + const { body: triggerAfterReplace } = await container.trigger(replacedTrigger.id).read(); + assert.equal(replacedTrigger.id, triggerAfterReplace.id); + + // delete trigger + await await container.trigger(replacedTrigger.id).delete(); + + // read triggers after deletion + try { + await container.trigger(replacedTrigger.id).read(); + assert.fail("Must fail to read after deletion"); + } catch (err) { + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }); + }); + + describe("validate trigger functionality", function() { + const triggers: TriggerDefinition[] = [ + { + id: "t1", + // tslint:disable:no-var-keyword + // tslint:disable:prefer-const + // tslint:disable:curly + // tslint:disable:no-string-throw + // tslint:disable:object-literal-shorthand + body: function() { + var item = getContext() + .getRequest() + .getBody(); + item.id = item.id.toUpperCase() + "t1"; + getContext() + .getRequest() + .setBody(item); + }, + triggerType: DocumentBase.TriggerType.Pre, + triggerOperation: DocumentBase.TriggerOperation.All + }, + { + id: "t2", + body: "function() { }", // trigger already stringified + triggerType: DocumentBase.TriggerType.Pre, + triggerOperation: DocumentBase.TriggerOperation.All + }, + { + id: "t3", + body: function() { + const item = getContext() + .getRequest() + .getBody(); + item.id = item.id.toLowerCase() + "t3"; + getContext() + .getRequest() + .setBody(item); + }, + triggerType: DocumentBase.TriggerType.Pre, + triggerOperation: DocumentBase.TriggerOperation.All + }, + { + id: "response1", + body: function() { + const prebody = getContext() + .getRequest() + .getBody(); + if (prebody.id !== "TESTING POST TRIGGERt1") throw "name mismatch"; + const postbody = getContext() + .getResponse() + .getBody(); + if (postbody.id !== "TESTING POST TRIGGERt1") throw "name mismatch"; + }, + triggerType: DocumentBase.TriggerType.Post, + triggerOperation: DocumentBase.TriggerOperation.All + }, + { + id: "triggerOpType", + body: "function() { }", + triggerType: DocumentBase.TriggerType.Post, + triggerOperation: DocumentBase.TriggerOperation.Delete + } + ]; + // tslint:enable:no-var-keyword + // tslint:enable:prefer-const + // tslint:enable:curly + // tslint:enable:no-string-throw + // tslint:enable:object-literal-shorthand + + it("should do trigger operations successfully with create", async function() { + for (const trigger of triggers) { + await container.triggers.create(trigger); + } + // create document + const { body: document } = await container.items.create( + { id: "doc1", key: "value" }, + { preTriggerInclude: "t1" } + ); + assert.equal(document.id, "DOC1t1", "name should be capitalized"); + const { body: document2 } = await container.items.create( + { id: "doc2", key2: "value2" }, + { preTriggerInclude: "t2" } + ); + assert.equal(document2.id, "doc2", "name shouldn't change"); + const { body: document3 } = await container.items.create( + { id: "Doc3", prop: "empty" }, + { preTriggerInclude: "t3" } + ); + assert.equal(document3.id, "doc3t3"); + const { body: document4 } = await container.items.create( + { id: "testing post trigger" }, + { postTriggerInclude: "response1", preTriggerInclude: "t1" } + ); + assert.equal(document4.id, "TESTING POST TRIGGERt1"); + const { body: document5, headers } = await container.items.create( + { id: "responseheaders" }, + { preTriggerInclude: "t1" } + ); + assert.equal(document5.id, "RESPONSEHEADERSt1"); + try { + await container.items.create({ id: "Docoptype" }, { postTriggerInclude: "triggerOpType" }); + assert.fail("Must fail"); + } catch (err) { + assert.equal(err.code, 400, "Must throw when using a DELETE trigger on a CREATE operation"); + } + }); + + it("should do trigger operations successfully with upsert", async function() { + for (const trigger of triggers) { + await container.triggers.upsert(trigger); + } + // create document + const { body: document } = await container.items.upsert( + { id: "doc1", key: "value" }, + { preTriggerInclude: "t1" } + ); + assert.equal(document.id, "DOC1t1", "name should be capitalized"); + const { body: document2 } = await container.items.upsert( + { id: "doc2", key2: "value2" }, + { preTriggerInclude: "t2" } + ); + assert.equal(document2.id, "doc2", "name shouldn't change"); + const { body: document3 } = await container.items.upsert( + { id: "Doc3", prop: "empty" }, + { preTriggerInclude: "t3" } + ); + assert.equal(document3.id, "doc3t3"); + const { body: document4 } = await container.items.upsert( + { id: "testing post trigger" }, + { postTriggerInclude: "response1", preTriggerInclude: "t1" } + ); + assert.equal(document4.id, "TESTING POST TRIGGERt1"); + const { body: document5, headers } = await container.items.upsert( + { id: "responseheaders" }, + { preTriggerInclude: "t1" } + ); + assert.equal(document5.id, "RESPONSEHEADERSt1"); + try { + await container.items.upsert({ id: "Docoptype" }, { postTriggerInclude: "triggerOpType" }); + assert.fail("Must fail"); + } catch (err) { + assert.equal(err.code, 400, "Must throw when using a DELETE trigger on a CREATE operation"); + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/ttl.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/ttl.spec.ts new file mode 100644 index 000000000000..be6ffee23680 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/ttl.spec.ts @@ -0,0 +1,303 @@ +import assert from "assert"; +import { Container, ContainerDefinition, Database } from "../../client"; +import { getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +async function sleep(time: number) { + return new Promise(resolve => { + setTimeout(resolve, time); + }); +} + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 600000); + beforeEach(async function() { + await removeAllDatabases(); + }); + + describe("TTL tests", function() { + async function createcontainerWithInvalidDefaultTtl( + db: Database, + containerDefinition: ContainerDefinition, + collId: any, + defaultTtl: number + ) { + containerDefinition.id = collId; + containerDefinition.defaultTtl = defaultTtl; + try { + await db.containers.create(containerDefinition); + } catch (err) { + const badRequestErrorCode = 400; + assert.equal(err.code, badRequestErrorCode, "response should return error code " + badRequestErrorCode); + } + } + + async function createItemWithInvalidTtl(container: Container, itemDefinition: any, itemId: any, ttl: number) { + itemDefinition.id = itemId; + itemDefinition.ttl = ttl; + + try { + await container.items.create(itemDefinition); + assert.fail("Must throw if using invalid TTL"); + } catch (err) { + const badRequestErrorCode = 400; + assert.equal(err.code, badRequestErrorCode, "response should return error code " + badRequestErrorCode); + } + } + + it("nativeApi Validate container and Item TTL values.", async function() { + try { + const database = await getTestDatabase("ttl test1 database"); + + const containerDefinition = { + id: "sample container1", + defaultTtl: 5 + }; + const { body: containerResult } = await database.containers.create(containerDefinition); + + assert.equal(containerDefinition.defaultTtl, containerResult.defaultTtl); + const container = database.container(containerResult.id); + + // null, 0, -10 are unsupported value for defaultTtl.Valid values are -1 or a non-zero positive 32-bit integer value + await createcontainerWithInvalidDefaultTtl(database, containerDefinition, "sample container2", null); + await createcontainerWithInvalidDefaultTtl(database, containerDefinition, "sample container3", 0); + await createcontainerWithInvalidDefaultTtl(database, containerDefinition, "sample container4", -10); + + const itemDefinition = { + id: "doc", + name: "sample Item", + key: "value", + ttl: 2 + }; + + // 0, null, -10 are unsupported value for ttl.Valid values are -1 or a non-zero positive 32-bit integer value + await createItemWithInvalidTtl(container, itemDefinition, "doc1", 0); + await createItemWithInvalidTtl(container, itemDefinition, "doc2", null); + await createItemWithInvalidTtl(container, itemDefinition, "doc3", -10); + } catch (err) { + throw err; + } + }); + + async function checkItemGone(container: Container, createdItem: any) { + try { + await container.item(createdItem.id).read(); + assert.fail("Must throw if the Item isn't there"); + } catch (err) { + const badRequestErrorCode = 404; + assert.equal(err.code, badRequestErrorCode, "response should return error code " + badRequestErrorCode); + } + } + + async function checkItemExists(container: Container, createdItem: any) { + const { body: readItem } = await container.item(createdItem.id).read(); + assert.equal(readItem.ttl, createdItem.ttl); + } + + async function positiveDefaultTtlStep4(container: Container, createdItem: any) { + // the created Item should NOT be gone as it 's ttl value is set to 8 which overrides the containers' s defaultTtl value(5) + await checkItemExists(container, createdItem); + await sleep(4000); + await checkItemGone(container, createdItem); + } + + async function positiveDefaultTtlStep3(container: Container, createdItem: any, itemDefinition: any) { + // the created Item should be gone now as it 's ttl value is set to 2 which overrides the containers' s defaultTtl value(5) + await checkItemGone(container, createdItem); + itemDefinition.id = "doc4"; + itemDefinition.ttl = 8; + + const { body: doc } = await container.items.create(itemDefinition); + await sleep(6000); + await positiveDefaultTtlStep4(container, doc); + } + + async function positiveDefaultTtlStep2(container: Container, createdItem: any, itemDefinition: any) { + // the created Item should NOT be gone as it 's ttl value is set to -1(never expire) which overrides the containers' s defaultTtl value + await checkItemExists(container, createdItem); + itemDefinition.id = "doc3"; + itemDefinition.ttl = 2; + + const { body: doc } = await container.items.create(itemDefinition); + await sleep(4000); + await positiveDefaultTtlStep3(container, doc, itemDefinition); + } + + async function positiveDefaultTtlStep1(container: Container, createdItem: any, itemDefinition: any) { + // the created Item should be gone now as it 's ttl value would be same as defaultTtl value of the container + await checkItemGone(container, createdItem); + itemDefinition.id = "doc2"; + itemDefinition.ttl = -1; + + const { body: doc } = await container.items.create(itemDefinition); + await sleep(5000); + await positiveDefaultTtlStep2(container, doc, itemDefinition); + } + + it("nativeApi Validate Item TTL with positive defaultTtl.", async function() { + const database = await getTestDatabase("ttl test2 database"); + + const containerDefinition = { + id: "sample container", + defaultTtl: 5 + }; + + const { body: containerResult } = await database.containers.create(containerDefinition); + + const container = await database.container(containerResult.id); + + const itemDefinition = { + id: "doc1", + name: "sample Item", + key: "value" + }; + + const { body: createdItem } = await container.items.create(itemDefinition); + await sleep(7000); + await positiveDefaultTtlStep1(container, createdItem, itemDefinition); + }); + + async function minusOneDefaultTtlStep1( + container: Container, + createdItem1: any, + createdItem2: any, + createdItem3: any + ) { + // the created Item should be gone now as it 's ttl value is set to 2 which overrides the containers' s defaultTtl value(-1) + await checkItemGone(container, createdItem3); + + // The Items with id doc1 and doc2 will never expire + const { body: readItem1 } = await container.item(createdItem1.id).read(); + assert.equal(readItem1.id, createdItem1.id); + + const { body: readItem2 } = await container.item(createdItem2.id).read(); + assert.equal(readItem2.id, createdItem2.id); + } + + it("nativeApi Validate Item TTL with -1 defaultTtl.", async function() { + const database = await getTestDatabase("ttl test2 database"); + + const containerDefinition = { + id: "sample container", + defaultTtl: -1 + }; + + const { body: createdContainer } = await database.containers.create(containerDefinition); + + const container = await database.container(createdContainer.id); + + const itemDefinition: any = { + id: "doc1", + name: "sample Item", + key: "value" + }; + + // the created Item 's ttl value would be -1 inherited from the container' s defaultTtl and this Item will never expire + const { body: createdItem1 } = await container.items.create(itemDefinition); + + // This Item is also set to never expire explicitly + itemDefinition.id = "doc2"; + itemDefinition.ttl = -1; + + const { body: createdItem2 } = await container.items.create(itemDefinition); + + itemDefinition.id = "doc3"; + itemDefinition.ttl = 2; + + const { body: createdItem3 } = await container.items.create(itemDefinition); + await sleep(4000); + await minusOneDefaultTtlStep1(container, createdItem1, createdItem2, createdItem3); + }); + + it("nativeApi Validate Item TTL with no defaultTtl.", async function() { + const database = await getTestDatabase("ttl test3 database"); + + const containerDefinition = { id: "sample container" }; + + const { body: createdContainer } = await database.containers.create(containerDefinition); + + const container = await database.container(createdContainer.id); + + const itemDefinition = { + id: "doc1", + name: "sample Item", + key: "value", + ttl: 5 + }; + + const { body: createdItem } = await container.items.create(itemDefinition); + + // Created Item still exists even after ttl time has passed since the TTL is disabled at container level(no defaultTtl property defined) + await sleep(7000); + await checkItemExists(container, createdItem); + }); + + async function miscCasesStep4(container: Container, createdItem: any, itemDefinition: any) { + // Created Item still exists even after ttl time has passed since the TTL is disabled at container level + await checkItemExists(container, createdItem); + } + + async function miscCasesStep3(container: Container, upsertedItem: any, itemDefinition: any) { + // the upserted Item should be gone now after 10 secs from the last write(upsert) of the Item + await checkItemGone(container, upsertedItem); + const query = "SELECT * FROM root r"; + const { result: results } = await container.items.query(query).toArray(); + assert.equal(results.length, 0); + + // Use a container definition without defaultTtl to disable ttl at container level + const containerDefinition = { id: container.id }; + + await container.replace(containerDefinition); + + itemDefinition.id = "doc2"; + + const { body: createdItem } = await container.items.create(itemDefinition); + await sleep(5000); + await miscCasesStep4(container, createdItem, itemDefinition); + } + + async function miscCasesStep2(container: Container, itemDefinition: any) { + // Upsert the Item after 3 secs to reset the Item 's ttl + itemDefinition.key = "value2"; + const { body: upsertedItem } = await container.items.upsert(itemDefinition); + await sleep(7000); + // Upserted Item still exists after (3+7)10 secs from Item creation time( with container 's defaultTtl set to 8) since it' s ttl was reset after 3 secs by upserting it + await checkItemExists(container, upsertedItem); + await sleep(3000); + await miscCasesStep3(container, upsertedItem, itemDefinition); + } + + async function miscCasesStep1(container: Container, createdItem: any, itemDefinition: any) { + // the created Item should be gone now as the ttl time expired + await checkItemGone(container, createdItem); + // We can create a Item with the same id after the ttl time has expired + const { body: doc } = await container.items.create(itemDefinition); + assert.equal(itemDefinition.id, doc.id); + await sleep(3000); + await miscCasesStep2(container, itemDefinition); + } + + it("nativeApi Validate Item TTL Misc cases.", async function() { + const database = await getTestDatabase("ttl test4 database"); + + const containerDefinition = { + id: "sample container", + defaultTtl: 8 + }; + + const { body: containerResult } = await database.containers.create(containerDefinition); + + const container = await database.container(containerResult.id); + + const itemDefinition = { + id: "doc1", + name: "sample Item", + key: "value" + }; + + const { body: createdItem } = await container.items.create(itemDefinition); + + await sleep(10000); + await miscCasesStep1(container, createdItem, itemDefinition); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/udf.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/udf.spec.ts new file mode 100644 index 000000000000..ddea389fe4b8 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/udf.spec.ts @@ -0,0 +1,142 @@ +import assert from "assert"; +import { Container } from "../.."; +import { UserDefinedFunctionDefinition } from "../../client"; +import { getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +const containerId = "sample container"; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + + beforeEach(async function() { + await removeAllDatabases(); + }); + + describe("User Defined Function", function() { + let container: Container; + + beforeEach(async function() { + // create database + const database = await getTestDatabase("udf test database"); + + // create container + await database.containers.create({ id: containerId }); + + container = await database.container(containerId); + }); + it("nativeApi Should do UDF CRUD operations successfully", async function() { + const { result: udfs } = await container.userDefinedFunctions.readAll().toArray(); + + // create a udf + const beforeCreateUdfsCount = udfs.length; + const udfDefinition: UserDefinedFunctionDefinition = { + id: "sample udf", + body: "function () { const x = 10; }" + }; + + // TODO also handle upsert case + const { body: udf } = await container.userDefinedFunctions.create(udfDefinition); + + assert.equal(udf.id, udfDefinition.id); + assert.equal(udf.body, "function () { const x = 10; }"); + + // read udfs after creation + const { result: udfsAfterCreate } = await container.userDefinedFunctions.readAll().toArray(); + assert.equal(udfsAfterCreate.length, beforeCreateUdfsCount + 1, "create should increase the number of udfs"); + + // query udfs + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: udfDefinition.id + } + ] + }; + const { result: results } = await container.userDefinedFunctions.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + // replace udf + udfDefinition.body = "function () { const x = 10; }"; + const { body: replacedUdf } = await container.userDefinedFunction(udfDefinition.id).replace(udfDefinition); + + assert.equal(replacedUdf.id, udfDefinition.id); + assert.equal(replacedUdf.body, "function () { const x = 10; }"); + + // read udf + const { body: udfAfterReplace } = await container.userDefinedFunction(replacedUdf.id).read(); + + assert.equal(replacedUdf.id, udfAfterReplace.id); + + // delete udf + const { body: res } = await container.userDefinedFunction(replacedUdf.id).delete(); + + // read udfs after deletion + try { + const { body: badudf } = await container.userDefinedFunction(replacedUdf.id).read(); + assert.fail("Must fail to read after delete"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }); + + it("nativeApi Should do UDF CRUD operations successfully", async function() { + const { result: udfs } = await container.userDefinedFunctions.readAll().toArray(); + + // create a udf + const beforeCreateUdfsCount = udfs.length; + const udfDefinition = { + id: "sample udf", + body: "function () { const x = 10; }" + }; + + const { body: udf } = await container.userDefinedFunctions.upsert(udfDefinition); + + assert.equal(udf.id, udfDefinition.id); + assert.equal(udf.body, "function () { const x = 10; }"); + + // read udfs after creation + const { result: udfsAfterCreate } = await container.userDefinedFunctions.readAll().toArray(); + assert.equal(udfsAfterCreate.length, beforeCreateUdfsCount + 1, "create should increase the number of udfs"); + + // query udfs + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: udfDefinition.id + } + ] + }; + const { result: results } = await container.userDefinedFunctions.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + // replace udf + udfDefinition.body = "function () { const x = 10; }"; + const { body: replacedUdf } = await container.userDefinedFunctions.upsert(udfDefinition); + + assert.equal(replacedUdf.id, udfDefinition.id); + assert.equal(replacedUdf.body, "function () { const x = 10; }"); + + // read udf + const { body: udfAfterReplace } = await container.userDefinedFunction(replacedUdf.id).read(); + + assert.equal(replacedUdf.id, udfAfterReplace.id); + + // delete udf + const { body: res } = await container.userDefinedFunction(replacedUdf.id).delete(); + + // read udfs after deletion + try { + const { body: badudf } = await container.userDefinedFunction(replacedUdf.id).read(); + assert.fail("Must fail to read after delete"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/functional/user.spec.ts b/sdk/cosmosdb/cosmos/src/test/functional/user.spec.ts new file mode 100644 index 000000000000..58d8a91c13c9 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/functional/user.spec.ts @@ -0,0 +1,81 @@ +import assert from "assert"; +import { UserDefinition } from "../../client"; +import { createOrUpsertUser, getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +describe("NodeJS CRUD Tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + beforeEach(async function() { + await removeAllDatabases(); + }); + describe("Validate User CRUD", function() { + const userCRUDTest = async function(isUpsertTest: boolean) { + // create database + const database = await getTestDatabase("Validate user CRUD"); + + // list users + const { result: users } = await database.users.readAll().toArray(); + assert.equal(users.constructor, Array, "Value should be an array"); + const beforeCreateCount = users.length; + + // create user + const { body: userDef } = await createOrUpsertUser(database, { id: "new user" }, undefined, isUpsertTest); + assert.equal(userDef.id, "new user", "user name error"); + let user = database.user(userDef.id); + + // list users after creation + const { result: usersAfterCreation } = await database.users.readAll().toArray(); + assert.equal(usersAfterCreation.length, beforeCreateCount + 1); + + // query users + const querySpec = { + query: "SELECT * FROM root r WHERE r.id=@id", + parameters: [ + { + name: "@id", + value: "new user" + } + ] + }; + const { result: results } = await database.users.query(querySpec).toArray(); + assert(results.length > 0, "number of results for the query should be > 0"); + + // replace user + userDef.id = "replaced user"; + let replacedUser: UserDefinition; + if (isUpsertTest) { + const r = await database.users.upsert(userDef); + replacedUser = r.body; + } else { + const r = await user.replace(userDef); + replacedUser = r.body; + } + assert.equal(replacedUser.id, "replaced user", "user name should change"); + assert.equal(userDef.id, replacedUser.id, "user id should stay the same"); + user = database.user(replacedUser.id); + + // read user + const { body: userAfterReplace } = await user.read(); + assert.equal(replacedUser.id, userAfterReplace.id); + + // delete user + const { body: res } = await user.delete(); + + // read user after deletion + try { + await user.read(); + assert.fail("Must fail to read user after deletion"); + } catch (err) { + const notFoundErrorCode = 404; + assert.equal(err.code, notFoundErrorCode, "response should return error code 404"); + } + }; + + it("nativeApi Should do User CRUD operations successfully name based", async function() { + await userCRUDTest(false); + }); + + it("nativeApi Should do User CRUD operations successfully name based with upsert", async function() { + await userCRUDTest(true); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/aggregateQuery.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/aggregateQuery.spec.ts new file mode 100644 index 000000000000..7cb915c19834 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/aggregateQuery.spec.ts @@ -0,0 +1,323 @@ +import assert from "assert"; +import * as util from "util"; +import { QueryIterator } from "../.."; +import { Container, ContainerDefinition, Database } from "../../client"; +import { DataType, IndexKind, PartitionKind } from "../../documents"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { FeedOptions } from "../../request"; +import { TestData } from "../common/TestData"; +import { bulkInsertItems, getTestContainer, removeAllDatabases } from "../common/TestHelpers"; + +// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +describe("NodeJS Aggregate Query Tests", async function() { + this.timeout(process.env.MOCHA_TIMEOUT || 20000); + const partitionKey = "key"; + const uniquePartitionKey = "uniquePartitionKey"; + const testdata = new TestData(partitionKey, uniquePartitionKey); + const documentDefinitions = testdata.docs; + let db: Database; + let container: Container; + + const containerDefinition: ContainerDefinition = { + id: "sample container", + indexingPolicy: { + includedPaths: [ + { + path: "/", + indexes: [ + { + kind: IndexKind.Hash, + dataType: DataType.String + }, + { + kind: IndexKind.Range, + dataType: DataType.Number + } + ] + } + ] + }, + partitionKey: { + paths: ["/" + partitionKey], + kind: PartitionKind.Hash + } + }; + + const containerOptions = { offerThroughput: 10100 }; + + describe("Validate Aggregate Document Query", function() { + // - removes all the databases, + // - creates a new database, + // - creates a new collecton, + // - bulk inserts documents to the container + before(async function() { + await removeAllDatabases(); + container = await getTestContainer( + "Validate Aggregate Document Query", + undefined, + containerDefinition, + containerOptions + ); + db = container.database; + await bulkInsertItems(container, documentDefinitions); + }); + + const validateResult = function(actualValue: any, expectedValue: any) { + assert.deepEqual(actualValue, expectedValue, "actual value doesn't match with expected value."); + }; + + const validateToArray = async function(queryIterator: QueryIterator, expectedResults: any) { + try { + const { result: results } = await queryIterator.toArray(); + assert.equal(results.length, expectedResults.length, "invalid number of results"); + assert.equal(queryIterator.hasMoreResults(), false, "hasMoreResults: no more results is left"); + } catch (err) { + throw err; + } + }; + + const validateNextItem = async function(queryIterator: QueryIterator, expectedResults: any) { + let results: any = []; + + try { + while (results.length < expectedResults.length) { + const { result: item } = await queryIterator.nextItem(); + if (item === undefined) { + assert(!queryIterator.hasMoreResults(), "hasMoreResults must signal results exhausted"); + validateResult(results, expectedResults); + return; + } + results = results.concat(item); + + if (results.length < expectedResults.length) { + assert(queryIterator.hasMoreResults(), "hasMoreResults must indicate more results"); + } + } + } catch (err) { + throw err; + } + }; + + const validateNextItemAndCurrentAndHasMoreResults = async function( + queryIterator: QueryIterator, + expectedResults: any[] + ) { + // curent and nextItem recursively invoke each other till queryIterator is exhausted + //////////////////////////////// + // validate nextItem() + //////////////////////////////// + + const results: any[] = []; + try { + while (results.length <= expectedResults.length) { + const { result: item } = await queryIterator.nextItem(); + const { result: currentItem } = await queryIterator.current(); + if (item === undefined) { + break; + } + results.push(item); + if (results.length < expectedResults.length) { + assert(queryIterator.hasMoreResults(), "hasMoreResults must indicate more results"); + } + assert.equal(item, currentItem, "current must give the previously item returned by nextItem"); + } + + assert(!queryIterator.hasMoreResults(), "hasMoreResults must signal results exhausted"); + validateResult(results, expectedResults); + } catch (err) { + throw err; + } + }; + + const validateExecuteNextAndHasMoreResults = async function( + queryIterator: QueryIterator, + options: any, + expectedResults: any[] + ) { + //////////////////////////////// + // validate executeNext() + //////////////////////////////// + const pageSize = options["maxItemCount"]; + const listOfResultPages: any[] = []; + const listOfHeaders: any[] = []; + + let totalFetchedResults: any[] = []; + + try { + while (totalFetchedResults.length <= expectedResults.length) { + const { result: results, headers } = await queryIterator.executeNext(); + listOfResultPages.push(results); + listOfHeaders.push(headers); + + if (results === undefined || totalFetchedResults.length === expectedResults.length) { + break; + } + + totalFetchedResults = totalFetchedResults.concat(results); + + if (totalFetchedResults.length < expectedResults.length) { + // there are more results + assert(results.length <= pageSize, "executeNext: invalid fetch block size"); + assert.equal(results.length, pageSize, "executeNext: invalid fetch block size"); + assert(queryIterator.hasMoreResults(), "hasMoreResults expects to return true"); + } else { + // no more results + assert.equal( + expectedResults.length, + totalFetchedResults.length, + "executeNext: didn't fetch all the results" + ); + assert(results.length <= pageSize, "executeNext: actual fetch size is more than the requested page size"); + } + } + + // no more results + validateResult(totalFetchedResults, expectedResults); + assert.equal(queryIterator.hasMoreResults(), false, "hasMoreResults: no more results is left"); + } catch (err) { + throw err; + } + }; + + const validateForEach = async function(queryIterator: QueryIterator, expectedResults: any[]) { + //////////////////////////////// + // validate forEach() + //////////////////////////////// + + const results: any[] = []; + let callbackSingnalledEnd = false; + // forEach uses callbacks still, so just wrap in a promise + for await (const { result: item } of queryIterator.getAsyncIterator()) { + // if the previous invocation returned false, forEach must avoid invoking the callback again! + assert.equal(callbackSingnalledEnd, false, "forEach called callback after the first false returned"); + results.push(item); + if (results.length === expectedResults.length) { + callbackSingnalledEnd = true; + } + } + validateResult(results, expectedResults); + }; + + const executeQueryAndValidateResults = async function(query: string | SqlQuerySpec, expectedResults: any[]) { + const options: FeedOptions = { enableCrossPartitionQuery: true, maxDegreeOfParallelism: 2, maxItemCount: 1 }; + + const queryIterator = container.items.query(query, options); + await validateToArray(queryIterator, expectedResults); + queryIterator.reset(); + await validateExecuteNextAndHasMoreResults(queryIterator, options, expectedResults); + queryIterator.reset(); + await validateNextItemAndCurrentAndHasMoreResults(queryIterator, expectedResults); + await validateForEach(queryIterator, expectedResults); + }; + + const generateTestConfigs = function() { + const testConfigs: any[] = []; + const aggregateQueryFormat = "SELECT VALUE %s(r.%s) FROM r WHERE %s"; + const aggregateOrderByQueryFormat = "SELECT VALUE %s(r.%s) FROM r WHERE %s ORDER BY r.%s"; + const aggregateConfigs = [ + { + operator: "AVG", + expected: testdata.sum / testdata.numberOfDocumentsWithNumbericId, + condition: util.format("IS_NUMBER(r.%s)", partitionKey) + }, + { operator: "AVG", expected: undefined, condition: "true" }, + { + operator: "COUNT", + expected: testdata.numberOfDocuments, + condition: "true" + }, + { operator: "MAX", expected: "xyz", condition: "true" }, + { operator: "MIN", expected: null, condition: "true" }, + { + operator: "SUM", + expected: testdata.sum, + condition: util.format("IS_NUMBER(r.%s)", partitionKey) + }, + { operator: "SUM", expected: undefined, condition: "true" } + ]; + + aggregateConfigs.forEach(function(config) { + let query = util.format(aggregateQueryFormat, config.operator, partitionKey, config.condition); + let testName = util.format("%s %s", config.operator, config.condition); + testConfigs.push({ + testName, + query, + expected: config.expected + }); + + query = util.format(aggregateOrderByQueryFormat, config.operator, partitionKey, config.condition, partitionKey); + testName = util.format("%s %s OrderBy", config.operator, config.condition); + testConfigs.push({ + testName, + query, + expected: config.expected + }); + }); + + const aggregateSinglePartitionQueryFormat = "SELECT VALUE %s(r.%s) FROM r WHERE r.%s = '%s'"; + const aggregateSinglePartitionQueryFormatSelect = "SELECT %s(r.%s) FROM r WHERE r.%s = '%s'"; + const samePartitionSum = + (testdata.numberOfDocsWithSamePartitionKey * (testdata.numberOfDocsWithSamePartitionKey + 1)) / 2.0; + const aggregateSinglePartitionConfigs = [ + { + operator: "AVG", + expected: samePartitionSum / testdata.numberOfDocsWithSamePartitionKey + }, + { + operator: "COUNT", + expected: testdata.numberOfDocsWithSamePartitionKey + }, + { + operator: "MAX", + expected: testdata.numberOfDocsWithSamePartitionKey + }, + { operator: "MIN", expected: 1 }, + { operator: "SUM", expected: samePartitionSum } + ]; + + aggregateSinglePartitionConfigs.forEach(function(config) { + let query = util.format( + aggregateSinglePartitionQueryFormat, + config.operator, + testdata.field, + partitionKey, + uniquePartitionKey + ); + let testName = util.format("%s SinglePartition %s", config.operator, "SELECT VALUE"); + testConfigs.push({ + testName, + query, + expected: config.expected + }); + + query = util.format( + aggregateSinglePartitionQueryFormatSelect, + config.operator, + testdata.field, + partitionKey, + uniquePartitionKey + ); + testName = util.format("%s SinglePartition %s", config.operator, "SELECT"); + testConfigs.push({ + testName, + query, + expected: { $1: config.expected } + }); + }); + + return testConfigs; + }; + + generateTestConfigs().forEach(function(test) { + it(test.testName, async function() { + try { + const expected = test.expected === undefined ? [] : [test.expected]; + await executeQueryAndValidateResults(test.query, expected); + } catch (err) { + throw err; + } + }); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/authorization.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/authorization.spec.ts new file mode 100644 index 000000000000..145cf914123a --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/authorization.spec.ts @@ -0,0 +1,152 @@ +import assert from "assert"; +import { Container, CosmosClient, DocumentBase } from "../.."; +import { Database } from "../../client"; +import { endpoint } from "../common/_testConfig"; +import { getTestContainer, removeAllDatabases } from "../common/TestHelpers"; + +describe("Authorization", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + + // TODO: should have types for all these things + let database: Database; + let container: Container; + + let userReadDefinition: any = { id: "User With Read Permission" }; + let userAllDefinition: any = { id: "User With All Permission" }; + let collReadPermission: any = { + id: "container Read Permission", + permissionMode: DocumentBase.PermissionMode.Read + }; + let collAllPermission: any = { + id: "container All Permission", + permissionMode: DocumentBase.PermissionMode.All + }; + /************** TEST **************/ + + beforeEach(async function() { + await removeAllDatabases(); + + // create a database & container + container = await getTestContainer("Authorization tests"); + database = container.database; + + // create userReadPermission + const { body: userDef } = await container.database.users.create(userReadDefinition); + assert.equal(userReadDefinition.id, userDef.id, "userReadPermission is not created properly"); + userReadDefinition = userDef; + const userRead = container.database.user(userDef.id); + + // give permission to read container, to userReadPermission + collReadPermission.resource = container.url; + const { body: readPermission } = await userRead.permissions.create(collReadPermission); + assert.equal(readPermission.id, collReadPermission.id, "permission to read coll1 is not created properly"); + collReadPermission = readPermission; + + // create userAllPermission + const { body: userAllDef } = await container.database.users.create(userAllDefinition); + assert.equal(userAllDefinition.id, userAllDef.id, "userAllPermission is not created properly"); + userAllDefinition = userAllDef; + const userAll = container.database.user(userAllDef.id); + + // create collAllPermission + collAllPermission.resource = container.url; + const { body: allPermission } = await userAll.permissions.create(collAllPermission); + assert.equal(collAllPermission.id, allPermission.id, "permission to read coll2 is not created properly"); + collAllPermission = allPermission; + }); + + afterEach(async function() { + await removeAllDatabases(); + }); + + it("Accessing container by resourceTokens", async function() { + const rTokens: any = {}; + rTokens[container.id] = collReadPermission._token; + + const clientReadPermission = new CosmosClient({ + endpoint, + auth: { resourceTokens: rTokens } + }); + + const { body: coll } = await clientReadPermission + .database(database.id) + .container(container.id) + .read(); + assert.equal(coll.id, container.id, "invalid container"); + }); + + it("Accessing container by permissionFeed", async function() { + const clientReadPermission = new CosmosClient({ + endpoint, + auth: { permissionFeed: [collReadPermission] } + }); + + // self link must be used to access a resource using permissionFeed + const { body: coll } = await clientReadPermission + .database(database.id) + .container(container.id) + .read(); + assert.equal(coll.id, container.id, "invalid container"); + }); + + it("Accessing container without permission fails", async function() { + const clientNoPermission = new CosmosClient({ endpoint, auth: null }); + + try { + await clientNoPermission + .database(database.id) + .container(container.id) + .read(); + assert.fail("accessing container did not throw"); + } catch (err) { + assert(err !== undefined); // TODO: should check that we get the right error message + } + }); + + it("Accessing document by permissionFeed of parent container", async function() { + const { body: createdDoc } = await container.items.create({ + id: "document1" + }); + const clientReadPermission = new CosmosClient({ + endpoint, + auth: { permissionFeed: [collReadPermission] } + }); + assert.equal("document1", createdDoc.id, "invalid documnet create"); + + const { body: readDoc } = await clientReadPermission + .database(database.id) + .container(container.id) + .item(createdDoc.id) + .read(); + assert.equal(readDoc.id, createdDoc.id, "invalid document read"); + }); + + it("Modifying container by resourceTokens", async function() { + const rTokens: any = {}; + rTokens[container.id] = collAllPermission._token; + const clientAllPermission = new CosmosClient({ + endpoint, + auth: { resourceTokens: rTokens } + }); + + // delete container + return clientAllPermission + .database(database.id) + .container(container.id) + .delete(); + }); + + it("Modifying container by permissionFeed", async function() { + const clientAllPermission = new CosmosClient({ + endpoint, + auth: { permissionFeed: [collAllPermission] } + }); + + // self link must be used to access a resource using permissionFeed + // delete container + return clientAllPermission + .database(database.id) + .container(container.id) + .delete(); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/container.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/container.spec.ts new file mode 100644 index 000000000000..d182745d1a53 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/container.spec.ts @@ -0,0 +1,51 @@ +import assert from "assert"; +import { Container } from "../../client"; + +describe("Container", function() { + describe("extractPartitionKey", function() { + let partitionKeyDefinition: any; // TODO: any + const container: Container = new Container({ database: { client: null } } as any, undefined, undefined); + + beforeEach(function() { + partitionKeyDefinition = undefined; + }); + + describe("With undefined partitionKeyDefinition", function() { + it("should return undefined", function() { + const document: any = {}; + const result = container.extractPartitionKey(document, partitionKeyDefinition); + assert.equal(result, undefined); + }); + }); + + describe("With a defined partitionKeyDefinition", function() { + beforeEach(function() { + partitionKeyDefinition = { paths: ["/a/b"] }; + }); + + it("should return [{}] when document has no partition key value", function() { + const document = {}; + const result = container.extractPartitionKey(document, partitionKeyDefinition); + assert.deepEqual(result, [{}]); + }); + + it("should return [null] when document has a null partition key value", function() { + const document: any = { a: { b: null } }; + const result = container.extractPartitionKey(document, partitionKeyDefinition); + assert.deepEqual(result, [null]); + }); + + it("should return [{}] when document has a partially defined partition key value", function() { + const document = { a: "some value" }; + const result = container.extractPartitionKey(document, partitionKeyDefinition); + assert.deepEqual(result, [{}]); + }); + + it("should return [value] when document has a valid partition key value", function() { + const document = { a: { b: "some value" } }; + const result = container.extractPartitionKey(document, partitionKeyDefinition); + assert.deepEqual(result, ["some value"]); + }); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/crossPartition.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/crossPartition.spec.ts new file mode 100644 index 000000000000..ee3f8f8d858e --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/crossPartition.spec.ts @@ -0,0 +1,913 @@ +import assert from "assert"; +import * as util from "util"; +import { Constants } from "../.."; +import { Container, ContainerDefinition } from "../../client"; +import { DataType, IndexKind, PartitionKind } from "../../documents"; +import { SqlQuerySpec } from "../../queryExecutionContext"; +import { QueryIterator } from "../../queryIterator"; +import { bulkInsertItems, getTestContainer, removeAllDatabases } from "../common/TestHelpers"; + +function compare(key: string) { + return function(a: any, b: any): number { + if (a[key] > b[key]) { + return 1; + } + if (a[key] < b[key]) { + return -1; + } + return 0; + }; +} + +describe("Cross Partition", function() { + this.timeout(process.env.MOCHA_TIMEOUT || "30000"); + const generateDocuments = function(docSize: number) { + const docs = []; + for (let i = 0; i < docSize; i++) { + const d = { + id: i.toString(), + name: "sample document", + spam: "eggs" + i.toString(), + cnt: i, + key: "value", + spam2: i === 3 ? "eggs" + i.toString() : i, + boolVar: i % 2 === 0, + number: 1.1 * i + }; + docs.push(d); + } + return docs; + }; + + describe("Validate Query", function() { + const documentDefinitions = generateDocuments(20); + + const containerDefinition: ContainerDefinition = { + id: "sample container", + indexingPolicy: { + includedPaths: [ + { + path: "/", + indexes: [ + { + kind: IndexKind.Range, + dataType: DataType.Number + }, + { + kind: IndexKind.Range, + dataType: DataType.String + } + ] + } + ] + }, + partitionKey: { + paths: ["/id"], + kind: PartitionKind.Hash + } + }; + const containerOptions = { offerThroughput: 25100 }; + + let container: Container; + + // - removes all the databases, + // - creates a new database, + // - creates a new collecton, + // - bulk inserts documents to the container + before(async function() { + await removeAllDatabases(); + container = await getTestContainer("Validate 中文 Query", undefined, containerDefinition, containerOptions); + await bulkInsertItems(container, documentDefinitions); + }); + + const validateResults = function(actualResults: any[], expectedOrderIds: string[]) { + assert.equal( + actualResults.length, + expectedOrderIds.length, + "actual results length doesn't match with expected results length." + ); + + for (let i = 0; i < actualResults.length; i++) { + assert.equal( + actualResults[i].id, + expectedOrderIds[i], + "actual result content doesn't match with expected result content. " + + actualResults[i].id + + " != " + + expectedOrderIds[i] + ); + } + }; + + const validateToArray = async function( + queryIterator: QueryIterator, + options: any, + expectedOrderIds: string[] + ) { + //////////////////////////////// + // validate toArray() + //////////////////////////////// + options.continuation = undefined; + try { + const { result: results } = await queryIterator.toArray(); + assert.equal(results.length, expectedOrderIds.length, "invalid number of results"); + assert.equal(queryIterator.hasMoreResults(), false, "hasMoreResults: no more results is left"); + + return validateResults(results, expectedOrderIds); + } catch (err) { + throw err; + } + }; + + const validateNextItem = async function(queryIterator: QueryIterator, expectedOrderIds: string[]) { + //////////////////////////////// + // validate nextItem() + //////////////////////////////// + const results: any[] = []; + try { + while (results.length < expectedOrderIds.length) { + assert(queryIterator.hasMoreResults(), "hasMoreResults must indicate more results"); + const { result: item } = await queryIterator.nextItem(); + if (item === undefined) { + break; + } + results.push(item); + } + + assert(!queryIterator.hasMoreResults(), "hasMoreResults must signal results exhausted"); + validateResults(results, expectedOrderIds); + } catch (err) { + throw err; + } + }; + + const validateNextItemAndCurrentAndHasMoreResults = async function( + queryIterator: QueryIterator, + expectedOrderIds: string[] + ) { + // curent and nextItem recursively invoke each other till queryIterator is exhausted + //////////////////////////////// + // validate nextItem() + //////////////////////////////// + const results: any[] = []; + try { + while (results.length <= expectedOrderIds.length) { + const { result: currentItem } = await queryIterator.current(); + const { result: item } = await queryIterator.nextItem(); + if (!item) { + break; + } + results.push(item); + if (results.length < expectedOrderIds.length) { + assert(queryIterator.hasMoreResults(), "hasMoreResults must indicate more results"); + } + assert.equal(item, currentItem, "current must give the previously item returned by nextItem"); + } + + assert(!queryIterator.hasMoreResults(), "hasMoreResults must signal results exhausted"); + validateResults(results, expectedOrderIds); + } catch (err) { + throw err; + } + }; + + const validateExecuteNextAndHasMoreResults = async function( + options: any, + queryIterator: QueryIterator, + expectedOrderIds: string[], + validateExecuteNextWithContinuationToken?: boolean + ) { + const pageSize = options["maxItemCount"]; + + //////////////////////////////// + // validate executeNext() + //////////////////////////////// + + const listOfResultPages: any[] = []; + const listOfHeaders: any[] = []; + + let totalFetchedResults: any[] = []; + + try { + while (totalFetchedResults.length <= expectedOrderIds.length) { + const { result: results, headers } = await queryIterator.executeNext(); + listOfResultPages.push(results); + listOfHeaders.push(headers); + + if (results === undefined || totalFetchedResults.length === expectedOrderIds.length) { + break; + } + + totalFetchedResults = totalFetchedResults.concat(results); + + if (totalFetchedResults.length < expectedOrderIds.length) { + // there are more results + assert(results.length <= pageSize, "executeNext: invalid fetch block size"); + if (validateExecuteNextWithContinuationToken) { + assert(results.length <= pageSize, "executeNext: invalid fetch block size"); + } else { + assert.equal(results.length, pageSize, "executeNext: invalid fetch block size"); + } + assert(queryIterator.hasMoreResults(), "hasMoreResults expects to return true"); + } else { + // no more results + assert.equal( + expectedOrderIds.length, + totalFetchedResults.length, + "executeNext: didn't fetch all the results" + ); + assert(results.length <= pageSize, "executeNext: actual fetch size is more than the requested page size"); + } + } + + // no more results + validateResults(totalFetchedResults, expectedOrderIds); + assert.equal(queryIterator.hasMoreResults(), false, "hasMoreResults: no more results is left"); + if (validateExecuteNextWithContinuationToken) { + // TODO: chrande + // I don't think this code is ever called, which means we're missing tests or should delete it. + throw new Error("Not yet implemented"); + // return validateExecuteNextWithGivenContinuationToken( + // containerLink, query, options, listOfResultPages, listOfHeaders); + } + } catch (err) { + throw err; + } + }; + + const validateForEach = async function(queryIterator: QueryIterator, expectedOrderIds: any[]) { + //////////////////////////////// + // validate forEach() + //////////////////////////////// + const results: any[] = []; + let callbackSingnalledEnd = false; + // forEach uses callbacks still, so just wrap in a promise + for await (const { result: item } of queryIterator.getAsyncIterator()) { + // if the previous invocation returned false, forEach must avoid invoking the callback again! + assert.equal(callbackSingnalledEnd, false, "forEach called callback after the first false returned"); + results.push(item); + if (results.length === expectedOrderIds.length) { + callbackSingnalledEnd = true; + } + } + validateResults(results, expectedOrderIds); + }; + + const validateQueryMetrics = async function(queryIterator: QueryIterator) { + try { + while (queryIterator.hasMoreResults()) { + const { result: results, headers } = await queryIterator.executeNext(); + if (results === undefined) { + break; + } + + assert.notEqual(headers[Constants.HttpHeaders.QueryMetrics], null); + } + } catch (err) { + throw err; + } + }; + + const executeQueryAndValidateResults = async function( + query: string | SqlQuerySpec, + options: any, + expectedOrderIds: any[], + validateExecuteNextWithContinuationToken?: boolean + ) { + options.populateQueryMetrics = true; + validateExecuteNextWithContinuationToken = validateExecuteNextWithContinuationToken || false; + const queryIterator = container.items.query(query, options); + + await validateToArray(queryIterator, options, expectedOrderIds); + queryIterator.reset(); + await validateExecuteNextAndHasMoreResults( + options, + queryIterator, + expectedOrderIds, + validateExecuteNextWithContinuationToken + ); + queryIterator.reset(); + await validateNextItemAndCurrentAndHasMoreResults(queryIterator, expectedOrderIds); + await validateForEach(queryIterator, expectedOrderIds); + await validateQueryMetrics(queryIterator); + }; + + const requestChargeValidator = async function(queryIterator: QueryIterator) { + let counter = 0; + let totalRequestCharge = 0; + + while (queryIterator.hasMoreResults()) { + const { result: results, headers } = await queryIterator.executeNext(); + const rc: number = (headers || {})[Constants.HttpHeaders.RequestCharge] as number; + + if (counter === 0) { + assert(rc > 0); + counter += 1; + } + + if (results === undefined) { + assert(totalRequestCharge > 0); + return; + } else { + totalRequestCharge += rc; + assert(rc >= 0); + } + } + }; + + it("Validate Parallel Query As String With maxDegreeOfParallelism = 0", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 0 + }; + + const expectedOrderedIds = [1, 10, 18, 2, 3, 13, 14, 16, 17, 0, 11, 12, 5, 9, 19, 4, 6, 7, 8, 15]; + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds, false); + }); + + it("Validate Parallel Query As String With maxDegreeOfParallelism: -1", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: -1, + populateQueryMetrics: true + }; + + const expectedOrderedIds = [1, 10, 18, 2, 3, 13, 14, 16, 17, 0, 11, 12, 5, 9, 19, 4, 6, 7, 8, 15]; + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds, false); + }); + + it("Validate Parallel Query As String With maxDegreeOfParallelism: 1", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 1 + }; + + const expectedOrderedIds = [1, 10, 18, 2, 3, 13, 14, 16, 17, 0, 11, 12, 5, 9, 19, 4, 6, 7, 8, 15]; + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds, false); + }); + + it("Validate Parallel Query As String With maxDegreeOfParallelism: 3", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 3 + }; + + const expectedOrderedIds = [1, 10, 18, 2, 3, 13, 14, 16, 17, 0, 11, 12, 5, 9, 19, 4, 6, 7, 8, 15]; + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds, false); + }); + + it("Validate Parallel Query Request Charge With maxDegreeOfParallelism: 3", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 3 + }; + + const queryIterator = container.items.query(query, options); + await requestChargeValidator(queryIterator); + }); + + it("Validate Parallel Query Request Charge With maxDegreeOfParallelism: 1", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 1 + }; + + const queryIterator = container.items.query(query, options); + await requestChargeValidator(queryIterator); + }); + + it("Validate Simple OrderBy Query Request Charge With maxDegreeOfParallelism = 1", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.spam"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 1 + }; + + const queryIterator = container.items.query(query, options); + await requestChargeValidator(queryIterator); + }); + + it("Validate Simple OrderBy Query Request Charge With maxDegreeOfParallelism = 0", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.spam"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 0 + }; + + const queryIterator = container.items.query(query, options); + await requestChargeValidator(queryIterator); + }); + + it("Validate Top Query Request Charge with maxDegreeOfParallelism = 3", async function() { + // a top query + const topCount = 6; + // sanity check + assert(topCount < documentDefinitions.length, "test setup is wrong"); + + const query = util.format("SELECT top %d * FROM root r", topCount); + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 3 + }; + + const queryIterator = container.items.query(query, options); + await requestChargeValidator(queryIterator); + }); + + it("Validate Top Query Request Charge with maxDegreeOfParallelism = 0", async function() { + // a top query + const topCount = 6; + // sanity check + assert(topCount < documentDefinitions.length, "test setup is wrong"); + + const query = util.format("SELECT top %d * FROM root r", topCount); + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 0 + }; + + const queryIterator = container.items.query(query, options); + await requestChargeValidator(queryIterator); + }); + + it("Validate Simple OrderBy Query As String With maxDegreeOfParallelism = 0", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.spam"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 0 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds); + }); + + it("Validate Simple OrderBy Query As String With maxDegreeOfParallelism = 1", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.spam"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 1 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds); + }); + + it("Validate Simple OrderBy Query As String With maxDegreeOfParallelism = 3", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.spam"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 3 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds); + }); + + it("Validate Simple OrderBy Query As String With maxDegreeOfParallelism = -1", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.spam"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: -1 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds); + }); + + it("Validate Simple OrderBy Query As String", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.spam"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds); + }); + + it("Validate Simple OrderBy Query", async function() { + // simple order by query + const querySpec = { + query: "SELECT * FROM root r order by r.spam" + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(querySpec, options, expectedOrderedIds); + }); + + it("Validate OrderBy Query With ASC", async function() { + // an order by query with explicit ascending ordering + const querySpec = { + query: "SELECT * FROM root r order by r.spam ASC" + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(querySpec, options, expectedOrderedIds); + }); + + it("Validate OrderBy Query With DESC", async function() { + // an order by query with explicit descending ordering + const querySpec = { + query: "SELECT * FROM root r order by r.spam DESC" + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions + .sort(compare("spam")) + .map(function(r) { + return r["id"]; + }) + .reverse(); + + // validates the results size and order + await executeQueryAndValidateResults(querySpec, options, expectedOrderedIds); + }); + + it("Validate OrderBy with top", async function() { + // an order by query with top, total existing docs more than requested top count + const topCount = 9; + const querySpec = { + query: util.format("SELECT top %d * FROM root r order by r.spam", topCount) + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions + .sort(compare("spam")) + .map(function(r) { + return r["id"]; + }) + .slice(0, topCount); + + await executeQueryAndValidateResults(querySpec, options, expectedOrderedIds); + }); + + it("Validate OrderBy with Top Query (less results than top counts)", async function() { + // an order by query with top, total existing docs less than requested top count + const topCount = 30; + // sanity check + assert(topCount > documentDefinitions.length, "test setup is wrong"); + const querySpec = { + query: util.format("SELECT top %d * FROM root r order by r.spam", topCount) + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + await executeQueryAndValidateResults(querySpec, options, expectedOrderedIds); + }); + + it("Validate Top Query with maxDegreeOfParallelism = 3", async function() { + // a top query + const topCount = 6; + // sanity check + assert(topCount < documentDefinitions.length, "test setup is wrong"); + + const query = util.format("SELECT top %d * FROM root r", topCount); + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2, + maxDegreeOfParallelism: 3 + }; + + // prepare expected behaviour verifier + const queryIterator = container.items.query(query, options); + + const { result: results } = await queryIterator.toArray(); + assert.equal(results.length, topCount); + + // select unique ids + const uniqueIds: any = {}; + results.forEach(function(item) { + uniqueIds[item.id] = true; + }); + // assert no duplicate results + assert.equal(results.length, Object.keys(uniqueIds).length); + }); + + it("Validate Top Query", async function() { + // a top query + const topCount = 6; + // sanity check + assert(topCount < documentDefinitions.length, "test setup is wrong"); + + const query = util.format("SELECT top %d * FROM root r", topCount); + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + // prepare expected behaviour verifier + const queryIterator = container.items.query(query, options); + + const { result: results } = await queryIterator.toArray(); + assert.equal(results.length, topCount); + + // select unique ids + const uniqueIds: any = {}; + results.forEach(item => { + uniqueIds[item.id] = true; + }); + // assert no duplicate results + assert.equal(results.length, Object.keys(uniqueIds).length); + }); + + it("Validate Top Query (with 0 topCount)", async function() { + // a top query + const topCount = 0; + // sanity check + assert(topCount < documentDefinitions.length, "test setup is wrong"); + + const query = util.format("SELECT top %d * FROM root r", topCount); + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + // prepare expected behaviour verifier + const queryIterator = container.items.query(query, options); + + const { result: results } = await queryIterator.toArray(); + assert.equal(results.length, topCount); + + // select unique ids + const uniqueIds: any = {}; + results.forEach(item => { + uniqueIds[item.id] = true; + }); + // assert no duplicate results + assert.equal(results.length, Object.keys(uniqueIds).length); + }); + + it("Validate Parametrized Top Query", async function() { + // a top query + const topCount = 6; + // sanity check + assert(topCount < documentDefinitions.length, "test setup is wrong"); + + const querySpec: SqlQuerySpec = { + query: "SELECT top @n * FROM root r", + + parameters: [{ name: "@n", value: topCount }] + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + // prepare expected behaviour verifier + const queryIterator = container.items.query(querySpec, options); + + const { result: results } = await queryIterator.toArray(); + assert.equal(results.length, topCount); + + // select unique ids + const uniqueIds: any = {}; + results.forEach(item => { + uniqueIds[item.id] = true; + }); + // assert no duplicate results + assert.equal(results.length, Object.keys(uniqueIds).length); + }); + + it("Validate OrderBy with Parametrized Top Query", async function() { + // a parametrized top order by query + const topCount = 9; + // sanity check + assert(topCount < documentDefinitions.length, "test setup is wrong"); + // a parametrized top order by query + const querySpec = { + query: "SELECT top @n * FROM root r order by r.spam", + + parameters: [{ name: "@n", value: topCount }] + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions + .sort(compare("spam")) + .map(function(r) { + return r["id"]; + }) + .slice(0, topCount); + + await executeQueryAndValidateResults(querySpec, options, expectedOrderedIds); + }); + + it("Validate OrderBy with Parametrized Predicate", async function() { + // an order by query combined with parametrized predicate + const querySpec = { + query: "SELECT * FROM root r where r.cnt > @cnt order by r.spam", + parameters: [{ name: "@cnt", value: 5 }] + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions + .sort(compare("spam")) + .filter(function(r) { + return r["cnt"] > 5; + }) + .map(function(r) { + return r["id"]; + }); + + await executeQueryAndValidateResults(querySpec, options, expectedOrderedIds); + }); + + it("Validate Error Handling - Orderby where types are noncomparable", async function() { + // test orderby with different order by item type + // an order by query + const query = { + query: "SELECT * FROM root r order by r.spam2" + }; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + // prepare expected behaviour verifier + try { + const queryIterator = container.items.query(query, options); + await queryIterator.toArray(); + } catch (err) { + assert.notEqual(err, undefined); + } + }); + + it("Validate OrderBy Integer Query", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.cnt"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("cnt")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds); + }); + + it("Validate OrderBy Floating Point Number Query", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.number"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("number")).map(function(r) { + return r["id"]; + }); + + // validates the results size and order + await executeQueryAndValidateResults(query, options, expectedOrderedIds); + }); + + it("Validate OrderBy Boolean Query", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.boolVar"; + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const queryIterator = container.items.query(query, options); + const { result: results } = await queryIterator.toArray(); + assert.equal(results.length, documentDefinitions.length); + + let index = 0; + while (index < results.length) { + if (results[index].boolVar) { + break; + } + assert(results[index].id % 2 === 1); + index++; + } + + while (index < results.length) { + assert(results[index].boolVar); + assert(results[index].id % 2 === 0); + index++; + } + }); + + it("Validate Failure", async function() { + // simple order by query in string format + const query = "SELECT * FROM root r order by r.spam"; + + const options = { + enableCrossPartitionQuery: true, + maxItemCount: 2 + }; + + const expectedOrderedIds = documentDefinitions.sort(compare("spam")).map(function(r) { + return r["id"]; + }); + + const queryIterator = container.items.query(query, options); + + let firstTime = true; + + const { result } = await queryIterator.current(); + + if (firstTime) { + firstTime = false; + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/encoding.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/encoding.spec.ts new file mode 100644 index 000000000000..64970c360f54 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/encoding.spec.ts @@ -0,0 +1,50 @@ +import assert from "assert"; +import { IndexingMode } from "../../documents"; +import { getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +const testDoc = { + id: "ABC", + content: + "€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€" +}; + +describe("Create And Read Validation", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + + const dateTime = new Date(); + const databaseId = "encodingTestDB"; + + afterEach(async function() { + await removeAllDatabases(); + }); + beforeEach(async function() { + await removeAllDatabases(); + }); + + it("check if the document from db matches the actual document", async function() { + try { + // Create Database + const database = await getTestDatabase(databaseId); + const containerBody = { + id: "डेटाबेस پایگاه داده 数据库" + dateTime.getTime(), + indexingPolicy: { indexingMode: IndexingMode.lazy } // Modes : Lazy, Consistent + }; + + // Create a container inside the database + const { body: containerDef } = await database.containers.create(containerBody); + const container = database.container(containerDef.id); + + assert.equal(containerDef.id, containerBody.id, "invalid container Id"); + + // Add the document in the container + const { body: doc } = await container.items.create(testDoc); + assert.equal(doc.id, testDoc.id, "invalid document Id"); + + // Read the container and see if it matches to the initial document + const { body: resultDoc } = await container.item(doc.id).read<{ id: string; content: string }>(); + assert.equal(testDoc.content, resultDoc.content, "read document result is different from initial document"); + } catch (err) { + throw err; + } + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/incrementalFeed.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/incrementalFeed.spec.ts new file mode 100644 index 000000000000..a17bbce31733 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/incrementalFeed.spec.ts @@ -0,0 +1,367 @@ +import assert from "assert"; +import { RequestOptions } from "../.."; +import { Container, ContainerDefinition } from "../../client"; +import { Helper } from "../../common"; +import { getTestContainer, removeAllDatabases } from "../common/TestHelpers"; + +describe("Change Feed Iterator", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 20000); + + describe("Non-partitioned", function() { + // delete all databases and create sample database + before(async function() { + await removeAllDatabases(); + }); + + (process.env.TESTS_MULTIREGION ? describe.skip : describe)("Should only find items after start time", function() { + let container: Container; + + // create container and two items + before(async function() { + container = await getTestContainer("Newly updated items should be fetched incrementally"); + }); + + after(async function() { + await container.delete(); + }); + + it("should fetch updated items only with start time", async function() { + await container.items.create({ id: "item1" }); + const date = new Date(); + await Helper.sleep(3000); + await container.items.create({ id: "item2" }); + const iterator = container.items.readChangeFeed({ startTime: date }); + + const { result: itemsShouldBeEmpty, etag: initialEtag } = await iterator.executeNext(); + + assert(initialEtag, "change feed response should have etag header"); + const etag = initialEtag; + + assert.equal(itemsShouldBeEmpty.length, 0, "Initial request should have empty results"); + + const { result: items } = await iterator.executeNext(); + + assert.equal(items.length, 1, "initial number of items should be equal 1"); + assert.equal(items[0].id, "item2", "should find the newest item, but not the old"); + const item = { id: "item2", name: "xyz" }; + + const { body: replaced } = await container.item(item.id).replace(item); + assert.deepEqual(replaced.name, "xyz", "replaced item should be valid"); + + // Should continue from last etag + const { result: itemsAfterUpdate } = await iterator.executeNext(); + assert.equal(itemsAfterUpdate.length, 1, "initial number of items should be equal 1"); + assert.equal(itemsAfterUpdate[0].name, "xyz", "fetched item should have 'name: xyz'"); + assert.equal(itemsAfterUpdate[0].id, item.id, "fetched item should be valid"); + + // Equivalent to execute next on other iterator from the previous etag + const iteratorWithContinuation = container.items.readChangeFeed({ continuation: etag }); + const { result: itemsWithContinuation } = await iteratorWithContinuation.executeNext(); + assert.equal(itemsWithContinuation.length, 1, "initial number of items should be equal 1"); + assert.equal(itemsWithContinuation[0].name, "xyz", "fetched item should have 'name: xyz'"); + assert.equal(itemsWithContinuation[0].id, item.id, "fetched item should be valid"); + + const { result: shouldHaveNoItems } = await iterator.executeNext(); + assert.equal(shouldHaveNoItems.length, 0, "there should be 0 results"); + const hasMoreResults = iterator.hasMoreResults; + assert.equal(hasMoreResults, false, "hasMoreResults should be false when we read the whole page"); + }); + }); + + describe("Newly updated items should be fetched incrementally", function() { + let container: Container; + + // create container and two items + before(async function() { + container = await getTestContainer("Newly updated items should be fetched incrementally"); + await container.items.create({ id: "item1" }); + await container.items.create({ id: "item2" }); + }); + + after(async function() { + await container.delete(); + }); + + it("should fetch updated items only", async function() { + const iterator = container.items.readChangeFeed({ startFromBeginning: true }); + + const { result: items, headers } = await iterator.executeNext(); + assert(headers.etag, "change feed response should have etag header"); + const etag = headers.etag; + + assert.equal(items.length, 2, "initial number of items should be equal 2"); + + const item = items[1]; + item.name = "xyz"; + + const { body: replaced } = await container.item(item.id).replace(item); + assert.deepEqual(replaced.name, "xyz", "replaced item should be valid"); + + // Should continue from last etag + const { result: itemsAfterUpdate } = await iterator.executeNext(); + assert.equal(itemsAfterUpdate.length, 1, "initial number of items should be equal 1"); + assert.equal(itemsAfterUpdate[0].name, "xyz", "fetched item should have 'name: xyz'"); + assert.equal(itemsAfterUpdate[0].id, item.id, "fetched item should be valid"); + + // Equivalent to execute next on other iterator from the previous etag + const iteratorWithContinuation = container.items.readChangeFeed({ continuation: etag }); + const { result: itemsWithContinuation } = await iteratorWithContinuation.executeNext(); + assert.equal(itemsWithContinuation.length, 1, "initial number of items should be equal 1"); + assert.equal(itemsWithContinuation[0].name, "xyz", "fetched item should have 'name: xyz'"); + assert.equal(itemsWithContinuation[0].id, item.id, "fetched item should be valid"); + + const { result: shouldHaveNoItems } = await iterator.executeNext(); + assert.equal(shouldHaveNoItems.length, 0, "there should be 0 results"); + const hasMoreResults = iterator.hasMoreResults; + assert.equal(hasMoreResults, false, "hasMoreResults should be false when we read the whole page"); + }); + }); + + describe("Async iterator should find items", function() { + let container: Container; + + // create container and two items + before(async function() { + container = await getTestContainer("Newly updated items should be fetched incrementally"); + await container.items.create({ id: "item1" }); + await container.items.create({ id: "item2" }); + }); + + after(async function() { + await container.delete(); + }); + + it("should fetch updated items only", async function() { + const iterator = container.items.readChangeFeed({ startFromBeginning: true }); + + const items: any[] = []; + for await (const page of iterator.getAsyncIterator()) { + if (page.result.length === 0) { + break; + } + items.push(...page.result); + } + + assert.equal(items.length, 2, "initial number of items should be equal 2"); + + const item = items[1]; + item.name = "xyz"; + + const { body: replaced } = await container.item(item.id).replace(item); + assert.deepEqual(replaced.name, "xyz", "replaced item should be valid"); + + // Should continue from last etag + const itemsAfterUpdate: any[] = []; + for await (const page of iterator.getAsyncIterator()) { + if (page.result.length === 0) { + break; + } + itemsAfterUpdate.push(...page.result); + } + assert.equal(itemsAfterUpdate.length, 1, "initial number of items should be equal 1"); + assert.equal(itemsAfterUpdate[0].name, "xyz", "fetched item should have 'name: xyz'"); + assert.equal(itemsAfterUpdate[0].id, item.id, "fetched item should be valid"); + + const { result: shouldHaveNoItems } = await iterator.executeNext(); + assert.equal(shouldHaveNoItems.length, 0, "there should be 0 results"); + const hasMoreResults = iterator.hasMoreResults; + assert.equal(hasMoreResults, false, "hasMoreResults should be false when we read the whole page"); + + let count = 0; + for await (const page of iterator.getAsyncIterator()) { + ++count; + } + assert.equal(count, 0, "async iterator should return any results if there are none left to serve"); + }); + }); + + describe("Newly created items should be fetched incrementally", async function() { + let container: Container; + + // create container and one item + before(async function() { + container = await getTestContainer("Newly updated items should be fetched incrementally"); + await container.items.create({ id: "item1" }); + }); + + after(async function() { + await container.delete(); + }); + + it("should fetch new items only", async function() { + const iterator = container.items.readChangeFeed({}); + + const { result: items, headers } = await iterator.executeNext(); + assert(headers.etag, "change feed response should have etag header"); + assert.equal(items.length, 0, "change feed response should have no items on it initially"); + + const { body: itemThatWasCreated } = await container.items.create({ + id: "item2", + prop: 1 + }); + + const { result: itemsAfterCreate } = await iterator.executeNext(); + assert.equal(itemsAfterCreate.length, 1, "should have 1 item from create"); + const itemThatWasFound = itemsAfterCreate[0]; + + assert.notDeepEqual(itemThatWasFound, itemThatWasCreated, "actual should not match with expected value."); + delete itemThatWasFound._lsn; + delete itemThatWasFound._metadata; + assert.deepEqual(itemThatWasFound, itemThatWasCreated, "actual value doesn't match with expected value."); + + const { result: itemsShouldBeEmptyWithNoNewCreates } = await iterator.executeNext(); + assert.equal(itemsShouldBeEmptyWithNoNewCreates.length, 0, "should be nothing new"); + + await container.items.create({ id: "item3" }); + await container.items.create({ id: "item4" }); + const { result: itemsShouldHave2NewItems } = await iterator.executeNext(); + assert.equal(itemsShouldHave2NewItems.length, 2, "there should be 2 results"); + + const { result: shouldHaveNoItems } = await iterator.executeNext(); + assert.equal(shouldHaveNoItems.length, 0, "there should be 0 results"); + const hasMoreResults = iterator.hasMoreResults; + assert.equal(hasMoreResults, false, "hasMoreResults should be false when we read the whole page"); + }); + }); + }); + + describe("Partition Key", function() { + // delete all databases and create sample database + before(async function() { + await removeAllDatabases(); + }); + + describe("Newly updated items should be fetched incrementally", function() { + let container: Container; + + // create container and two items + before(async function() { + const containerDef: ContainerDefinition = { + partitionKey: { + kind: "Hash", + paths: ["/key"] + } + }; + const throughput: RequestOptions = { offerThroughput: 25100 }; + container = await getTestContainer( + "Newly updated items should be fetched incrementally", + undefined, + containerDef, + throughput + ); + await container.items.create({ id: "item1", key: "0" }); + await container.items.create({ id: "item2", key: "0" }); + await container.items.create({ id: "item1", key: "1" }); + await container.items.create({ id: "item2", key: "1" }); + }); + + after(async function() { + await container.delete(); + }); + + it("should throw if used with no partition key or partition key range id", async function() { + const iterator = container.items.readChangeFeed({ startFromBeginning: true }); + + try { + await iterator.executeNext(); + } catch (err) { + assert.equal( + err.message, + "Container is partitioned, but no partition key or partition key range id was specified." + ); + return; + } + assert.fail("Should have failed"); + }); + + it("should fetch updated items only", async function() { + const iterator = container.items.readChangeFeed("0", { startFromBeginning: true }); + + const { result: items, headers } = await iterator.executeNext(); + assert(headers.etag, "change feed response should have etag header"); + + assert.equal(items.length, 2, "initial number of items should be equal 2"); + + const item = items[1]; + item.name = "xyz"; + + const { body: replaced } = await container.item(item.id).replace(item); + assert.deepEqual(replaced.name, "xyz", "replaced item should be valid"); + + const { result: itemsAfterUpdate } = await iterator.executeNext(); + assert.equal(itemsAfterUpdate.length, 1, "initial number of items should be equal 1"); + assert.equal(itemsAfterUpdate[0].name, "xyz", "fetched item should have 'name: xyz'"); + assert.equal(itemsAfterUpdate[0].id, item.id, "fetched item should be valid"); + + const { result: shouldHaveNoItems } = await iterator.executeNext(); + assert.equal(shouldHaveNoItems.length, 0, "there should be 0 results"); + const hasMoreResults = iterator.hasMoreResults; + assert.equal(hasMoreResults, false, "hasMoreResults should be false when we read the whole page"); + }); + }); + + describe("Newly created items should be fetched incrementally", async function() { + let container: Container; + + // create container and one item + before(async function() { + const containerDef: ContainerDefinition = { + partitionKey: { + kind: "Hash", + paths: ["/key"] + } + }; + const throughput: RequestOptions = { offerThroughput: 25100 }; + container = await getTestContainer( + "Newly updated items should be fetched incrementally", + undefined, + containerDef, + throughput + ); + await container.items.create({ id: "item1", key: "0" }); + await container.items.create({ id: "item1", key: "1" }); + }); + + after(async function() { + await container.delete(); + }); + + it("should fetch new items only", async function() { + const iterator = container.items.readChangeFeed("0", {}); + + const { result: items, headers } = await iterator.executeNext(); + assert(headers.etag, "change feed response should have etag header"); + assert.equal(items.length, 0, "change feed response should have no items on it initially"); + + const { body: itemThatWasCreated, headers: createHeaders } = await container.items.create({ + id: "item2", + prop: 1, + key: "0" + }); + + const { result: itemsAfterCreate } = await iterator.executeNext(); + assert.equal(itemsAfterCreate.length, 1, "should have 1 item from create"); + const itemThatWasFound = itemsAfterCreate[0]; + + assert.notDeepEqual(itemThatWasFound, itemThatWasCreated, "actual should not match with expected value."); + delete itemThatWasFound._lsn; + delete itemThatWasFound._metadata; + assert.deepEqual(itemThatWasFound, itemThatWasCreated, "actual value doesn't match with expected value."); + + const { result: itemsShouldBeEmptyWithNoNewCreates } = await iterator.executeNext(); + assert.equal(itemsShouldBeEmptyWithNoNewCreates.length, 0, "should be nothing new"); + + await container.items.create({ id: "item3", key: "0" }); + await container.items.create({ id: "item4", key: "0" }); + await container.items.create({ id: "item3", key: "1" }); + await container.items.create({ id: "item4", key: "1" }); + const { result: itemsShouldHave2NewItems } = await iterator.executeNext(); + assert.equal(itemsShouldHave2NewItems.length, 2, "there should be 2 results"); + const { result: shouldHaveNoItems } = await iterator.executeNext(); + assert.equal(shouldHaveNoItems.length, 0, "there should be 0 results"); + const hasMoreResults = iterator.hasMoreResults; + assert.equal(hasMoreResults, false, "hasMoreResults should be false when we read the whole page"); + }); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/multiregion.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/multiregion.spec.ts new file mode 100644 index 000000000000..f88487e83d28 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/multiregion.spec.ts @@ -0,0 +1,52 @@ +import assert from "assert"; + +import { CosmosClient } from "../../CosmosClient"; +import { ConnectionPolicy, DatabaseAccount } from "../../documents"; + +import { endpoint, masterKey } from "../common/_testConfig"; + +// This test requires a multi-region write enabled account with at least two regions. +(process.env.TESTS_MULTIREGION ? describe : describe.skip)("Multi-region tests", function() { + this.timeout(process.env.MOCHA_TIMEOUT || "30000"); + let preferredLocations: string[] = []; + let dbAccount: DatabaseAccount; + + before(async function() { + const client = new CosmosClient({ endpoint, auth: { masterKey } }); + ({ body: dbAccount } = await client.getDatabaseAccount()); + // We reverse the order of the preferred locations list to make sure + // we don't just follow the order we got back from the server + preferredLocations = dbAccount.readableLocations.map(v => v.name).reverse(); + assert( + preferredLocations.length > 1, + "Not a multi-region account. Please add a region before running this test again." + ); + }); + + it("Preferred locations should be honored for readEndpoint", async function() { + const connectionPolicy = new ConnectionPolicy(); + connectionPolicy.PreferredLocations = preferredLocations; + const client = new CosmosClient({ endpoint, auth: { masterKey }, connectionPolicy }); + const currentReadEndpoint = await client.getReadEndpoint(); + assert( + currentReadEndpoint.includes(preferredLocations[0].toLowerCase().replace(/ /g, "")), + "The readendpoint should be the first preferred location" + ); + }); + + it("Preferred locations should be honored for writeEndpoint", async function() { + assert( + dbAccount.enableMultipleWritableLocations, + "MultipleWriteableLocations must be set on your database account for this test to work" + ); + const connectionPolicy = new ConnectionPolicy(); + connectionPolicy.PreferredLocations = preferredLocations; + connectionPolicy.UseMultipleWriteLocations = true; + const client = new CosmosClient({ endpoint, auth: { masterKey }, connectionPolicy }); + const currentWriteEndpoint = await client.getWriteEndpoint(); + assert( + currentWriteEndpoint.includes(preferredLocations[0].toLowerCase().replace(/ /g, "")), + "The writeendpoint should be the first preferred location" + ); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/proxy.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/proxy.spec.ts new file mode 100644 index 000000000000..a86f29ebb037 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/proxy.spec.ts @@ -0,0 +1,81 @@ +import * as http from "http"; +import * as net from "net"; +import * as url from "url"; +import { CosmosClient, DocumentBase } from "../.."; +import { endpoint, masterKey } from "../common/_testConfig"; +import { addEntropy } from "../common/TestHelpers"; + +const isBrowser = new Function("try {return this===window;}catch(e){ return false;}"); +if (!isBrowser()) { + describe("Validate http proxy setting in environment variable", function() { + const proxy = http.createServer((req, resp) => { + resp.writeHead(200, { "Content-Type": "text/plain" }); + resp.end(); + }); + + proxy.on("connect", (req, clientSocket, head) => { + const serverUrl = url.parse(`http://${req.url}`); + const serverSocket = net.connect( + parseInt(serverUrl.port, 10), + serverUrl.hostname, + () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n" + "Proxy-agent: Node.js-Proxy\r\n" + "\r\n"); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + } + ); + }); + + const proxyPort = 8989; + const connectionPolicy = new DocumentBase.ConnectionPolicy(); + connectionPolicy.ProxyUrl = "http://127.0.0.1:8989"; + + it("nativeApi Client Should successfully execute request", async function() { + return new Promise((resolve, reject) => { + proxy.listen(proxyPort, "127.0.0.1", async () => { + try { + const client = new CosmosClient({ + endpoint, + auth: { masterKey }, + connectionPolicy + }); + // create database + await client.databases.create({ + id: addEntropy("ProxyTest") + }); + resolve(); + } catch (err) { + throw err; + } finally { + proxy.close(); + } + }); + }); + }); + + it("nativeApi Client Should execute request in error while the proxy setting is not correct", async function() { + this.timeout(process.env.MOCHA_TIMEOUT || 30000); + return new Promise((resolve, reject) => { + proxy.listen(proxyPort + 1, "127.0.0.1", async () => { + try { + const client = new CosmosClient({ + endpoint, + auth: { masterKey }, + connectionPolicy + }); + // create database + await client.databases.create({ + id: addEntropy("ProxyTest") + }); + reject(new Error("Should create database in error while the proxy setting is not correct")); + } catch (err) { + resolve(); + } finally { + proxy.close(); + } + }); + }); + }); + }); +} diff --git a/sdk/cosmosdb/cosmos/src/test/integration/query.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/query.spec.ts new file mode 100644 index 000000000000..c8af6f71df21 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/query.spec.ts @@ -0,0 +1,89 @@ +import assert from "assert"; +import { Constants, FeedOptions } from "../.."; +import { PartitionKind } from "../../documents"; +import { getTestContainer, getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +const doc = { id: "myId", pk: "pk" }; + +describe("ResourceLink Trimming of leading and trailing slashes", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 10000); + const containerId = "testcontainer"; + + beforeEach(async function() { + await removeAllDatabases(); + }); + + it("validate correct execution of query using named container link with leading and trailing slashes", async function() { + const containerDefinition = { + id: containerId, + partitionKey: { + paths: ["/pk"], + kind: PartitionKind.Hash + } + }; + const containerOptions = { offerThroughput: 10100 }; + + const container = await getTestContainer( + "validate correct execution of query", + undefined, + containerDefinition, + containerOptions + ); + + await container.items.create(doc); + const query = "SELECT * from " + containerId; + const queryOptions = { partitionKey: "pk" }; + const queryIterator = container.items.query(query, queryOptions); + + const { result } = await queryIterator.toArray(); + assert.equal(result[0]["id"], "myId"); + }); +}); + +describe("Test Query Metrics On Single Partition Collection", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 20000); + const collectionId = "testCollection2"; + + const testQueryMetricsOnSinglePartition = async function(document: any) { + try { + const database = await getTestDatabase("query metrics test db"); + + const collectionDefinition = { id: collectionId }; + const collectionOptions = { offerThroughput: 4000 }; + + const { body: createdCollectionDef } = await database.containers.create(collectionDefinition, collectionOptions); + const createdContainer = database.container(createdCollectionDef.id); + + await createdContainer.items.create(document); + const collectionLink = "/dbs/" + database.id + "/colls/" + collectionId + "/"; + const query = "SELECT * from " + collectionId; + const queryOptions: FeedOptions = { populateQueryMetrics: true }; + const queryIterator = createdContainer.items.query(query, queryOptions); + + while (queryIterator.hasMoreResults()) { + const { result: results, headers } = await queryIterator.executeNext(); + + if (results === undefined) { + // no more results + break; + } + + assert.notEqual(headers[Constants.HttpHeaders.QueryMetrics]["0"], null); + } + } catch (err) { + throw err; + } + }; + + afterEach(async function() { + await removeAllDatabases(); + }); + + beforeEach(async function() { + await removeAllDatabases(); + }); + + it("validate that query metrics are correct for a single partition query", async function() { + await testQueryMetricsOnSinglePartition(doc); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/queryMetrics.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/queryMetrics.spec.ts new file mode 100644 index 000000000000..4415400a1258 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/queryMetrics.spec.ts @@ -0,0 +1,199 @@ +import assert from "assert"; +import { Constants } from "../../common"; +import { + ClientSideMetrics, + QueryMetrics, + QueryPreparationTimes, + RuntimeExecutionTimes, + TimeSpan +} from "../../queryMetrics"; + +describe("QueryMetrics", function() { + // Properties + const totalQueryExecutionTime = TimeSpan.fromMilliseconds(33.67); + const queryCompilationTime = TimeSpan.fromMilliseconds(0.06); + const logicalPlanBuildTime = TimeSpan.fromMilliseconds(0.02); + const physicalPlanBuildTime = TimeSpan.fromMilliseconds(0.1); + const queryOptimizationTime = TimeSpan.fromMilliseconds(0.01); + const vmExecutionTime = TimeSpan.fromMilliseconds(32.56); + const indexLookupTime = TimeSpan.fromMilliseconds(0.36); + const documentLoadTime = TimeSpan.fromMilliseconds(9.58); + const systemFunctionExecutionTime = TimeSpan.fromMilliseconds(0.05); + const userDefinedFunctionExecutionTime = TimeSpan.fromMilliseconds(0.07); + const documentWriteTime = TimeSpan.fromMilliseconds(18.1); + const retrievedDocumentCount = 2000; + const retrievedDocumentSize = 1125600; + const outputDocumentCount = 2000; + const outputDocumentSize = 1125600; + const indexUtilizationRatio = 1.0; + const requestCharge = 42; + + const delimitedString = + "totalExecutionTimeInMs=33.67;queryCompileTimeInMs=0.06;queryLogicalPlanBuildTimeInMs=0.02;queryPhysicalPlanBuildTimeInMs=0.10;queryOptimizationTimeInMs=0.01;VMExecutionTimeInMs=32.56;indexLookupTimeInMs=0.36;documentLoadTimeInMs=9.58;systemFunctionExecuteTimeInMs=0.05;userFunctionExecuteTimeInMs=0.07;retrievedDocumentCount=2000;retrievedDocumentSize=1125600;outputDocumentCount=2000;outputDocumentSize=1125600;writeOutputTimeInMs=18.10;indexUtilizationRatio=1.00"; + + const queryEngineExecutionTime = TimeSpan.zero + .add(vmExecutionTime) + .subtract(indexLookupTime) + .subtract(documentLoadTime) + .subtract(documentWriteTime); + + // Base line query metrics + const queryMetrics = new QueryMetrics( + retrievedDocumentCount, + retrievedDocumentSize, + outputDocumentCount, + outputDocumentSize, + indexUtilizationRatio * retrievedDocumentCount, + totalQueryExecutionTime, + new QueryPreparationTimes(queryCompilationTime, logicalPlanBuildTime, physicalPlanBuildTime, queryOptimizationTime), + indexLookupTime, + documentLoadTime, + vmExecutionTime, + new RuntimeExecutionTimes(queryEngineExecutionTime, systemFunctionExecutionTime, userDefinedFunctionExecutionTime), + documentWriteTime, + new ClientSideMetrics(requestCharge) + ); + + const assertQueryMetricsEquality = function(queryMetrics1: QueryMetrics, queryMetrics2: QueryMetrics) { + assert.deepEqual(queryMetrics1.indexHitRatio, queryMetrics2.indexHitRatio); + assert.deepEqual(queryMetrics1.outputDocumentCount, queryMetrics2.outputDocumentCount); + assert.deepEqual(queryMetrics1.outputDocumentSize, queryMetrics2.outputDocumentSize); + assert.deepEqual(queryMetrics1.retrievedDocumentCount, queryMetrics2.retrievedDocumentCount); + assert.deepEqual(queryMetrics1.retrievedDocumentSize, queryMetrics2.retrievedDocumentSize); + assert.deepEqual(queryMetrics1.totalQueryExecutionTime, queryMetrics2.totalQueryExecutionTime); + + assert.deepEqual(queryMetrics1.documentLoadTime, queryMetrics2.documentLoadTime); + assert.deepEqual(queryMetrics1.documentWriteTime, queryMetrics2.documentWriteTime); + assert.deepEqual(queryMetrics1.indexLookupTime, queryMetrics2.indexLookupTime); + assert.deepEqual(queryMetrics1.vmExecutionTime, queryMetrics2.vmExecutionTime); + + assert.deepEqual( + queryMetrics1.queryPreparationTimes.logicalPlanBuildTime, + queryMetrics2.queryPreparationTimes.logicalPlanBuildTime + ); + assert.deepEqual( + queryMetrics1.queryPreparationTimes.physicalPlanBuildTime, + queryMetrics2.queryPreparationTimes.physicalPlanBuildTime + ); + assert.deepEqual( + queryMetrics1.queryPreparationTimes.queryCompilationTime, + queryMetrics2.queryPreparationTimes.queryCompilationTime + ); + assert.deepEqual( + queryMetrics1.queryPreparationTimes.queryOptimizationTime, + queryMetrics2.queryPreparationTimes.queryOptimizationTime + ); + + assert.deepEqual( + queryMetrics1.runtimeExecutionTimes.queryEngineExecutionTime, + queryMetrics2.runtimeExecutionTimes.queryEngineExecutionTime + ); + assert.deepEqual( + queryMetrics1.runtimeExecutionTimes.systemFunctionExecutionTime, + queryMetrics2.runtimeExecutionTimes.systemFunctionExecutionTime + ); + assert.deepEqual( + queryMetrics1.runtimeExecutionTimes.userDefinedFunctionExecutionTime, + queryMetrics2.runtimeExecutionTimes.userDefinedFunctionExecutionTime + ); + + assert.deepEqual(queryMetrics1.clientSideMetrics.requestCharge, queryMetrics2.clientSideMetrics.requestCharge); + }; + + it("Can Be Cloned", function() { + const queryMetrics2 = new QueryMetrics( + queryMetrics.retrievedDocumentCount, + queryMetrics.retrievedDocumentSize, + queryMetrics.outputDocumentCount, + queryMetrics.outputDocumentSize, + queryMetrics.indexHitDocumentCount, + queryMetrics.totalQueryExecutionTime, + queryMetrics.queryPreparationTimes, + queryMetrics.indexLookupTime, + queryMetrics.documentLoadTime, + queryMetrics.vmExecutionTime, + queryMetrics.runtimeExecutionTimes, + queryMetrics.documentWriteTime, + queryMetrics.clientSideMetrics + ); + + assertQueryMetricsEquality(queryMetrics, queryMetrics2); + }); + + it("Should Add Two Query Metrics", function() { + const doubleQueryMetrics = queryMetrics.add([queryMetrics]); + + const doubleRetrievedDocumentCount = retrievedDocumentCount * 2; + const doubleRetrievedDocumentSize = retrievedDocumentSize * 2; + const doubleOutputDocumentCount = outputDocumentCount * 2; + const doubleOutputDocumentSize = outputDocumentSize * 2; + const doubleIndexHitCount = indexUtilizationRatio * retrievedDocumentCount * 2; + const doubleTotalQueryExecutionTime = TimeSpan.fromMilliseconds(totalQueryExecutionTime.totalMilliseconds() * 2); + const doubleQueryCompilationTime = TimeSpan.fromMilliseconds(queryCompilationTime.totalMilliseconds() * 2); + const doubleLogicalPlanBuildTime = TimeSpan.fromMilliseconds(logicalPlanBuildTime.totalMilliseconds() * 2); + const doublePhysicalPlanBuildTime = TimeSpan.fromMilliseconds(physicalPlanBuildTime.totalMilliseconds() * 2); + const doubleIndexLookupTime = TimeSpan.fromMilliseconds(indexLookupTime.totalMilliseconds() * 2); + const doubleDocumentLoadTime = TimeSpan.fromMilliseconds(documentLoadTime.totalMilliseconds() * 2); + const doubleVMExecutionTime = TimeSpan.fromMilliseconds(vmExecutionTime.totalMilliseconds() * 2); + const doubleQueryOptimizationTime = TimeSpan.fromMilliseconds(queryOptimizationTime.totalMilliseconds() * 2); + const doubleQueryEngineExecutionTime = TimeSpan.fromMilliseconds(queryEngineExecutionTime.totalMilliseconds() * 2); + const doubleSystemFunctionExecutionTime = TimeSpan.fromMilliseconds( + systemFunctionExecutionTime.totalMilliseconds() * 2 + ); + const doubleUserDefinedFunctionExecutionTime = TimeSpan.fromMilliseconds( + userDefinedFunctionExecutionTime.totalMilliseconds() * 2 + ); + const doubleDocumentWriteTime = TimeSpan.fromMilliseconds(documentWriteTime.totalMilliseconds() * 2); + const doubleRequestCharge = requestCharge * 2; + + const expectedQueryMetrics = new QueryMetrics( + doubleRetrievedDocumentCount, + doubleRetrievedDocumentSize, + doubleOutputDocumentCount, + doubleOutputDocumentSize, + doubleIndexHitCount, + doubleTotalQueryExecutionTime, + new QueryPreparationTimes( + doubleQueryCompilationTime, + doubleLogicalPlanBuildTime, + doublePhysicalPlanBuildTime, + doubleQueryOptimizationTime + ), + doubleIndexLookupTime, + doubleDocumentLoadTime, + doubleVMExecutionTime, + new RuntimeExecutionTimes( + doubleQueryEngineExecutionTime, + doubleSystemFunctionExecutionTime, + doubleUserDefinedFunctionExecutionTime + ), + doubleDocumentWriteTime, + new ClientSideMetrics(doubleRequestCharge) + ); + + assertQueryMetricsEquality(doubleQueryMetrics, expectedQueryMetrics); + + const queryMetricsFromCreateArray = QueryMetrics.createFromArray([queryMetrics, queryMetrics]); + + assertQueryMetricsEquality(queryMetricsFromCreateArray, expectedQueryMetrics); + }); + + it("Can Be Create From Delimited String", function() { + const queryMetricsFromDelimitedString = QueryMetrics.createFromDelimitedString( + delimitedString, + new ClientSideMetrics(requestCharge) + ); + + assertQueryMetricsEquality(queryMetricsFromDelimitedString, queryMetrics); + }); + + it("Can Be Converted To A Delimited String", function() { + const delimitedStringFromMetrics = queryMetrics.toDelimitedString(); + const queryMetricsFromDelimitedString = QueryMetrics.createFromDelimitedString( + delimitedStringFromMetrics, + new ClientSideMetrics(requestCharge) + ); + + assertQueryMetricsEquality(queryMetrics, queryMetricsFromDelimitedString); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/retry.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/retry.spec.ts new file mode 100644 index 000000000000..6e2fd2428696 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/retry.spec.ts @@ -0,0 +1,141 @@ +import assert from "assert"; +import { AzureDocuments, Constants, CosmosClient, RetryOptions } from "../.."; +import * as request from "../../request"; + +describe("retry policy tests", function() { + this.timeout(300000); + const collectionDefinition = { + id: "sample collection" + }; + + const documentDefinition = { + id: "doc", + name: "sample document", + key: "value" + }; + + const connectionPolicy = new AzureDocuments.ConnectionPolicy(); + + // mocked database account to return the WritableLocations and ReadableLocations + // set with the default endpoint + // const mockGetDatabaseAccount = function (options, callback) { + // const databaseAccount = new AzureDocuments.DatabaseAccount(); + // callback(undefined, databaseAccount); + // }; + + const retryAfterInMilliseconds = 1000; + // // mocked request object stub that calls the callback with 429 throttling error + // const mockCreateRequestObjectStub = function (connectionPolicy, requestOptions, callback) { + // callback({ code: 429, body: "Request rate is too large", + // retryAfterInMilliseconds: retryAfterInMilliseconds }); + // }; + + // var mockCreateRequestObjectForDefaultRetryStub = function (connectionPolicy, requestOptions, callback) { + // global.counter++; + // if (global.counter % 5 == 0) + // return global.originalFunc(connectionPolicy, requestOptions, callback) + // else + // return callback({ code: "ECONNRESET", body: "Connection was reset" }) + // } + + // TODO: need to fix this, the stubbing doesn't work with the new way we work + xit("throttle retry policy test default retryAfter", async function() { + // connectionPolicy.RetryOptions = new RetryOptions(5); + // const client = new CosmosClient({endpoint, auth: { masterKey }, connectionPolicy}); + // const { result: db } = await client.createDatabase({ id: "sample database" }); + // const { result: collection } = await client.createCollection(db._self, collectionDefinition); + // const originalGetDatabaseAccount = client.getDatabaseAccount; + // client.getDatabaseAccount = mockGetDatabaseAccount; + // const originalCreateRequestObjectStub = request._createRequestObjectStub; + // request._createRequestObjectStub = mockCreateRequestObjectStub; + // try { + // const { result: createdDocument } = + // await client.createDocument(collection._self, documentDefinition); + // } catch (err) { + // const responseHeaders = (err as request.ErrorResponse).headers; + // assert.equal(err.code, 429, "invalid error code"); + // assert.equal(responseHeaders[Constants.ThrottleRetryCount], + // connectionPolicy.RetryOptions.MaxRetryAttemptCount, "Current retry attempts not maxed out"); + // assert.ok(responseHeaders[Constants.ThrottleRetryWaitTimeInMs] + // >= connectionPolicy.RetryOptions.MaxRetryAttemptCount * retryAfterInMilliseconds); + // } + // request._createRequestObjectStub = originalCreateRequestObjectStub; + // client.getDatabaseAccount = originalGetDatabaseAccount; + }); + + xit("throttle retry policy test fixed retryAfter", async function() { + // connectionPolicy.RetryOptions = new RetryOptions(5, 2000); + // const client = new CosmosClient(endpoint, { masterKey }, connectionPolicy); + // const { result: db } = await client.createDatabase({ id: "sample database" }); + // const { result: collection } = await client.createCollection(db._self, collectionDefinition); + // const originalGetDatabaseAccount = client.getDatabaseAccount; + // client.getDatabaseAccount = mockGetDatabaseAccount; + // const originalCreateRequestObjectStub = request._createRequestObjectStub; + // request._createRequestObjectStub = mockCreateRequestObjectStub; + // try { + // await client.createDocument(collection._self, documentDefinition); + // assert.fail("Must throw"); + // } catch (err) { + // const responseHeaders = (err as request.ErrorResponse).headers; + // assert.equal(err.code, 429, "invalid error code"); + // assert.equal(responseHeaders[Constants.ThrottleRetryCount], + // connectionPolicy.RetryOptions.MaxRetryAttemptCount, "Current retry attempts not maxed out"); + // assert.ok(responseHeaders[Constants.ThrottleRetryWaitTimeInMs] + // >= connectionPolicy.RetryOptions.MaxRetryAttemptCount + // * connectionPolicy.RetryOptions.FixedRetryIntervalInMilliseconds); + // } + // request._createRequestObjectStub = originalCreateRequestObjectStub; + // client.getDatabaseAccount = originalGetDatabaseAccount; + }); + + xit("throttle retry policy test max wait time", async function() { + // connectionPolicy.RetryOptions = new RetryOptions(5, 2000, 3); + // const client = new CosmosClient(endpoint, { masterKey }, connectionPolicy); + // const { result: db } = await client.createDatabase({ id: "sample database" }); + // const { result: collection } = await client.createCollection(db._self, collectionDefinition); + // const originalGetDatabaseAccount = client.getDatabaseAccount; + // client.getDatabaseAccount = mockGetDatabaseAccount; + // const originalCreateRequestObjectStub = request._createRequestObjectStub; + // request._createRequestObjectStub = mockCreateRequestObjectStub; + // try { + // await client.createDocument(collection._self, documentDefinition); + // } catch (err) { + // const responseHeaders = (err as request.ErrorResponse).headers; + // assert.equal(err.code, 429, "invalid error code"); + // assert.ok(responseHeaders[Constants.ThrottleRetryWaitTimeInMs] + // >= connectionPolicy.RetryOptions.MaxWaitTimeInSeconds * 1000); + // } + // request._createRequestObjectStub = originalCreateRequestObjectStub; + // client.getDatabaseAccount = originalGetDatabaseAccount; + }); + + xit("default retry policy validate create failure", async function() { + // const client = new CosmosClient(endpoint, { masterKey }, connectionPolicy); + // const { result: db } = await client.createDatabase({ id: "sample database" }); + // const { result: collection } = await client.createCollection(db._self, collectionDefinition); + // global.originalFunc = request._createRequestObjectStub; + // global.counter = 0; + // request._createRequestObjectStub = mockCreateRequestObjectForDefaultRetryStub; + // try { + // await client.createDocument(collection._self, documentDefinition); + // } catch (err) { + // assert.equal(err.code, "ECONNRESET", "invalid error code"); + // // assert.equal(global.counter, 6, "invalid number of retries"); + // } + // request._createRequestObjectStub = global.originalFunc; + }); + + xit("default retry policy validate read success", async function() { + // const client = new CosmosClient(endpoint, { masterKey }, connectionPolicy); + // const { result: db } = await client.createDatabase({ id: "sample database" }); + // const { result: collection } = await client.createCollection(db._self, collectionDefinition); + // const { result: createdDocument } = await client.createDocument(collection._self, documentDefinition); + // global.originalFunc = request._createRequestObjectStub; + // global.counter = 0; + // request._createRequestObjectStub = mockCreateRequestObjectForDefaultRetryStub; + // const { result: readDocument } = await client.readDocument(createdDocument._self); + // assert.equal(readDocument.id, documentDefinition.id, "invalid document id"); + // assert.equal(global.counter, 5, "invalid number of retries"); + // request._createRequestObjectStub = global.originalFunc; + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/ruPerMin.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/ruPerMin.spec.ts new file mode 100644 index 000000000000..54cb5fb31883 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/ruPerMin.spec.ts @@ -0,0 +1,85 @@ +import assert from "assert"; +import { Constants, CosmosClient, Database } from "../.."; +import { endpoint, masterKey } from "../common/_testConfig"; +import { getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +const client = new CosmosClient({ endpoint, auth: { masterKey } }); + +// TODO: these tests are all disabled + +describe("RU Per Minute", function() { + let database: Database; + + // - removes all the databases, + // - creates a new database, + beforeEach(async () => { + await removeAllDatabases(); + database = await getTestDatabase("RU Per minute"); + }); + + // - removes all the databases, + afterEach(async () => { + await removeAllDatabases(); + }); + + xit("Create container with RU Per Minute Offer", async function() { + const containerDefinition = { + id: "sample col" + }; + + const options = { + offerEnableRUPerMinuteThroughput: true, + offerVersion: "V2", + offerThroughput: 400 + }; + + await database.containers.create(containerDefinition, options); + const { result: offers } = await client.offers.readAll().toArray(); + assert.equal(offers.length, 1); + const offer = offers[0]; + + assert.equal(offer.offerType, "Invalid"); + assert.notEqual(offer.content, undefined); + assert.equal(offer.content.offerIsRUPerMinuteThroughputEnabled, true); + }); + + xit("Create container without RU Per Minute Offer", async function() { + const containerDefinition = { + id: "sample col" + }; + + const options = { + offerVersion: "V2", + offerThroughput: 400 + }; + + await database.containers.create(containerDefinition, options); + const { result: offers } = await client.offers.readAll().toArray(); + assert.equal(offers.length, 1); + const offer = offers[0]; + + assert.equal(offer.offerType, "Invalid"); + assert.notEqual(offer.content, undefined); + assert.equal(offer.content.offerIsRUPerMinuteThroughputEnabled, false); + }); + + xit("Create container with RU Per Minute Offer and insert Document with disableRUPerMinuteUsage options", async function() { + const containerDefinition = { + id: "sample col" + }; + + const options = { + offerEnableRUPerMinuteThroughput: true, + offerVersion: "V2", + offerThroughput: 400 + }; + + await database.containers.create(containerDefinition, options); + const container = database.container(containerDefinition.id); + const options2: any = { + disableRUPerMinuteUsage: true + }; + const { headers } = await container.items.create({ id: "sample document" }, options2); + assert(headers[Constants.HttpHeaders.IsRUPerMinuteUsed] !== true); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/session.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/session.spec.ts new file mode 100644 index 000000000000..1d0cb0f783d0 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/session.spec.ts @@ -0,0 +1,366 @@ +import assert from "assert"; +import * as sinon from "sinon"; +import { Constants, CosmosClient, IHeaders } from "../.."; +import { ClientContext } from "../../ClientContext"; +import { Helper } from "../../common"; +import { ConsistencyLevel, PartitionKind } from "../../documents"; +import { RequestHandler } from "../../request"; +import { SessionContainer } from "../../session/sessionContainer"; +import { VectorSessionToken } from "../../session/VectorSessionToken"; +import { endpoint, masterKey } from "../common/_testConfig"; +import { getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; + +// TODO: there is alot of "any" types for tokens here +// TODO: there is alot of leaky document client stuff here that will make removing document client hard + +const client = new CosmosClient({ + endpoint, + auth: { masterKey }, + consistencyLevel: ConsistencyLevel.Session +}); + +function getCollection2TokenMap(sessionContainer: SessionContainer): Map> { + return (sessionContainer as any).collectionResourceIdToSessionTokens; +} + +describe("Session Token", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 20000); + + const containerId = "sessionTestColl"; + + const containerDefinition = { + id: containerId, + partitionKey: { paths: ["/id"], kind: PartitionKind.Hash } + }; + const containerOptions = { offerThroughput: 25100 }; + + const clientContext: ClientContext = (client as any).clientContext; + const requestHandler: RequestHandler = (clientContext as any).requestHandler; + const sessionContainer: SessionContainer = (clientContext as any).sessionContainer; + + const getSpy = sinon.spy(requestHandler, "get"); + const postSpy = sinon.spy(requestHandler, "post"); + const putSpy = sinon.spy(requestHandler, "put"); + const deleteSpy = sinon.spy(requestHandler, "delete"); + + beforeEach(async function() { + await removeAllDatabases(); + }); + + it("validate session tokens for sequence of operations", async function() { + const database = await getTestDatabase("session test", client); + + const { body: createdContainerDef } = await database.containers.create(containerDefinition, containerOptions); + const container = database.container(createdContainerDef.id); + assert.equal(postSpy.lastCall.args[3][Constants.HttpHeaders.SessionToken], undefined); + // TODO: testing implementation detail by looking at containerResourceIdToSesssionTokens + let collRid2SessionToken: Map> = (sessionContainer as any) + .collectionResourceIdToSessionTokens; + assert.equal(collRid2SessionToken.size, 0, "Should have no tokens in container"); + + const { body: document1 } = await container.items.create({ id: "1" }); + assert.equal( + postSpy.lastCall.args[3][Constants.HttpHeaders.SessionToken], + undefined, + "Initial create token should be qual" + ); + + collRid2SessionToken = getCollection2TokenMap(sessionContainer); + assert.equal(collRid2SessionToken.size, 1, "Should only have one container in the sessioncontainer"); + const containerRid = collRid2SessionToken.keys().next().value; + let containerTokens = collRid2SessionToken.get(containerRid); + assert.equal(containerTokens.size, 1, "Should only have one partition in container"); + const firstPartition = containerTokens.keys().next().value; + let firstPartitionToken = containerTokens.get(firstPartition); + assert.notEqual(firstPartitionToken, "Should have a token for first partition"); + + const token = sessionContainer.get({ + isNameBased: true, + operationType: "create", + resourceAddress: container.url, + resourceType: "docs", + resourceId: "2" + }); + const { body: document2 } = await container.items.create({ id: "2" }); + assert.equal(postSpy.lastCall.args[3][Constants.HttpHeaders.SessionToken], token, "create token should be equal"); + + collRid2SessionToken = getCollection2TokenMap(sessionContainer); + assert.equal(collRid2SessionToken.size, 1, "Should only have one container in the sessioncontainer"); + containerTokens = collRid2SessionToken.get(containerRid); + assert.equal(containerTokens.size, 2, "Should have two partitions in container"); + const keysIterator = containerTokens.keys(); + keysIterator.next(); // partition 1 + const secondPartition = keysIterator.next().value; + assert.equal( + containerTokens.get(firstPartition).toString(), + firstPartitionToken.toString(), + "First partition token should still match after create" + ); + let secondPartitionToken = containerTokens.get(secondPartition); + assert(secondPartitionToken, "Should have a LSN for second partition"); + + const readToken = sessionContainer.get({ + isNameBased: true, + operationType: "read", + resourceAddress: container.url, + resourceType: "docs", + resourceId: "1" + }); + await container.item(document1.id, "1").read(); + assert.equal(getSpy.lastCall.args[2][Constants.HttpHeaders.SessionToken], readToken, "read token should be equal"); + + collRid2SessionToken = getCollection2TokenMap(sessionContainer); + assert.equal(collRid2SessionToken.size, 1, "Should only have one container in the sessioncontainer"); + containerTokens = collRid2SessionToken.get(containerRid); + assert.equal(containerTokens.size, 2, "Should have two partitions in container"); + assert.equal( + containerTokens.get(firstPartition).toString(), + firstPartitionToken.toString(), + "First partition token should still match after read" + ); + assert.equal( + containerTokens.get(secondPartition).toString(), + secondPartitionToken.toString(), + "Second partition token should still match after read" + ); + + const upsertToken = sessionContainer.get({ + isNameBased: true, + operationType: "upsert", + resourceAddress: container.url, + resourceType: "docs", + resourceId: "1" + }); + const { body: document13 } = await container.items.upsert({ id: "1", operation: "upsert" }, { partitionKey: "1" }); + assert.equal( + postSpy.lastCall.args[3][Constants.HttpHeaders.SessionToken], + upsertToken, + "upsert token should be equal" + ); + + collRid2SessionToken = getCollection2TokenMap(sessionContainer); + assert.equal(collRid2SessionToken.size, 1, "Should only have one container in the sessioncontainer"); + containerTokens = collRid2SessionToken.get(containerRid); + assert.equal(containerTokens.size, 2, "Should have two partitions in container"); + // TODO: should validate the LSN only increased by 1... + assert.notEqual( + containerTokens.get(firstPartition).toString(), + firstPartitionToken.toString(), + "First partition token should no longer match after upsert" + ); + assert.equal( + containerTokens.get(secondPartition).toString(), + secondPartitionToken.toString(), + "Second partition token should still match after upsert" + ); + firstPartitionToken = containerTokens.get(firstPartition); + + const deleteToken = sessionContainer.get({ + isNameBased: true, + operationType: "delete", + resourceAddress: container.url, + resourceType: "docs", + resourceId: "2" + }); + await container.item(document2.id, "2").delete(); + assert.equal( + deleteSpy.lastCall.args[2][Constants.HttpHeaders.SessionToken], + deleteToken, + "delete token should be equal" + ); + + collRid2SessionToken = getCollection2TokenMap(sessionContainer); + assert.equal(collRid2SessionToken.size, 1, "Should only have one container in the sessioncontainer"); + containerTokens = collRid2SessionToken.get(containerRid); + assert.equal(containerTokens.size, 2, "Should have two partitions in container"); + assert.equal( + containerTokens.get(firstPartition).toString(), + firstPartitionToken.toString(), + "First partition token should still match delete" + ); + // TODO: should validate the LSN only increased by 1... + assert.notEqual( + containerTokens.get(secondPartition).toString(), + secondPartitionToken.toString(), + "Second partition token should not match after delete" + ); + secondPartitionToken = containerTokens.get(secondPartition); + + const replaceToken = sessionContainer.get({ + isNameBased: true, + operationType: "replace", + resourceAddress: container.url, + resourceType: "docs", + resourceId: "1" + }); + await container.item(document13.id).replace({ id: "1", operation: "replace" }, { partitionKey: "1" }); + assert.equal( + putSpy.lastCall.args[3][Constants.HttpHeaders.SessionToken], + replaceToken, + "replace token should be equal" + ); + collRid2SessionToken = getCollection2TokenMap(sessionContainer); + assert.equal(collRid2SessionToken.size, 1, "Should only have one container in the sessioncontainer"); + containerTokens = collRid2SessionToken.get(containerRid); + assert.equal(containerTokens.size, 2, "Should have two partitions in container"); + // TODO: should validate the LSN only increased by 1... + assert.notEqual( + containerTokens.get(firstPartition).toString(), + firstPartitionToken.toString(), + "First partition token should no longer match after replace" + ); + assert.equal( + containerTokens.get(secondPartition).toString(), + secondPartitionToken.toString(), + "Second partition token should still match after replace" + ); + firstPartitionToken = containerTokens.get(firstPartition); + + const query = "SELECT * from " + containerId; + const queryOptions = { partitionKey: "1" }; + const queryIterator = container.items.query(query, queryOptions); + + const queryToken = sessionContainer.get({ + isNameBased: true, + operationType: "query", + resourceAddress: container.url, + resourceType: "docs" + }); + await queryIterator.toArray(); + assert.equal(postSpy.lastCall.args[3][Constants.HttpHeaders.SessionToken], queryToken); + + collRid2SessionToken = getCollection2TokenMap(sessionContainer); + assert.equal(collRid2SessionToken.size, 1, "Should only have one container in the sessioncontainer"); + containerTokens = collRid2SessionToken.get(containerRid); + assert.equal(containerTokens.size, 2, "Should have two partitions in container"); + assert.equal( + containerTokens.get(firstPartition).toString(), + firstPartitionToken.toString(), + "First partition token should still match after query" + ); + assert.equal( + containerTokens.get(secondPartition).toString(), + secondPartitionToken.toString(), + "Second partition token should still match after query" + ); + + const deleteContainerToken = sessionContainer.get({ + isNameBased: true, + operationType: "delete", + resourceAddress: container.url, + resourceType: "container", + resourceId: container.id + }); + await container.delete(); + assert.equal( + deleteSpy.lastCall.args[2][Constants.HttpHeaders.SessionToken], + deleteContainerToken, + "delete container token should match" + ); + collRid2SessionToken = getCollection2TokenMap(sessionContainer); + assert.equal(collRid2SessionToken.size, 0, "collRid map should be empty on container delete"); + + getSpy.restore(); + postSpy.restore(); + deleteSpy.restore(); + putSpy.restore(); + }); + + it("validate 'lsn not caught up' error for higher lsn and clearing session token", async function() { + this.retries(2); + const database = await getTestDatabase("session test", client); + + const containerLink = "dbs/" + database.id + "/colls/" + containerId; + const increaseLSN = function(oldTokens: Map>) { + for (const [coll, tokens] of oldTokens.entries()) { + for (const [pk, token] of tokens.entries()) { + (token as any).globalLsn = (token as any).globalLsn + 200; + const newToken = token.merge(token); + return `0:${newToken.toString()}`; + } + } + throw new Error("No valid token found to increase"); + }; + + await database.containers.create(containerDefinition, containerOptions); + const container = database.container(containerDefinition.id); + const { headers } = await container.items.create({ id: "1" }); + const callbackSpy = sinon.spy(function(path: string, reqHeaders: IHeaders) { + const oldTokens = getCollection2TokenMap(sessionContainer); + reqHeaders[Constants.HttpHeaders.SessionToken] = increaseLSN(oldTokens); + }); + const applySessionTokenStub = sinon.stub(clientContext as any, "applySessionToken").callsFake(callbackSpy); + try { + await container.item("1").read({ partitionKey: "1" }); + assert.fail("readDocument must throw"); + } catch (err) { + assert.equal(err.substatus, 1002, "Substatus should indicate the LSN didn't catchup."); + assert.equal(callbackSpy.callCount, 1); + assert.equal(Helper.trimSlashes(callbackSpy.lastCall.args[0]), containerLink + "/docs/1"); + } finally { + applySessionTokenStub.restore(); + } + await container.item("1").read({ partitionKey: "1" }); + }); + + // TODO: chrande - looks like this might be broken by going name based? + // We never had a name based version of this test. Looks like we fail to set the session token + // because OwnerId is missing on the header. This only happens for name based. + it.skip("client should not have session token of a container created by another client", async function() { + const client2 = new CosmosClient({ + endpoint, + auth: { masterKey }, + consistencyLevel: ConsistencyLevel.Session + }); + const database = await getTestDatabase("clientshouldnothaveanotherclienttoken"); + await database.containers.create(containerDefinition, containerOptions); + const container = database.container(containerDefinition.id); + await container.read(); + await client2 + .database(database.id) + .container(containerDefinition.id) + .delete(); + await client2.database(database.id).containers.create(containerDefinition, containerOptions); + await client2 + .database(database.id) + .container(containerDefinition.id) + .read(); + assert.equal((client as any).clientContext.getSessionToken(container.url), ""); // TODO: _self + assert.notEqual((client2 as any).clientContext.getSessionToken(container.url), ""); + }); + + it("validate session container update on 'Not found' with 'undefined' status code for non master resource", async function() { + const client2 = new CosmosClient({ + endpoint, + auth: { masterKey }, + consistencyLevel: ConsistencyLevel.Session + }); + + const db = await getTestDatabase("session test", client); + + const { body: createdContainerDef } = await db.containers.create(containerDefinition, containerOptions); + const createdContainer = db.container(createdContainerDef.id); + + const { body: createdDocument } = await createdContainer.items.create({ + id: "1" + }); + const requestOptions = { partitionKey: "1" }; + await client2 + .database(db.id) + .container(createdContainerDef.id) + .item(createdDocument.id) + .delete(requestOptions); + const setSessionTokenSpy = sinon.spy(sessionContainer, "set"); + + try { + await createdContainer.item(createdDocument.id).read(requestOptions); + assert.fail("Must throw"); + } catch (err) { + assert.equal(err.code, 404, "expecting 404 (Not found)"); + assert.equal(err.substatus, undefined, "expecting substatus code to be undefined"); + assert.equal(setSessionTokenSpy.callCount, 1, "unexpected number of calls to sesSessionToken"); + } finally { + setSessionTokenSpy.restore(); + } + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/split.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/split.spec.ts new file mode 100644 index 000000000000..35e34a615cd9 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/split.spec.ts @@ -0,0 +1,445 @@ +// import assert from "assert"; +// import { execFileSync, spawnSync } from "child_process"; +// import * as Stream from "stream"; +// import * as _ from "underscore"; +// import * as util from "util"; +// import { +// AzureDocuments, Base, Constants, CosmosClient, +// DocumentBase, HashPartitionResolver, Range, +// RangePartitionResolver, RetryOptions, +// } from "../../"; +// import { HeaderUtils } from "../../queryExecutionContext"; +// import testConfig from "./../common/_testConfig"; +// import { TestHelpers } from "./../common/TestHelpers"; + +// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +// const host = testConfig.host; +// const masterKey = testConfig.masterKey; +// const adminUtilitiesPath = testConfig.adminUtilitiesPath; +// const splitRangeCommand = "SplitRange"; +// const partitionKey = "key"; +// const stopWorkload = false; + +// const SplitMethod = { +// EqualRange: 0, +// EqualCount: 1, +// Explicit: 2, +// }; + +// // TODO: These tests are currently disabled. Should remove them or re-enable them. + +// describe.skip("NodeJS Split Tests", function () { +// const generateDocuments = function (docSize) { +// const docs = []; +// for (let i: number = 0; i < docSize; i++) { +// const d = { +// id: i.toString(), +// name: "sample document", +// spam: "eggs" + i.toString(), +// cnt: i, +// key: "value", +// spam2: (i === 3) ? "eggs" + i.toString() : i, +// boolconst: (i % 2 === 0), +// number: 1.1 * i, + +// }; +// docs.push(d); +// } +// return docs; +// }; + +// describe("Validate Split", function () { +// const client = new CosmosClient(host, { masterKey }); +// const documentDefinitions = generateDocuments(20); +// // Global constiable to determine if we should split after a round trip. +// let shouldSplit = true; +// let db: any; +// let collection: any; +// const isNameBased = false; +// // - removes all the databases, +// // - creates a new database, +// // - creates a new collecton, +// // - bulk inserts documents to the collection +// beforeEach(async function () { +// try { +// shouldSplit = true; +// TestHelpers.removeAllDatabases(host, masterKey); + +// const { result: createdDB } = await client.createDatabase({ id: "sample 中文 database" }); +// db = createdDB; + +// const collectionDefinition = { +// id: "sample collection", +// indexingPolicy: { +// includedPaths: [ +// { +// path: "/", +// indexes: [ +// { +// kind: "Range", +// dataType: "Number", +// }, +// { +// kind: "Range", +// dataType: "String", +// }, +// ], +// }, +// ], +// }, +// partitionKey: { +// paths: [ +// "/id", +// ], +// kind: "Hash", +// }, +// }; + +// const collectionOptions = { offerThroughput: 10100 }; +// const { result: createdCollection } = +// await client.createCollection("dbs/sample 中文 database", collectionDefinition, collectionOptions); +// collection = createdCollection; +// await TestHelpers.bulkInsertDocuments(client, isNameBased, db, collection, documentDefinitions); +// } catch (err) { +// throw err; +// } +// }); + +// const executeSplitRange = function (collectionRid, partitionKeyRangeId, minimumAllowedFraction, splitMethod) { +// console.log("Launching Command: "); +// const args = [splitRangeCommand, collectionRid, partitionKeyRangeId, minimumAllowedFraction, splitMethod]; +// const childProcess = spawnSync(adminUtilitiesPath, args, { stdio: "inherit" }); +// assert.equal(childProcess.status, 0); +// }; + +// const validateResults = function (actualResults, expectedOrderIds) { +// assert.equal(actualResults.length, expectedOrderIds.length, +// "actual results length doesn't match with expected results length."); + +// for (let i = 0; i < actualResults.length; i++) { +// assert.equal(actualResults[i].id, expectedOrderIds[i], +// "actual result content doesn't match with expected result content."); +// } +// }; + +// const validateToArray = function (queryIterator, options, expectedOrderIds, done) { + +// //////////////////////////////// +// // validate toArray() +// //////////////////////////////// +// options.continuation = undefined; +// const toArrayVerifier = function (err, results) { +// assert.equal(err, undefined, "unexpected failure in fetching the results: " + JSON.stringify(err)); +// assert.equal(results.length, expectedOrderIds.length, "invalid number of results"); +// assert.equal(queryIterator.hasMoreResults(), false, "hasMoreResults: no more results is left"); + +// validateResults(results, expectedOrderIds); +// return done(); +// }; + +// queryIterator.toArray(toArrayVerifier); +// }; + +// const validateNextItem = function (queryIterator, options, expectedOrderIds, done) { + +// //////////////////////////////// +// // validate nextItem() +// //////////////////////////////// +// const results = []; +// const nextItemVerifier = function (err, item) { +// assert.equal(err, undefined, "unexpected failure in fetching the results: " + err); +// if (item === undefined) { +// assert(!queryIterator.hasMoreResults(), "hasMoreResults must signal results exhausted"); +// validateResults(results, expectedOrderIds); + +// return done(); +// } +// results = results.concat(item); + +// if (results.length < expectedOrderIds.length) { +// assert(queryIterator.hasMoreResults(), "hasMoreResults must indicate more results"); +// } +// return queryIterator.nextItem(nextItemVerifier); +// }; + +// queryIterator.nextItem(nextItemVerifier); +// }; + +// const validateNextItemAndCurrentAndHasMoreResults = function (queryIterator, options, expectedOrderIds, done) { +// // curent and nextItem recursively invoke each other till queryIterator is exhausted +// //////////////////////////////// +// // validate nextItem() +// //////////////////////////////// +// const results = []; +// const nextItemVerifier = function (err, item) { + +// //////////////////////////////// +// // validate current() +// //////////////////////////////// +// const currentVerifier = function (err, currentItem) { +// assert.equal(err, undefined, "unexpected failure in fetching the results: " + err); +// assert.equal(item, currentItem, "current must give the previously item returned by nextItem"); + +// if (currentItem === undefined) { +// assert(!queryIterator.hasMoreResults(), "hasMoreResults must signal results exhausted"); +// validateResults(results, expectedOrderIds); + +// return done(); +// } + +// if (results.length < expectedOrderIds.length) { +// assert(queryIterator.hasMoreResults(), "hasMoreResults must indicate more results"); +// } + +// return queryIterator.nextItem(nextItemVerifier); +// }; + +// assert.equal(err, undefined, "unexpected failure in fetching the results: " + err); + +// if (item === undefined) { +// assert(!queryIterator.hasMoreResults(), "hasMoreResults must signal results exhausted"); +// validateResults(results, expectedOrderIds); + +// return queryIterator.current(currentVerifier); +// } +// results = results.concat(item); + +// if (results.length < expectedOrderIds.length) { +// assert(queryIterator.hasMoreResults(), "hasMoreResults must indicate more results"); +// } + +// const currentVerifier = function (err, currentItem) { +// queryIterator.nextItem(nextItemVerifier); +// } + +// return queryIterator.current(currentVerifier); +// }; +// queryIterator.nextItem(nextItemVerifier); +// }; + +// const validateExecuteNextWithGivenContinuationToken = function (collectionLink, query, origOptions, listOfResultPages, listOfHeaders, done) { +// const options = JSON.parse(JSON.stringify(origOptions)); +// const expectedResults = listOfResultPages.shift(); +// const headers = listOfHeaders.shift(); +// if (headers === undefined) { +// assert(listOfHeaders.length === 0, "only last header is empty"); +// assert(listOfResultPages.length === 0); +// return done(); +// } + +// assert.notEqual(expectedResults, undefined); + +// const continuationToken = headers[Constants.HttpHeaders.Continuation]; + +// const fromTokenValidator = function (token, expectedResultsFromToken, expectedHeadersFromToken) { +// options.continuation = token; +// const queryIterator = client.queryDocuments(collectionLink, query, options); + +// const fromTokenToLastPageValidator = function (queryIterator, token, expectedResultsFromToken, expectedHeadersFromToken) { + +// // validates single page result and +// const resultPageValidator = function (err, resources, headers) { +// assert.equal(err, undefined, "unexpected failure in fetching the results: " + err + JSON.stringify(err)); + +// const exptectedResultPage = expectedResultsFromToken.shift(); +// const expectedHeaders = expectedHeadersFromToken.shift(); +// if (exptectedResultPage === undefined) { +// assert.equal(resources, undefined); +// assert.equal(headers, undefined); +// } else { + +// validateResults(resources, exptectedResultPage.map( +// function (r) { +// return r["id"]; +// })); + +// if (expectedHeaders) { +// assert.equal( +// headers[Constants.HttpHeaders.Continuation], +// expectedHeaders[Constants.HttpHeaders.Continuation]); +// } else { +// assert.equal(headers, undefined); +// } +// } + +// if (expectedHeadersFromToken.length > 0) { +// return fromTokenToLastPageValidator(queryIterator, token, expectedResultsFromToken, expectedHeadersFromToken); +// } else { +// // start testing from next continuation token ... +// return validateExecuteNextWithGivenContinuationToken(collectionLink, query, options, listOfResultPages, listOfHeaders, done); +// } +// } +// queryIterator.executeNext(resultPageValidator); +// } +// return fromTokenToLastPageValidator(queryIterator, continuationToken, listOfResultPages, listOfHeaders); +// } +// return fromTokenValidator(continuationToken, listOfResultPages, listOfHeaders); +// } + +// const validateExecuteNextAndHasMoreResults = function (collectionLink, query, options, queryIterator, expectedOrderIds, done, +// validateExecuteNextWithContinuationToken) { +// const pageSize = options["maxItemCount"]; + +// //////////////////////////////// +// // validate executeNext() +// //////////////////////////////// + +// const listOfResultPages = []; +// const listOfHeaders = []; + +// const totalFetchedResults = []; +// const executeNextValidator = function (err, results, headers) { +// // CollectionRid is case sensitive. +// const collectionRid = collectionLink.split("/")[3]; + +// // Spliting to test split proof after retrieving the page +// if (shouldSplit) { +// executeSplitRange(collectionRid, "0", "0.1", "EqualRange"); +// shouldSplit = false; +// } + +// listOfResultPages.push(results); +// listOfHeaders.push(headers); + +// assert.equal(err, undefined, "unexpected failure in fetching the results: " + err + JSON.stringify(err)); +// if (results === undefined || (totalFetchedResults.length === expectedOrderIds.length)) { +// // no more results +// validateResults(totalFetchedResults, expectedOrderIds); +// assert.equal(queryIterator.hasMoreResults(), false, "hasMoreResults: no more results is left"); +// assert.equal(results, undefined, "unexpected more results" + JSON.stringify(results)); +// if (validateExecuteNextWithContinuationToken) { +// return validateExecuteNextWithGivenContinuationToken( +// collectionLink, query, options, listOfResultPages, listOfHeaders, done, +// ); +// } else { +// return done(); +// } +// } + +// totalFetchedResults = totalFetchedResults.concat(results); + +// if (totalFetchedResults.length < expectedOrderIds.length) { +// // there are more results +// assert(results.length <= pageSize, "executeNext: invalid fetch block size"); +// //if (validateExecuteNextWithContinuationToken) { +// // assert(results.length <= pageSize, "executeNext: invalid fetch block size"); +// // } else { +// // assert.equal(results.length, pageSize, "executeNext: invalid fetch block size"); + +// // } +// assert(queryIterator.hasMoreResults(), "hasMoreResults expects to return true"); +// return queryIterator.executeNext(executeNextValidator); + +// } else { +// // no more results +// assert.equal(expectedOrderIds.length, totalFetchedResults.length, "executeNext: didn't fetch all the results"); +// assert(results.length <= pageSize, "executeNext: actual fetch size is more than the requested page size"); + +// // validate that next execute returns undefined resources +// return queryIterator.executeNext(executeNextValidator); +// } +// }; + +// queryIterator.executeNext(executeNextValidator); +// } + +// const validateForEach = function (queryIterator, options, expectedOrderIds, done) { + +// //////////////////////////////// +// // validate forEach() +// //////////////////////////////// +// const results = []; +// const callbackSingnalledEnd = false; +// const forEachCallback = function (err, item) { +// assert.equal(err, undefined, "unexpected failure in fetching the results: " + err + JSON.stringify(err)); +// // if the previous invocation returned false, forEach must avoid invoking the callback again! +// assert.equal(callbackSingnalledEnd, false, "forEach called callback after the first false returned"); +// results = results.concat(item); +// if (results.length === expectedOrderIds.length) { +// callbackSingnalledEnd = true; +// validateResults(results, expectedOrderIds); +// process.nextTick(done); +// return false +// } +// return true; +// }; + +// queryIterator.forEach(forEachCallback); +// } + +// const executeQueryAndValidateResults = function (collectionLink, query, options, expectedOrderIds, done, validateExecuteNextWithContinuationToken) { + +// validateExecuteNextWithContinuationToken = validateExecuteNextWithContinuationToken || false; +// const queryIterator = client.queryDocuments(collectionLink, query, options); + +// validateToArray(queryIterator, options, expectedOrderIds, +// function () { +// queryIterator.reset(); +// validateExecuteNextAndHasMoreResults(collectionLink, query, options, queryIterator, expectedOrderIds, +// function () { +// queryIterator.reset(); +// validateNextItemAndCurrentAndHasMoreResults(queryIterator, options, expectedOrderIds, +// function () { +// validateForEach(queryIterator, options, expectedOrderIds, done); +// }, +// ); +// }, +// validateExecuteNextWithContinuationToken, +// ); +// }, +// ); +// }; +// // We can only have 5 split test cases, since the VM will only let us split 10 times + +// // Parallel Query Tests +// it("Validate Parallel Query As String With maxDegreeOfParallelism: 3", function (done) { +// // simple order by query in string format +// const query = "SELECT * FROM root r"; +// const options = { enableCrossPartitionQuery: true, maxItemCount: 2, maxDegreeOfParallelism: 3 }; + +// const expectedOrderedIds = [1, 10, 18, 2, 3, 13, 14, 16, 17, 0, 11, 12, 5, 9, 19, 4, 6, 7, 8, 15]; + +// // validates the results size and order +// executeQueryAndValidateResults(getCollectionLink(isNameBased, db, collection), query, options, expectedOrderedIds, done); +// }); + +// // OrderBy Tests +// it("Validate Simple OrderBy Query As String With maxDegreeOfParallelism = 3", function (done) { +// // simple order by query in string format +// const query = "SELECT * FROM root r order by r.spam"; +// const options = { enableCrossPartitionQuery: true, maxItemCount: 2, maxDegreeOfParallelism: 3 }; + +// // prepare expected results +// const getOrderByKey = function (r) { +// return r["spam"]; +// } +// const expectedOrderedIds = (_.sortBy(documentDefinitions, getOrderByKey).map(function (r) { +// return r["id"]; +// })); + +// // validates the results size and order +// executeQueryAndValidateResults(getCollectionLink(isNameBased, db, collection), query, options, expectedOrderedIds, done); +// }); + +// it("Validate OrderBy with top", function (done) { +// // an order by query with top, total existing docs more than requested top count +// const topCount = 9; +// const querySpec = { +// query: util.format("SELECT top %d * FROM root r order by r.spam", topCount), +// } +// const options = { enableCrossPartitionQuery: true, maxItemCount: 2 }; + +// // prepare expected results +// const getOrderByKey = function (r) { +// return r["spam"]; +// } +// const expectedOrderedIds = (_.sortBy(documentDefinitions, getOrderByKey).map(function (r) { +// return r["id"]; +// })).slice(0, topCount); + +// executeQueryAndValidateResults(getCollectionLink(isNameBased, db, collection), querySpec, options, expectedOrderedIds, done); + +// }); +// }); +// }); diff --git a/sdk/cosmosdb/cosmos/src/test/integration/sslVerification.spec.ts b/sdk/cosmosdb/cosmos/src/test/integration/sslVerification.spec.ts new file mode 100644 index 000000000000..916dda0ca30e --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/integration/sslVerification.spec.ts @@ -0,0 +1,37 @@ +import assert from "assert"; +import { CosmosClient, DocumentBase } from "../.."; +import { getTestDatabase } from "../common/TestHelpers"; + +const endpoint = "https://localhost:443"; +const masterKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + +// TODO: Skipping these tests for now until we find a way to run these tests in a seperate nodejs process +// Currently all tests are run in same process so we cannot update the environment variables for different tests +// This test runs fine when run independently but fails when run along with rest of the tests. +describe.skip("Validate SSL verification check for emulator", function() { + it("nativeApi Client Should throw exception", async function() { + try { + const client = new CosmosClient({ endpoint, auth: { masterKey } }); + // create database + await getTestDatabase("ssl verification", client); + } catch (err) { + // connecting to emulator should throw SSL verification error, + // unless you explicitly disable it via connectionPolicy.DisableSSLVerification + assert.equal(err.code, "DEPTH_ZERO_SELF_SIGNED_CERT", "client should throw exception"); + } + }); + + it("nativeApi Client Should successfully execute request", async function() { + const connectionPolicy = new DocumentBase.ConnectionPolicy(); + // Disable SSL verification explicitly + connectionPolicy.DisableSSLVerification = true; + const client = new CosmosClient({ + endpoint, + auth: { masterKey }, + connectionPolicy + }); + + // create database + await getTestDatabase("ssl verification", client); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/readme.md b/sdk/cosmosdb/cosmos/src/test/readme.md new file mode 100644 index 000000000000..a18fd8017243 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/readme.md @@ -0,0 +1,30 @@ +Follow these instructions to run the tests locally. + +## Prerequisites + +1. Clone Azure/azure-documentdb-node repository +Please clone the source and tests from [https://github.com/Azure/azure-documentdb-node](https://github.com/Azure/azure-documentdb-node) + +2. Install Node.js and npm +[https://docs.npmjs.com/getting-started/installing-node](https://docs.npmjs.com/getting-started/installing-node) + +3. Install mocha package globally +> npm install -g mocha + +## Running the tests +Using your command-line tool, from the root of your local copy of azure-documentdb-node repository: +If you are contributing changes and submitting PR then you need to ensure that you run the tests against your local copy of the source, and not the published npm package. + +If you just want to run the tests against the published npm package then skip steps #1 & #2 proceed directly to step #3 + +1. Remove documentdb, if previously installed +> npm remove documentdb + +2. Install documentdb +> npm install source + +3. Change to `test` directory +> cd test + +3. Run the tests +> mocha -t 0 -R spec diff --git a/sdk/cosmosdb/cosmos/src/test/tslint.json b/sdk/cosmosdb/cosmos/src/test/tslint.json new file mode 100644 index 000000000000..d96028aeeecf --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/tslint.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tslint.json", + "rules": { + "only-arrow-functions": false, + "space-before-function-paren": false, + "no-console": false, + "no-implicit-dependencies": [true, "dev"], + "import-blacklist": false + } +} diff --git a/sdk/cosmosdb/cosmos/src/test/unit/helper.spec.ts b/sdk/cosmosdb/cosmos/src/test/unit/helper.spec.ts new file mode 100644 index 000000000000..d30e881926c8 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/unit/helper.spec.ts @@ -0,0 +1,79 @@ +import assert from "assert"; +import { IHeaders } from "../.."; +import { Constants, Helper } from "../../common"; + +describe("Helper methods", function() { + describe("isResourceValid Unit Tests", function() { + it("id is not string", function(done) { + const err = {}; + const result = Helper.isResourceValid({ id: 1 }, err); + + assert.equal(result, false); + assert.deepEqual(err, { message: "Id must be a string." }); + done(); + }); + }); + + describe("setIsUpsertHeader", function() { + it("Should add is-upsert header.", function(done) { + const headers: any = {}; + assert.equal(undefined, headers[Constants.HttpHeaders.IsUpsert]); + Helper.setIsUpsertHeader(headers); + assert.equal(true, headers[Constants.HttpHeaders.IsUpsert]); + done(); + }); + + it("Should update is-upsert header.", function(done) { + const headers: IHeaders = {}; + headers[Constants.HttpHeaders.IsUpsert] = false; + assert.equal(false, headers[Constants.HttpHeaders.IsUpsert]); + Helper.setIsUpsertHeader(headers); + assert.equal(true, headers[Constants.HttpHeaders.IsUpsert]); + done(); + }); + + it("Should throw on undefined headers", function(done) { + assert.throws(function() { + Helper.setIsUpsertHeader(undefined); + }, /The "headers" parameter must not be null or undefined/); + done(); + }); + + it("Should throw on null headers", function(done) { + assert.throws(function() { + Helper.setIsUpsertHeader(null); + }, /The "headers" parameter must not be null or undefined/); + done(); + }); + + it("Should throw on invalid string headers", function(done) { + assert.throws( + function() { + Helper.setIsUpsertHeader("" as any); + }, // Any type is intentional for test failure + /The "headers" parameter must be an instance of "Object". Actual type is: "string"./ + ); + done(); + }); + + it("Should throw on invalid number headers", function(done) { + assert.throws( + function() { + Helper.setIsUpsertHeader(0 as any); + }, // Any type is intentional for test failure + /The "headers" parameter must be an instance of "Object". Actual type is: "number"./ + ); + done(); + }); + + it("Should throw on invalid boolean headers", function(done) { + assert.throws( + function() { + Helper.setIsUpsertHeader(false as any); + }, // Any type is intentional for test failure + /The "headers" parameter must be an instance of "Object". Actual type is: "boolean"./ + ); + done(); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/unit/inMemoryCollectionRoutingMap.spec.ts b/sdk/cosmosdb/cosmos/src/test/unit/inMemoryCollectionRoutingMap.spec.ts new file mode 100644 index 000000000000..e2ab4f280503 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/unit/inMemoryCollectionRoutingMap.spec.ts @@ -0,0 +1,260 @@ +import assert from "assert"; +import { CollectionRoutingMapFactory, InMemoryCollectionRoutingMap, QueryRange } from "../../routing"; + +describe("InMemoryCollectionRoutingMap Tests", function() { + describe("getOverlappingRanges", function() { + const partitionKeyRanges = [ + { id: "0", minInclusive: "", maxExclusive: "05C1C9CD673398" }, + { + id: "1", + minInclusive: "05C1C9CD673398", + maxExclusive: "05C1D9CD673398" + }, + { + id: "2", + minInclusive: "05C1D9CD673398", + maxExclusive: "05C1E399CD6732" + }, + { + id: "3", + minInclusive: "05C1E399CD6732", + maxExclusive: "05C1E9CD673398" + }, + { id: "4", minInclusive: "05C1E9CD673398", maxExclusive: "FF" } + ]; + const partitionRangeWithInfo = partitionKeyRanges.map(r => [r, true]); + const collectionRoutingMap = CollectionRoutingMapFactory.createCompleteRoutingMap( + partitionRangeWithInfo, + "sample collection id" + ); + + it("queryCompleteRange", function() { + const completeRange = new QueryRange("", "FF", true, false); + const overlappingPartitionKeyRanges = collectionRoutingMap.getOverlappingRanges(completeRange); + + assert.equal(overlappingPartitionKeyRanges.length, partitionKeyRanges.length); + assert.deepEqual(overlappingPartitionKeyRanges, partitionKeyRanges); + }); + + it("queryEmptyRange", function() { + const emtpyRange = new QueryRange("05C1C9CD673396", "05C1C9CD673396", true, false); + const overlappingPartitionKeyRanges = collectionRoutingMap.getOverlappingRanges(emtpyRange); + + assert.equal(overlappingPartitionKeyRanges.length, 0); + }); + + it("queryPoint", function() { + const pointRange = new QueryRange("05C1D9CD673397", "05C1D9CD673397", true, true); + const overlappingPartitionKeyRanges = collectionRoutingMap.getOverlappingRanges(pointRange); + + assert.equal(overlappingPartitionKeyRanges.length, 1); + assert(overlappingPartitionKeyRanges[0].minInclusive <= pointRange.min); + assert(overlappingPartitionKeyRanges[0].maxExclusive > pointRange.max); + }); + + it("boundaryPointQuery", function() { + const pointRange = new QueryRange("05C1C9CD673398", "05C1C9CD673398", true, true); + const overlappingPartitionKeyRanges = collectionRoutingMap.getOverlappingRanges(pointRange); + + assert.equal(overlappingPartitionKeyRanges.length, 1); + assert(overlappingPartitionKeyRanges[0].minInclusive <= pointRange.min); + assert(overlappingPartitionKeyRanges[0].maxExclusive > pointRange.max); + assert(overlappingPartitionKeyRanges[0].minInclusive === pointRange.min); + }); + }); + + describe("All methods", function() { + const partitionRangeWithInfo = [ + [ + { + id: "2", + minInclusive: "0000000050", + maxExclusive: "0000000070" + }, + 2 + ], + [ + { + id: "0", + minInclusive: "", + maxExclusive: "0000000030" + }, + 0 + ], + [ + { + id: "1", + minInclusive: "0000000030", + maxExclusive: "0000000050" + }, + 1 + ], + [ + { + id: "3", + minInclusive: "0000000070", + maxExclusive: "FF" + }, + 3 + ] + ]; + + const collectionRoutingMap = CollectionRoutingMapFactory.createCompleteRoutingMap( + partitionRangeWithInfo, + "sample collection id" + ); + + it("validate _orderedPartitionKeyRanges", function() { + assert.equal("0", collectionRoutingMap.getOrderedParitionKeyRanges()[0].id); + assert.equal("1", collectionRoutingMap.getOrderedParitionKeyRanges()[1].id); + assert.equal("2", collectionRoutingMap.getOrderedParitionKeyRanges()[2].id); + assert.equal("3", collectionRoutingMap.getOrderedParitionKeyRanges()[3].id); + }); + + // TODO: bad practice to test implementation details + it("validate _orderedPartitionInfo", function() { + assert.equal(0, collectionRoutingMap.orderedPartitionInfo[0]); + assert.equal(1, collectionRoutingMap.orderedPartitionInfo[1]); + assert.equal(2, collectionRoutingMap.orderedPartitionInfo[2]); + assert.equal(3, collectionRoutingMap.orderedPartitionInfo[3]); + }); + + it("validate getRangeByEffectivePartitionKey", function() { + assert.equal("0", collectionRoutingMap.getRangeByEffectivePartitionKey("").id); + assert.equal("0", collectionRoutingMap.getRangeByEffectivePartitionKey("0000000000").id); + assert.equal("1", collectionRoutingMap.getRangeByEffectivePartitionKey("0000000030").id); + assert.equal("1", collectionRoutingMap.getRangeByEffectivePartitionKey("0000000031").id); + assert.equal("3", collectionRoutingMap.getRangeByEffectivePartitionKey("0000000071").id); + }); + + // // TODO: bad practice to test implementation details + // it("validate getRangeByPartitionKeyRangeId", function () { + // assert.equal("0", collectionRoutingMap.getRangeByPartitionKeyRangeId(0).id); + // assert.equal("1", collectionRoutingMap.getRangeByPartitionKeyRangeId(1).id); + // }); + + it("validate getOverlappingRanges", function() { + const completeRange = new QueryRange("", "FF", true, false); + + const compareId = function(a: any, b: any) { + // TODO: any + return a["id"] - b["id"]; + }; + + const overlappingRanges = collectionRoutingMap.getOverlappingRanges([completeRange]).sort(compareId); + assert.equal(4, overlappingRanges.length); + + let onlyParitionRanges = partitionRangeWithInfo.map(function(item) { + return item[0]; + }); + + onlyParitionRanges = onlyParitionRanges.sort(compareId); + assert.deepEqual(overlappingRanges, onlyParitionRanges); + + const noPoint = new QueryRange("", "", false, false); + assert.equal(0, collectionRoutingMap.getOverlappingRanges([noPoint]).length); + + const onePoint = new QueryRange("0000000040", "0000000040", true, true); + let overlappingPartitionKeyRanges = collectionRoutingMap.getOverlappingRanges([onePoint]); + assert.equal(1, overlappingPartitionKeyRanges.length); + assert.equal("1", overlappingPartitionKeyRanges[0].id); + + const ranges = [ + new QueryRange("0000000040", "0000000045", true, true), + new QueryRange("0000000045", "0000000046", true, true), + new QueryRange("0000000046", "0000000050", true, true) + ]; + overlappingPartitionKeyRanges = collectionRoutingMap.getOverlappingRanges(ranges).sort(compareId); + + assert.equal(2, overlappingPartitionKeyRanges.length); + assert.equal("1", overlappingPartitionKeyRanges[0].id); + assert.equal("2", overlappingPartitionKeyRanges[1].id); + }); + }); + + describe("Error Handling", function() { + describe("Incorrect instantiation", function() { + it("Invalid Routing Map", function() { + const partitionRangeWithInfo = [ + [ + { + id: "1", + minInclusive: "0000000020", + maxExclusive: "0000000030" + }, + 2 + ], + [ + { + id: "2", + minInclusive: "0000000025", + maxExclusive: "0000000035" + }, + 2 + ] + ]; + const collectionUniqueId = ""; + try { + const collectionRoutingMap = CollectionRoutingMapFactory.createCompleteRoutingMap( + partitionRangeWithInfo, + "sample collection id" + ); + assert.fail("must throw exception"); + } catch (e) { + assert.equal(e.message, "Ranges overlap"); + } + }); + + // TODO: test does two things (code smell) + it("Incomplete Routing Map", function() { + let partitionRangeWithInfo = [ + [ + { + id: "2", + minInclusive: "", + maxExclusive: "0000000030" + }, + 2 + ], + [ + { + id: "3", + minInclusive: "0000000031", + maxExclusive: "FF" + }, + 2 + ] + ]; + let collectionRoutingMap = CollectionRoutingMapFactory.createCompleteRoutingMap( + partitionRangeWithInfo, + "sample collection id" + ); + assert.equal(collectionRoutingMap, null); + + partitionRangeWithInfo = [ + [ + { + id: "2", + minInclusive: "", + maxExclusive: "0000000030" + }, + 2 + ], + [ + { + id: "2", + minInclusive: "0000000030", + maxExclusive: "FF" + }, + 2 + ] + ]; + collectionRoutingMap = CollectionRoutingMapFactory.createCompleteRoutingMap( + partitionRangeWithInfo, + "sample collection id" + ); + assert.notEqual(collectionRoutingMap, null); + }); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/unit/locationCache.spec.ts b/sdk/cosmosdb/cosmos/src/test/unit/locationCache.spec.ts new file mode 100644 index 000000000000..e5e883143e9d --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/unit/locationCache.spec.ts @@ -0,0 +1,390 @@ +import { CosmosClientOptions } from "../../CosmosClientOptions"; +import { ConnectionPolicy, DatabaseAccount, Location } from "../../documents"; +import { LocationCache } from "../../LocationCache"; + +import * as assert from "assert"; +import { Constants, ResourceType } from "../../common"; + +const scenarios: Scenario[] = []; +const regions = ["westus", "East US", "eastus2", "south Centralus", "sEasIa"]; + +interface Scenario { + defaultEndpoint?: string; + connectionPolicy?: ConnectionPolicy; + databaseAccount?: DatabaseAccount; +} + +function getEndpointFromRegion(regionName?: string) { + const prefix = "https://test"; + const suffix = ".documents.azure.com:443"; + return `${prefix}${regionName ? `-${regionName}` : ""}${suffix}`; +} + +function addScenario(options?: { numberOfRegions?: number; useMultipleWriteLocations?: boolean }) { + const connectionPolicy = new ConnectionPolicy(); + const databaseAccountConfig: { + writableLocations?: Location[]; + readableLocations?: Location[]; + enableMultipleWriteLocations?: boolean; + } = {}; + const defaultEndpoint = getEndpointFromRegion(); + + if (options) { + if (options.numberOfRegions) { + connectionPolicy.PreferredLocations = regions.slice(0, options.numberOfRegions); + databaseAccountConfig.readableLocations = connectionPolicy.PreferredLocations.map(locationName => { + return { name: locationName, databaseAccountEndpoint: getEndpointFromRegion(locationName) }; + }); + if (options.useMultipleWriteLocations) { + connectionPolicy.UseMultipleWriteLocations = options.useMultipleWriteLocations; + databaseAccountConfig.writableLocations = connectionPolicy.PreferredLocations.map(locationName => { + return { name: locationName, databaseAccountEndpoint: getEndpointFromRegion(locationName) }; + }).sort((a, b) => (a.name > b.name ? 1 : -1)); + databaseAccountConfig.enableMultipleWriteLocations = options.useMultipleWriteLocations; + } else { + databaseAccountConfig.writableLocations = regions + .slice(0, 1) + .map(locationName => { + return { name: locationName, databaseAccountEndpoint: getEndpointFromRegion(locationName) }; + }) + .sort((a, b) => (a.name > b.name ? 1 : -1)); + } + } + } + + scenarios.push({ + connectionPolicy, + defaultEndpoint, + databaseAccount: new DatabaseAccount(databaseAccountConfig, {}) + }); +} + +addScenario(); // Default +addScenario({ numberOfRegions: 1 }); +addScenario({ numberOfRegions: 2 }); +addScenario({ numberOfRegions: 3 }); +addScenario({ numberOfRegions: 5 }); +addScenario({ numberOfRegions: 1, useMultipleWriteLocations: true }); +addScenario({ numberOfRegions: 2, useMultipleWriteLocations: true }); +addScenario({ numberOfRegions: 3, useMultipleWriteLocations: true }); +addScenario({ numberOfRegions: 5, useMultipleWriteLocations: true }); + +describe("Location Cache", function() { + this.timeout(process.env.MOCHA_TIMEOUT || 2000); + for (const scenario of scenarios) { + describe(`when there is a DatabaseAccount refresh and ${ + scenario.connectionPolicy.PreferredLocations.length + } preferred region and multi-region write is ${scenario.connectionPolicy.UseMultipleWriteLocations}.`, function() { + const connectionPolicy: ConnectionPolicy = scenario.connectionPolicy; + const endpoint = scenario.defaultEndpoint; + const cosmosClientOptions: CosmosClientOptions = { auth: {}, endpoint, connectionPolicy }; + const locationCache = new LocationCache(cosmosClientOptions); + + before(function() { + locationCache.onDatabaseAccountRead(scenario.databaseAccount); + }); + + it("shouldn't refresh", function() { + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal(shouldRefresh, false, "shouldn't need to refresh"); + }); + + it("preferred locations should match the connection policy preferred locations", function() { + const preferredLocations = locationCache.prefferredLocations; + assert.equal( + preferredLocations.length, + scenario.connectionPolicy.PreferredLocations.length, + "preffered locations size should match" + ); + }); + + it("read endpoint should match most preferred endpoint", function() { + const readEndpoint = locationCache.getReadEndpoint(); + assert.equal( + readEndpoint, + scenario.connectionPolicy.PreferredLocations.length > 0 ? getEndpointFromRegion(regions[0]) : endpoint, + "read endpoint should match most preferred endpoint after database account info refresh" + ); + }); + + it("write endpoint should match default endpoint", function() { + const writeEndpoint = locationCache.getWriteEndpoint(); + const expectedEndpoint = + scenario.connectionPolicy.PreferredLocations.length > 0 + ? getEndpointFromRegion(scenario.connectionPolicy.PreferredLocations[0]) + : endpoint; + assert.equal( + writeEndpoint, + expectedEndpoint, + "write endpoint should match most preferred endpoint after database account info refresh" + ); + }); + + it(`read request for resolve endpoint, retry count 0, should match read endpoint`, function() { + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Read, + resourceType: ResourceType.item, + retryCount: 0 + }); + + const readEndpoint = locationCache.getReadEndpoint(); + assert.equal(resolveEndpoint, readEndpoint, "resolve endpoint should match read endpoint"); + }); + + it(`write request for resolve endpoint, retry count 0, should match write endpoint`, function() { + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Replace, + resourceType: ResourceType.item, + retryCount: 0 + }); + + const writeEndpoint = locationCache.getWriteEndpoint(); + assert.equal(resolveEndpoint, writeEndpoint, "resolve endpoint should match write endpoint"); + }); + + // After this, there are side effects. All the "markUnavailable" ones will remove locations from the list. + // It's probably best to not add new "it"s below here to avoid unreliable tests. + if (scenario.connectionPolicy.PreferredLocations.length > 0) { + if (!scenario.connectionPolicy.UseMultipleWriteLocations) { + it("write endpoint should match default endpoint even after being marked unavailable", function() { + locationCache.markCurrentLocationUnavailableForWrite(locationCache.getWriteEndpoint()); + const writeEndpoint = locationCache.getWriteEndpoint(); + assert.equal( + writeEndpoint, + scenario.databaseAccount.writableLocations[0].databaseAccountEndpoint, + "write endpoint should match default endpoint prior to any database account info" + ); + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Replace, + resourceType: ResourceType.item, + retryCount: 1 + }); + + assert.equal(resolveEndpoint, writeEndpoint, "resolve endpoint should match write endpoint"); + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal(shouldRefresh, true, "should need to refresh"); + }); + } + } else { + if (!scenario.connectionPolicy.UseMultipleWriteLocations) { + it("write endpoint should match default endpoint even after being marked unavailable", function() { + locationCache.markCurrentLocationUnavailableForWrite(locationCache.getWriteEndpoint()); + const writeEndpoint = locationCache.getWriteEndpoint(); + assert.equal( + writeEndpoint, + endpoint, + "write endpoint should match default endpoint prior to any database account info" + ); + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Replace, + resourceType: ResourceType.item, + retryCount: 1 + }); + + assert.equal(resolveEndpoint, writeEndpoint, "resolve endpoint should match write endpoint"); + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal( + shouldRefresh, + scenario.connectionPolicy.PreferredLocations.length > 0, + "should need to refresh" + ); + }); + } + } + + if (scenario.connectionPolicy.PreferredLocations.length > 1) { + it("read endpoint should return next endpoint after being marked unavailable", function() { + locationCache.markCurrentLocationUnavailableForRead(locationCache.getReadEndpoint()); + const readEndpoint = locationCache.getReadEndpoint(); + assert.equal( + readEndpoint, + getEndpointFromRegion(regions[1]), + "read endpoint should match default endpoint prior to any database account info even if unavailable" + ); + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Read, + resourceType: ResourceType.item, + retryCount: 1 + }); + assert.equal(resolveEndpoint, readEndpoint, "resolve endpoint should match read endpoint"); + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal(shouldRefresh, true, "should need to refresh"); + }); + + if (scenario.connectionPolicy.UseMultipleWriteLocations) { + it("write endpoint should return next endpoint after being marked unavailable", function() { + locationCache.markCurrentLocationUnavailableForWrite(locationCache.getWriteEndpoint()); + const writeEndpoint = locationCache.getWriteEndpoint(); + assert.equal( + writeEndpoint, + getEndpointFromRegion(regions[1]), + "write endpoint should match default endpoint prior to any database account info" + ); + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Replace, + resourceType: ResourceType.item, + retryCount: 1 + }); + + assert.equal(resolveEndpoint, writeEndpoint, "resolve endpoint should match write endpoint"); + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal( + shouldRefresh, + scenario.connectionPolicy.PreferredLocations.length > 0, + "should need to refresh" + ); + }); + } + } else { + it("read endpoint should match default endpoint even after being marked unavailable", function() { + locationCache.markCurrentLocationUnavailableForRead(locationCache.getReadEndpoint()); + const readEndpoint = locationCache.getReadEndpoint(); + assert.equal( + readEndpoint, + endpoint, + "read endpoint should match default endpoint prior to any database account info even if unavailable" + ); + + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Read, + resourceType: ResourceType.item, + retryCount: 1 + }); + assert.equal(resolveEndpoint, readEndpoint, "resolve endpoint should match read endpoint"); + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal( + shouldRefresh, + scenario.connectionPolicy.PreferredLocations.length > 0, + "shouldn't need to refresh" + ); + }); + + if (scenario.connectionPolicy.UseMultipleWriteLocations) { + it("write endpoint should match default endpoint even after being marked unavailable", function() { + locationCache.markCurrentLocationUnavailableForWrite(locationCache.getWriteEndpoint()); + const writeEndpoint = locationCache.getWriteEndpoint(); + assert.equal( + writeEndpoint, + endpoint, + "write endpoint should match default endpoint prior to any database account info" + ); + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Replace, + resourceType: ResourceType.item, + retryCount: 1 + }); + + assert.equal(resolveEndpoint, writeEndpoint, "resolve endpoint should match write endpoint"); + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal(shouldRefresh, true, "should need to refresh"); + }); + } + } + }); + + describe(`when there is not a DatabaseAccount refresh and ${ + scenario.connectionPolicy.PreferredLocations.length + } preferred regions and multi-region write is ${scenario.connectionPolicy.UseMultipleWriteLocations}.`, function() { + const connectionPolicy: ConnectionPolicy = scenario.connectionPolicy; + const endpoint = scenario.defaultEndpoint; + const cosmosClientOptions: CosmosClientOptions = { auth: {}, endpoint, connectionPolicy }; + const locationCache = new LocationCache(cosmosClientOptions); + + if (!scenario.connectionPolicy.UseMultipleWriteLocations) { + it("shouldn't refresh", function() { + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal(shouldRefresh, false, "shouldn't need to refresh"); + }); + } else { + it("should refresh", function() { + const { shouldRefresh, canRefreshInBackground } = locationCache.shouldRefreshEndpoints(); + assert.equal(shouldRefresh, true, "should need to refresh"); + }); + } + + it("preferred locations should match the connection policy preferred locations", function() { + const preferredLocations = locationCache.prefferredLocations; + assert.equal( + preferredLocations.length, + scenario.connectionPolicy.PreferredLocations.length, + "preffered locations size should match" + ); + }); + + it("read endpoint should match default endpoint", function() { + const readEndpoint = locationCache.getReadEndpoint(); + assert.equal( + readEndpoint, + endpoint, + "read endpoint should match default endpoint prior to any database account info" + ); + }); + + it("write endpoint should match default endpoint", function() { + const writeEndpoint = locationCache.getWriteEndpoint(); + assert.equal( + writeEndpoint, + endpoint, + "write endpoint should match default endpoint prior to any database account info" + ); + }); + + it(`read request for resolve endpoint, retry count 0, should match read endpoint`, function() { + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Read, + resourceType: ResourceType.item, + retryCount: 0 + }); + + const readEndpoint = locationCache.getReadEndpoint(); + assert.equal(resolveEndpoint, readEndpoint, "resolve endpoint should match read endpoint"); + }); + + it(`write request for resolve endpoint, retry count 0, should match write endpoint`, function() { + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Replace, + resourceType: ResourceType.item, + retryCount: 0 + }); + + const writeEndpoint = locationCache.getWriteEndpoint(); + assert.equal(resolveEndpoint, writeEndpoint, "resolve endpoint should match write endpoint"); + }); + + // After this, there are side effects. All the "markUnavailable" ones will remove locations from the list. + // It's probably best to not add new "it"s below here to avoid unreliable tests. + it("read endpoint should match default endpoint even after being marked unavailable", function() { + locationCache.markCurrentLocationUnavailableForRead(locationCache.getReadEndpoint()); + const readEndpoint = locationCache.getReadEndpoint(); + assert.equal( + readEndpoint, + endpoint, + "read endpoint should match default endpoint prior to any database account info even if unavailable" + ); + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Read, + resourceType: ResourceType.item, + retryCount: 1 + }); + assert.equal(resolveEndpoint, readEndpoint, "resolve endpoint should match read endpoint"); + }); + + it("write endpoint should match default endpoint even after being marked unavailable", function() { + locationCache.markCurrentLocationUnavailableForWrite(locationCache.getWriteEndpoint()); + const writeEndpoint = locationCache.getWriteEndpoint(); + assert.equal( + writeEndpoint, + endpoint, + "write endpoint should match default endpoint prior to any database account info" + ); + const resolveEndpoint = locationCache.resolveServiceEndpoint({ + operationType: Constants.OperationTypes.Replace, + resourceType: ResourceType.item, + retryCount: 1 + }); + + assert.equal(resolveEndpoint, writeEndpoint, "resolve endpoint should match write endpoint"); + }); + }); + } +}); diff --git a/sdk/cosmosdb/cosmos/src/test/unit/plaftorm.spec.ts b/sdk/cosmosdb/cosmos/src/test/unit/plaftorm.spec.ts new file mode 100644 index 000000000000..a0d959f42bde --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/unit/plaftorm.spec.ts @@ -0,0 +1,53 @@ +import assert from "assert"; +import * as os from "os"; +import * as util from "util"; +import { Constants } from "../.."; +import { Platform } from "../../common"; + +// var assert = require("assert") +// , Contants = require("../lib/constants") +// , os = require("os") +// , Platform = require("../lib/platform") +// , util = require("util"); + +describe("Platform.getUserAgent", function() { + it("getUserAgent()", function() { + const userAgent = Platform.getUserAgent(); + const expectedUserAgent = util.format( + "%s/%s Nodejs/%s azure-cosmos-js/%s", + os.platform(), + os.release(), + process.version, + Constants.SDKVersion + ); + assert.strictEqual(userAgent, expectedUserAgent, "invalid UserAgent format"); + }); + + describe("Platform._getSafeUserAgentSegmentInfo()", function() { + it("Removing spaces", function() { + const safeString = Platform._getSafeUserAgentSegmentInfo("a b c"); + assert.strictEqual(safeString, "abc"); + }); + it("empty string handling", function() { + const safeString = Platform._getSafeUserAgentSegmentInfo(""); + assert.strictEqual(safeString, "unknown"); + }); + it("undefined", function() { + const safeString = Platform._getSafeUserAgentSegmentInfo(undefined); + assert.strictEqual(safeString, "unknown"); + }); + it("null", function() { + const safeString = Platform._getSafeUserAgentSegmentInfo(null); + assert.strictEqual(safeString, "unknown"); + }); + }); +}); + +describe("Version", function() { + it("should have matching constant version & package version", function() { + const packageJson = require("../../../package.json"); + const packageVersion = packageJson["version"]; + const constantVersion = Constants.SDKVersion; + assert.equal(constantVersion, packageVersion, "Package.json and Constants.SDKVersion don't match"); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/unit/range.spec.ts b/sdk/cosmosdb/cosmos/src/test/unit/range.spec.ts new file mode 100644 index 000000000000..29938ed01b2c --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/unit/range.spec.ts @@ -0,0 +1,420 @@ +import assert from "assert"; +import { Range } from "../../range"; + +describe("Range Tests", function() { + describe("constructor", function() { + const invalidOptionsTest = function(options: any, expectedError: any) { + assert.throws(function() { + const r = new Range(options); + }, expectedError); + }; + + const optionsIsNullTest = function(options: any) { + invalidOptionsTest(options, /Invalid argument: 'options' is null/); + }; + + const optionsIsNotAnObjectTest = function(options: any) { + invalidOptionsTest(options, /Invalid argument: 'options' is not an object/); + }; + + const invalidRangeTest = function(options: any) { + invalidOptionsTest(options, /Invalid argument: 'options.low' must be less than or equal than 'options.high'/); + }; + + it("options - undefined (ommited argument)", function() { + assert(new Range()); + }); + + it("options - undefined (literal argument)", function() { + assert(new Range(undefined)); + }); + + it("options - null ", function() { + const options: any = null; + optionsIsNullTest(options); + }); + + it("options - number", function() { + const options = 0; + optionsIsNotAnObjectTest(options); + }); + + it("invalid options - string", function() { + const options = ""; + optionsIsNotAnObjectTest(options); + }); + + it("invalid options - boolean", function() { + const options = false; + optionsIsNotAnObjectTest(options); + }); + + it("Range instances are frozen", function() { + const r = new Range(); + + try { + (r as any).compareFunction = 1; + assert.fail("Must throw"); + } catch (err) { + assert(err.message.includes("add property compareFunction, object is not extensible")); + } + }); + }); + + describe("_contains", function() { + it("undefined,undefined contains undefined is true", function() { + const r = new Range(); + assert(r._contains(undefined)); + }); + + it("undefined,undefined contains null is false", function() { + const r = new Range(); + assert(!r._contains(null)); + }); + + it("null,null contains undefined is true", function() { + const r = new Range({ low: null }); + assert(r._contains(null)); + }); + + it("null,null contains null is true", function() { + const r = new Range({ low: null }); + assert(r._contains(null)); + }); + + it("range contains self is true - default range", function() { + const r = new Range(); + assert(r._contains(r)); + }); + + it("range contains self is true - non-default range", function() { + const r = new Range({ low: "A" }); + assert(r._contains(r)); + }); + + it("A,D contains B,C is true", function() { + const r1 = new Range({ low: "A", high: "D" }); + const r2 = new Range({ low: "B", high: "C" }); + assert(r1._contains(r2)); + }); + + it("B,C contains A,D is false", function() { + const r1 = new Range({ low: "B", high: "C" }); + const r2 = new Range({ low: "A", high: "D" }); + assert(!r1._contains(r2)); + }); + + it("A,C contains B,D is false", function() { + const r1 = new Range({ low: "A", high: "C" }); + const r2 = new Range({ low: "B", high: "D" }); + assert(!r1._contains(r2)); + }); + + it("B,D contains A,C is false", function() { + const r1 = new Range({ low: "B", high: "D" }); + const r2 = new Range({ low: "A", high: "C" }); + assert(!r1._contains(r2)); + }); + + it("A,B contains B,C is false", function() { + const r1 = new Range({ low: "A", high: "B" }); + const r2 = new Range({ low: "B", high: "C" }); + assert(!r1._contains(r2)); + }); + + it("B,C contains A,B is false", function() { + const r1 = new Range({ low: "B", high: "C" }); + const r2 = new Range({ low: "A", high: "B" }); + assert(!r1._contains(r2)); + }); + + it("A,B contains C,D is false", function() { + const r1 = new Range({ low: "A", high: "B" }); + const r2 = new Range({ low: "C", high: "D" }); + assert(!r1._contains(r2)); + }); + + it("C,D contains A,B is false", function() { + const r1 = new Range({ low: "C", high: "D" }); + const r2 = new Range({ low: "A", high: "B" }); + assert(!r1._contains(r2)); + }); + + it("A,C contains B is true", function() { + const r1 = new Range({ low: "A", high: "C" }); + assert(r1._contains("B")); + }); + + it("B,C contains A is false", function() { + const r1 = new Range({ low: "B", high: "C" }); + assert(!r1._contains("A")); + }); + + it("A,B contains C is false", function() { + const r1 = new Range({ low: "A", high: "B" }); + assert(!r1._contains("C")); + }); + }); + + describe("_containsPoint", function() { + const range = new Range({ low: 1, high: 3 }); + + it("numbers, default comparison", function() { + assert(range._containsPoint(20)); + }); + + it("numbers, custom comparison", function() { + assert( + !range._containsPoint(20, function(a, b) { + return a > b ? 1 : -1; + }) + ); + }); + }); + + describe("_containsRange", function() { + const range = new Range({ low: 1, high: 3 }); + + it("numbers, default comparison", function() { + assert(range._containsRange(new Range({ low: 20, high: 29 }))); + }); + + it("numbers, custom comparison", function() { + assert( + !range._containsRange(new Range({ low: 20, high: 29 }), function(a, b) { + return a > b ? 1 : -1; + }) + ); + }); + }); + + describe("_intersect", function() { + const otherIsUndefinedOrNullTest = function(other: any) { + const r = new Range(); + assert.throws(function() { + r._intersect(other); + }, /Invalid Argument: 'other' is undefined or null/); + }; + + it("error - other is undefined", function() { + otherIsUndefinedOrNullTest(undefined); + }); + + it("error - other is null", function() { + otherIsUndefinedOrNullTest(null); + }); + + it("range intersect self is true - default range", function() { + const r = new Range(); + assert(r._intersect(r)); + }); + + it("R intersect R is true - non default range", function() { + const r = new Range({ low: 1, high: "2" }); + assert(r._intersect(r)); + }); + + it("A,D insersects B,C is true", function() { + const r1 = new Range({ low: "A", high: "D" }); + const r2 = new Range({ low: "B", high: "C" }); + assert(r1._intersect(r2)); + }); + + it("B,C insersects A,D is true", function() { + const r1 = new Range({ low: "B", high: "C" }); + const r2 = new Range({ low: "A", high: "D" }); + assert(r1._intersect(r2)); + }); + + it("A,C insersects B,D is true", function() { + const r1 = new Range({ low: "A", high: "C" }); + const r2 = new Range({ low: "B", high: "D" }); + assert(r1._intersect(r2)); + assert(r2._intersect(r1)); + }); + + it("B,D insersects A,C is true", function() { + const r1 = new Range({ low: "B", high: "D" }); + const r2 = new Range({ low: "A", high: "C" }); + assert(r1._intersect(r2)); + }); + + it("A,B insersects B,C is true", function() { + const r1 = new Range({ low: "A", high: "B" }); + const r2 = new Range({ low: "B", high: "C" }); + assert(r1._intersect(r2)); + assert(r2._intersect(r1)); + }); + + it("B,C insersects A,B is true", function() { + const r1 = new Range({ low: "B", high: "C" }); + const r2 = new Range({ low: "A", high: "B" }); + assert(r1._intersect(r2)); + }); + + it("A,B insersects C,D is false", function() { + const r1 = new Range({ low: "A", high: "B" }); + const r2 = new Range({ low: "C", high: "D" }); + assert(!r1._intersect(r2)); + }); + + it("C,D insersects A,B is false", function() { + const r1 = new Range({ low: "C", high: "D" }); + const r2 = new Range({ low: "A", high: "B" }); + assert(!r1._intersect(r2)); + }); + }); + + describe("_toString", function() { + const toStringTest = function(options: any, expectedString: any) { + const r = new Range(options); + assert.strictEqual(r._toString(), expectedString); + }; + + it("undefined values", function() { + toStringTest(undefined, "undefined,undefined"); + }); + it("null values", function() { + toStringTest({ low: null }, "null,null"); + }); + it("NaN values", function() { + toStringTest({ low: NaN }, "NaN,NaN"); + }); + it("number values", function() { + toStringTest({ low: 1 }, "1,1"); + }); + it("string values", function() { + toStringTest({ low: "a" }, "a,a"); + }); + it("boolean values", function() { + toStringTest({ low: false, high: true }, "false,true"); + }); + it("object values", function() { + toStringTest({ low: {} }, "[object Object],[object Object]"); + }); + }); + + describe("_compare", function() { + const r = new Range(); + + const compareAsNumbers = function(a: any, b: any) { + return a - b; + }; + + const constantCompareFunction = function(a: any, b: any) { + return 0; + }; + + it("(undefined, undefined) === 0", function() { + // assert(r._compare() === 0); + // assert(r._compare(undefined) === 0); + assert(r._compare(undefined, undefined) === 0); + }); + + it("(undefined, y) > 0", function() { + assert(r._compare(undefined, null) > 0); + assert(r._compare(undefined, -NaN) > 0); + assert(r._compare(undefined, 0) > 0); + assert(r._compare(undefined, NaN) > 0); + assert(r._compare(undefined, true as any) > 0); + assert(r._compare(undefined, false as any) > 0); + assert(r._compare(undefined, "a") > 0); + assert(r._compare(undefined, "undefined") > 0); + assert(r._compare(undefined, "z") > 0); + assert(r._compare(undefined, [] as any) > 0); + assert(r._compare(undefined, {} as any) > 0); + assert(r._compare(undefined, 2, constantCompareFunction) > 0); + assert(r._compare(undefined, 2, compareAsNumbers) > 0); + }); + + it("(x, undefined) < 0", function() { + assert(r._compare(null, undefined) < 0); + assert(r._compare(-NaN, undefined) < 0); + assert(r._compare(0, undefined) < 0); + assert(r._compare(NaN, undefined) < 0); + assert(r._compare(true as any, undefined) < 0); + assert(r._compare(false as any, undefined) < 0); + assert(r._compare("a", undefined) < 0); + assert(r._compare("undefined", undefined) < 0); + assert(r._compare("z", undefined) < 0); + assert(r._compare([] as any, undefined) < 0); + assert(r._compare({} as any, undefined) < 0); + assert(r._compare(1, undefined, constantCompareFunction) < 0); + assert(r._compare(1, undefined, compareAsNumbers) < 0); + }); + + it("values as strings (default)", function() { + assert(r._compare("A", "B") < 0); + assert(r._compare("", "") === 0); + assert(r._compare("B", "A") > 0); + assert(r._compare("10", "2") < 0); + assert(r._compare(10, "02") > 0); + assert(r._compare(10, 2) < 0); + assert(r._compare(null, "nulm") < 0); + assert(r._compare(null, "null") === 0); + assert(r._compare(null, "nulk") > 0); + assert(r._compare(true as any, "truf") < 0); + assert(r._compare(true as any, "true") === 0); + assert(r._compare(true as any, "trud") > 0); + assert(r._compare({} as any, "[object Object]") === 0); + }); + + it("values as numbers", function() { + assert(r._compare(undefined, 2, compareAsNumbers) > 0); + assert(r._compare(1, 2, compareAsNumbers) < 0); + assert(r._compare(0, 0, compareAsNumbers) === 0); + assert(r._compare(10, 2, compareAsNumbers) > 0); + }); + + it("always return 0", function() { + assert(r._compare(1, 2, constantCompareFunction) === 0); + assert(r._compare(2, 1, constantCompareFunction) === 0); + }); + }); + + describe("_isRange", function() { + it("_isRange(undefined) is false", function() { + assert(!Range._isRange(undefined)); + }); + + it("_isRange(null) is false", function() { + assert(!Range._isRange(null)); + }); + + it("_isRange(non-object) is false", function() { + const points: any[] = [ + undefined, + null, + 1, + "", + true, + NaN, + function() { + /* no op */ + }, + {}, + { + low: "" + } + ]; + + for (const point of points) { + assert(!Range._isRange(point)); + } + }); + + it("_isRange(point) is false", function() { + const ranges: any[] = [ + { + low: "", + high: 1 + } + // new Range(), // TODO: this was here, but _isRange just tests for if it's instanceof + ]; + + for (const range of ranges) { + assert(!Range._isRange(range)); + } + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/unit/rangePartitionResolver.spec.ts b/sdk/cosmosdb/cosmos/src/test/unit/rangePartitionResolver.spec.ts new file mode 100644 index 000000000000..751ca1f7cc7f --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/unit/rangePartitionResolver.spec.ts @@ -0,0 +1,349 @@ +import assert from "assert"; +import { Range, RangePartitionResolver } from "../../range"; +import { CompareFunction } from "../../range"; + +describe("RangePartitionResolver", function() { + describe("constructor", function() { + // TODO: should split these up into individual tests + it("missing partitionKeyExtractor throws", function() { + const expetcedError = /Error: partitionKeyExtractor cannot be null or undefined/; + + assert.throws(function() { + const r = new RangePartitionResolver(undefined, undefined); + }, expetcedError); + + assert.throws(function() { + const r = new RangePartitionResolver(undefined, undefined); + }, expetcedError); + + assert.throws(function() { + const r = new RangePartitionResolver(null, undefined); + }, expetcedError); + }); + + it("invalid partitionKeyExtractor throws", function() { + const expetcedError = /partitionKeyExtractor must be either a 'string' or a 'function'/; + + assert.throws(function() { + const r = new RangePartitionResolver(0 as any, undefined); + }, expetcedError); + + assert.throws(function() { + const r = new RangePartitionResolver(true as any, undefined); + }, expetcedError); + + assert.throws(function() { + const r = new RangePartitionResolver(NaN as any, undefined); + }, expetcedError); + + assert.throws(function() { + const r = new RangePartitionResolver([] as any, undefined); + }, expetcedError); + + assert.throws(function() { + const r = new RangePartitionResolver({} as any, undefined); + }, expetcedError); + }); + + it("missing partitionKeyMap throws", function() { + const expectedError = /Error: partitionKeyMap cannot be null or undefined/; + + assert.throws(function() { + const r = new RangePartitionResolver("", undefined); + }, expectedError); + + assert.throws(function() { + const r = new RangePartitionResolver( + function() { + /* no op */ + } as any, + undefined + ); + }, expectedError); + + assert.throws(function() { + const r = new RangePartitionResolver("", null); + }, expectedError); + }); + + it("invalid partitionKeyMap throws", function() { + const expectedError = /Error: partitionKeyMap has to be an Array/; + + assert.throws(function() { + const r = new RangePartitionResolver("", 0 as any); + }, expectedError); + + assert.throws(function() { + const r = new RangePartitionResolver("", "" as any); + }, expectedError); + + assert.throws(function() { + const r = new RangePartitionResolver("", true as any); + }, expectedError); + + assert.throws(function() { + const r = new RangePartitionResolver("", NaN as any); + }, expectedError); + + assert.throws(function() { + const r = new RangePartitionResolver("", {} as any); + }, expectedError); + + const rpr = new RangePartitionResolver("", new Array()); + }); + + it("valid RangePartitionResolver", function(done) { + const resolver = new RangePartitionResolver("", []); + assert(resolver); + assert.strictEqual(resolver.partitionKeyExtractor, ""); + assert.deepEqual(resolver.partitionKeyMap, []); + done(); + }); + }); + + describe("getFirstContainingMapEntryOrNull", function() { + it("getFirstContainingMapEntryOrNull - empty map returns null", function(done) { + const ranges = [undefined, null, 0, "", true, [], {}, NaN, new Range()]; + const resolver = new RangePartitionResolver("", []); + ranges.forEach(function(r) { + const result = resolver.getFirstContainingMapEntryOrNull(r); + assert.equal(result, null); + }); + done(); + }); + + it("_tryGetContainingRange - map with no containing entry returns null", function(done) { + const mapEntry = { range: new Range({ low: "A" }), link: "link1" }; + const resolver = new RangePartitionResolver("key", [mapEntry]); + const result = resolver.getFirstContainingMapEntryOrNull(new Range({ low: "B" })); + assert.equal(result, null); + done(); + }); + + it("_tryGetContainingRange - map with single containing entry returns entry", function(done) { + const mapEntry = { range: new Range(), link: "link1" }; + const resolver = new RangePartitionResolver("key", [mapEntry]); + const result = resolver.getFirstContainingMapEntryOrNull(new Range()); + assert.deepEqual(result, { range: new Range(), link: "link1" }); + done(); + }); + + it("_tryGetContainingRange - map with more multiple containing entries returns first entry", function(done) { + const map1 = [ + { range: new Range({ low: "A", high: "B" }), link: "link1" }, + { range: new Range({ low: "A" }), link: "link2" } + ]; + + const resolver1 = new RangePartitionResolver("key", map1); + const result1 = resolver1.getFirstContainingMapEntryOrNull(new Range({ low: "A" })); + assert.strictEqual(result1.link, "link1"); + + const map2 = [ + { range: new Range({ low: "A" }), link: "link2" }, + { range: new Range({ low: "A", high: "Z" }), link: "link1" } + ]; + + const resolver2 = new RangePartitionResolver("key", map2); + const result2 = resolver2.getFirstContainingMapEntryOrNull(new Range({ low: "A" })); + assert.strictEqual(result2.link, "link2"); + done(); + }); + }); + + describe("resolveForCreate", function() { + it("_tryGetContainingRange - map containing parition key returns corresponding link", function(done) { + const resolver = new RangePartitionResolver("key", [ + { range: new Range({ low: "A", high: "M" }), link: "link1" }, + { range: new Range({ low: "N", high: "Z" }), link: "link2" } + ]); + const result = resolver.resolveForCreate("X"); + assert.strictEqual(result, "link2"); + done(); + }); + + it("_tryGetContainingRange - map not containing parition key throws", function(done) { + const resolver = new RangePartitionResolver("key", [ + { range: new Range({ low: "A", high: "M" }), link: "link1" } + ]); + + assert.throws(function() { + const result = resolver.resolveForCreate("X"); + }, /Error: Invalid operation: A containing range for 'X,X' doesn't exist in the partition map./); + done(); + }); + }); + + const resolveForReadTest = function(resolver: any, partitionKey: any, expectedLinks: any) { + const result = resolver.resolveForRead(partitionKey); + assert.deepEqual(expectedLinks, result); + }; + + describe("resolveForRead", function() { + const resolver = new RangePartitionResolver( + function(doc: any) { + // TODO: any + return doc.key; + }, + [ + { + range: new Range({ low: "A", high: "M" }), + link: "link1" + }, + { + range: new Range({ low: "N", high: "Z" }), + link: "link2" + } + ] + ); + + it("undefined", function(done) { + const partitionKey: any = undefined; + const expectedLinks = ["link1", "link2"]; + resolveForReadTest(resolver, partitionKey, expectedLinks); + done(); + }); + + it("null", function(done) { + const partitionKey: any = null; + const expectedLinks = ["link1", "link2"]; + resolveForReadTest(resolver, partitionKey, expectedLinks); + done(); + }); + }); + + describe("resolveForRead string", function() { + const resolver = new RangePartitionResolver( + function(doc: any) { + // TODO: any + return doc.key; + }, + [ + { + range: new Range({ low: "A", high: "M" }), + link: "link1" + }, + { + range: new Range({ low: "N", high: "Z" }), + link: "link2" + } + ] + ); + + it("point", function(done) { + const partitionKey = new Range({ low: "D" }); + const expectedLinks = ["link1"]; + resolveForReadTest(resolver, partitionKey, expectedLinks); + + const partitionKey2 = new Range({ low: "Q" }); + const expectedLinks2 = ["link2"]; + resolveForReadTest(resolver, partitionKey2, expectedLinks2); + done(); + }); + + it("range", function(done) { + const partitionKey = new Range({ low: "D", high: "Q" }); + const expectedLinks = ["link1", "link2"]; + resolveForReadTest(resolver, partitionKey, expectedLinks); + done(); + }); + + it("array of ranges", function(done) { + const partitionKey = [new Range({ low: "A", high: "B" }), new Range({ low: "Q" })]; + const expectedLinks = ["link1", "link2"]; + resolveForReadTest(resolver, partitionKey, expectedLinks); + done(); + }); + }); + + describe("resolveForRead number", function() { + const partitionKeyExtractor = function(doc: any) { + return doc.key; + }; + + const partitionKeyMap = [ + { + range: new Range({ low: 1, high: 15 }), + link: "link1" + }, + { + range: new Range({ low: 16, high: 30 }), + link: "link2" + } + ]; + + it("point, default compareFunction", function(done) { + const resolver = new RangePartitionResolver(partitionKeyExtractor, partitionKeyMap); + + const partitionKey = new Range({ low: 2 }); + const expectedLinks = ["link2"]; + + resolveForReadTest(resolver, partitionKey, expectedLinks); + done(); + }); + + it("point, custom compareFunction", function(done) { + const resolver = new RangePartitionResolver(partitionKeyExtractor, partitionKeyMap, function( + a: number, + b: number + ) { + return a - b; + }); + + const partitionKey = new Range({ low: 2 }); + const expectedLinks = ["link1"]; + + resolveForReadTest(resolver, partitionKey, expectedLinks); + done(); + }); + }); + + describe("compareFunction", function() { + const invalidCompareFunctionTest = function(compareFunction: any) { + assert.throws(function() { + const resolver = new RangePartitionResolver( + "key", + [{ range: new Range({ low: "A" }), link: "link1" }], + compareFunction + ); + }, /Invalid argument: 'compareFunction' is not a function/); + }; + + it("invalid compareFunction - null", function() { + const compareFunction: CompareFunction = null; + invalidCompareFunctionTest(compareFunction); + }); + + it("invalid compareFunction - string", function() { + const compareFunction = ""; + invalidCompareFunctionTest(compareFunction); + }); + + it("invalid compareFunction - number", function() { + const compareFunction = 0; + invalidCompareFunctionTest(compareFunction); + }); + + it("invalid compareFunction - boolean", function() { + const compareFunction = false; + invalidCompareFunctionTest(compareFunction); + }); + + it("invalid compareFunction - object", function() { + const compareFunction = {}; + invalidCompareFunctionTest(compareFunction); + }); + + it("compareFunction throws", function() { + const resolver = new RangePartitionResolver("key", [{ range: new Range({ low: "A" }), link: "link1" }], function( + a, + b + ) { + throw new Error("Compare error"); + }); + + assert.throws(function() { + const result = (resolver as any).resolveForRead("A", ["link1"]); // TODO: any + }, /Error: Compare error/); + }); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/unit/sessionContainer.spec.ts b/sdk/cosmosdb/cosmos/src/test/unit/sessionContainer.spec.ts new file mode 100644 index 000000000000..08f586334f78 --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/unit/sessionContainer.spec.ts @@ -0,0 +1,91 @@ +import assert from "assert"; +import { Constants } from "../../common"; +import { IHeaders } from "../../queryExecutionContext/IHeaders"; +import { SessionContainer } from "../../session/sessionContainer"; +import { SessionContext } from "../../session/SessionContext"; + +describe("SessionContainer", function() { + const collectionLink = "dbs/testDatabase/colls/testCollection"; + const collectionId = "oWxIAN48yN0="; + + it("set/get/delete", function() { + const sc = new SessionContainer(); + + const tokenString = "1:1#100#1=20#2=5#3=30"; + + const nameBasedRequest: SessionContext = { + isNameBased: true, + resourceId: null, + resourceAddress: "/" + collectionLink + "/", + resourceType: "docs", + operationType: "create" + }; + + const resHeadersNameBased: IHeaders = {}; + resHeadersNameBased[Constants.HttpHeaders.OwnerFullName] = collectionLink; + resHeadersNameBased[Constants.HttpHeaders.OwnerId] = collectionId; + resHeadersNameBased[Constants.HttpHeaders.SessionToken] = tokenString; + + // Add a token and get new token, should be equal + sc.set(nameBasedRequest, resHeadersNameBased); + const originalTokenString = sc.get(nameBasedRequest); + assert.equal( + tokenString, + originalTokenString, + "Session token string must be equal to original header on initial set" + ); + + // Add an older token, should still equal original token + const tokenStringWithOlderVersion = "1:1#99#1=19#2=4#3=29"; + resHeadersNameBased[Constants.HttpHeaders.SessionToken] = tokenStringWithOlderVersion; + sc.set(nameBasedRequest, resHeadersNameBased); + const sameTokenString = sc.get(nameBasedRequest); + assert.equal( + tokenString, + sameTokenString, + "Session token string must be equal to the original higher version header" + ); + + // Add a newer version token, should equal new token + const tokenStringWithNewerVersion = "1:1#100#1=30#2=10#3=40"; + resHeadersNameBased[Constants.HttpHeaders.SessionToken] = tokenStringWithNewerVersion; + sc.set(nameBasedRequest, resHeadersNameBased); + const updatedTokenString = sc.get(nameBasedRequest); + assert.equal( + tokenStringWithNewerVersion, + updatedTokenString, + "Session token string must be equal to the new higher version header" + ); + + // Add a new partition's token, should container new and old token + const tokenFromAnotherPartition = "2:1#100#1=10"; + resHeadersNameBased[Constants.HttpHeaders.SessionToken] = tokenFromAnotherPartition; + sc.set(nameBasedRequest, resHeadersNameBased); + const multiplePartitions = sc.get(nameBasedRequest); + assert( + multiplePartitions.includes(tokenStringWithNewerVersion), + "Token string must contain token from updated request" + ); + assert(multiplePartitions.includes(tokenFromAnotherPartition), "Token string must contain from new partition"); + + // Add a token with has multiple partitions in it, 1 old, and 1 new. Should only keep the new one, but still contain tokens for both + const p2TokenWithNewerVersion = "2:2#100#1=10#2=50"; + const tokenWithMultiplePartitions = `${tokenStringWithOlderVersion},${p2TokenWithNewerVersion}`; + resHeadersNameBased[Constants.HttpHeaders.SessionToken] = tokenWithMultiplePartitions; + sc.set(nameBasedRequest, resHeadersNameBased); + const multiplePartitions2 = sc.get(nameBasedRequest); + assert( + multiplePartitions2.includes(tokenStringWithNewerVersion), + "Token string must contain token from previous request for first partition" + ); + assert( + multiplePartitions2.includes(p2TokenWithNewerVersion), + "Token string must contain from updated token for second partition" + ); + + // Remove tokens and get new token, should be empty + sc.remove(nameBasedRequest); + const emptyTokenString = sc.get(nameBasedRequest); + assert.equal("", emptyTokenString, "Session token string must be empty after removal"); + }); +}); diff --git a/sdk/cosmosdb/cosmos/src/test/unit/smartRoutingMapProvider.spec.ts b/sdk/cosmosdb/cosmos/src/test/unit/smartRoutingMapProvider.spec.ts new file mode 100644 index 000000000000..6cd0b2ab1ecd --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/test/unit/smartRoutingMapProvider.spec.ts @@ -0,0 +1,335 @@ +import assert from "assert"; +import { ClientContext } from "../../ClientContext"; +import { PartitionKeyRangeCache, QueryRange, SmartRoutingMapProvider } from "../../routing"; +import { MockedClientContext } from "../common/MockClientContext"; + +describe("Smart Routing Map Provider OverlappingRanges", function() { + const containerLink = "dbs/7JZZAA==/colls/7JZZAOS-JQA=/"; + const containerId = "my container"; + + const partitionKeyRanges = [ + { id: "0", minInclusive: "", maxExclusive: "05C1C9CD673398" }, + { + id: "1", + minInclusive: "05C1C9CD673398", + maxExclusive: "05C1D9CD673398" + }, + { + id: "2", + minInclusive: "05C1D9CD673398", + maxExclusive: "05C1E399CD6732" + }, + { + id: "3", + minInclusive: "05C1E399CD6732", + maxExclusive: "05C1E9CD673398" + }, + { id: "4", minInclusive: "05C1E9CD673398", maxExclusive: "FF" } + ]; + + const mockedClientContext: ClientContext = new MockedClientContext(partitionKeyRanges, containerId) as any; + const smartRoutingMapProvider = new SmartRoutingMapProvider(mockedClientContext); + const partitionKeyRangeCache = new PartitionKeyRangeCache(mockedClientContext); + + describe("Test Full Range", function() { + it('query ranges: ["", ""FF)', function() { + // query range is the whole partition key range + const pkRange = new QueryRange("", "FF", true, false); + return validateOverlappingRanges([pkRange], partitionKeyRanges); + }); + + it('query ranges: ("", ""FF)', function() { + // query range is the whole partition key range + const pkRange = new QueryRange("", "FF", false, false); + return validateOverlappingRanges([pkRange], partitionKeyRanges); + }); + }); + + describe("Test Empty Range", function() { + it("empty query range list", async function() { + // query range list is empty + try { + await validateOverlappingRanges([], []); + } catch (err) { + throw err; + } + }); + + it('query ranges: ("", ""]', async function() { + // validate the overlaping partition key ranges results for empty ranges is empty + try { + await validateOverlappingRanges([new QueryRange("", "", false, true)], []); + } catch (err) { + throw err; + } + }); + + it('query ranges: ("", "")', async function() { + // validate the overlaping partition key ranges results for empty ranges is empty + try { + await validateOverlappingRanges([new QueryRange("", "", false, false)], []); + } catch (err) { + throw err; + } + }); + + it('query ranges: ["", "")', async function() { + // validate the overlaping partition key ranges results for empty ranges is empty + try { + await validateOverlappingRanges([new QueryRange("", "", true, false)], []); + } catch (err) { + throw err; + } + }); + }); + + describe("Error Handling: Bad Overlapping Query Range", function() { + it("overlapping query ranges (in a point)", async function() { + const r1 = new QueryRange("", "AA", true, true); + const r2 = new QueryRange("AA", "FF", true, false); + try { + await validateSmartOverlappingRanges([r1, r2], undefined, true); + } catch (err) { + throw err; + } + }); + + it("overlapping query ranges (in a range)", async function() { + const r1 = new QueryRange("", "AB", true, false); + const r2 = new QueryRange("AA", "FA", true, false); + try { + await validateSmartOverlappingRanges([r1, r2], undefined, true); + } catch (err) { + throw err; + } + }); + + it("not sorted query ranges", async function() { + const r1 = new QueryRange("AB", "AC", true, false); + const r2 = new QueryRange("AA", "AB", true, false); + try { + await validateSmartOverlappingRanges([r1, r2], undefined, true); + } catch (err) { + throw err; + } + }); + }); + + it("Empty Ranges are thrown away", async function() { + const e1 = new QueryRange("", "", true, false); + const r1 = new QueryRange("", "AB", true, false); + const e2 = new QueryRange("AB", "AB", true, false); + const r2 = new QueryRange("AB", "AC", true, false); + const e3 = new QueryRange("AC", "AC", true, false); + const e4 = new QueryRange("AD", "AD", true, false); + try { + await assertOverlappingRangesAreEqual([e1, r1, e2, r2, e3, e4], [r1, r2]); + } catch (err) { + throw err; + } + }); + + it("Single Query Range", async function() { + try { + const r = new QueryRange("AB", "AC", true, false); + await assertBothProvidersResultsEqual([r]); + } catch (err) { + throw err; + } + }); + + it("Multiple Query Ranges", async function() { + try { + const ranges = [ + new QueryRange("0000000040", "0000000045", true, false), + new QueryRange("0000000045", "0000000046", true, false), + new QueryRange("0000000046", "0000000050", true, false) + ]; + await assertBothProvidersResultsEqual(ranges); + } catch (err) { + throw err; + } + }); + + it("Single Boundary Case Query Range", async function() { + const ranges = [new QueryRange("05C1C9CD673398", "05C1D9CD673398", true, false)]; + try { + await validateOverlappingRanges(ranges, partitionKeyRanges.slice(1, 2)); + } catch (err) { + throw err; + } + }); + + it("Two Adjacent Boundary Case Query Ranges", async function() { + const ranges = [ + // partitionKeyRanges[1] + new QueryRange("05C1C9CD673398", "05C1D9CD673398", true, false), + // partitionKeyRanges[2] + new QueryRange("05C1D9CD673398", "05C1D9CD673399", true, false) + ]; + try { + await validateOverlappingRanges(ranges, partitionKeyRanges.slice(1, 3)); + } catch (err) { + throw err; + } + }); + + it("Two Ranges in one partition key range", async function() { + const ranges = [ + // two ranges fall in the same partition key range + new QueryRange("05C1C9CD673400", "05C1C9CD673401", true, false), + new QueryRange("05C1C9CD673402", "05C1C9CD673403", true, false) + ]; + try { + await validateOverlappingRanges(ranges, partitionKeyRanges.slice(1, 2)); + } catch (err) { + throw err; + } + }); + + it("Complex", async function() { + const ranges = [ + // all are covered by partitionKeyRanges[1] + new QueryRange("05C1C9CD673398", "05C1D9CD673391", true, false), + new QueryRange("05C1D9CD673391", "05C1D9CD673392", true, false), + new QueryRange("05C1D9CD673393", "05C1D9CD673395", true, false), + new QueryRange("05C1D9CD673395", "05C1D9CD673395", true, false), + // all are covered by partitionKeyRanges[4]] + new QueryRange("05C1E9CD673398", "05C1E9CD673401", true, false), + new QueryRange("05C1E9CD673402", "05C1E9CD673403", true, false), + // empty range + new QueryRange("FF", "FF", true, false) + ]; + try { + await validateOverlappingRanges(ranges, [partitionKeyRanges[1], partitionKeyRanges[4]]); + } catch (err) { + throw err; + } + }); + + // Validates the results + // smartRoutingMapProvider.getOverlappingRanges() + // partitionKeyRangeCache.getOverlappingRanges() is equal + const assertBothProvidersResultsEqual = async (queryRanges: any) => { + let results1: any; + let results2: any; + let err1: any; + let err2: any; + results1 = results2 = null; + err1 = err2 = null; + try { + results1 = await smartRoutingMapProvider.getOverlappingRanges(containerLink, queryRanges); + } catch (err) { + err1 = err; + } + try { + results2 = await partitionKeyRangeCache.getOverlappingRanges(containerLink, queryRanges); + } catch (err) { + err2 = err; + } + assert.equal(err1, err2); + assert.deepEqual(results1, results2); + }; + + // Validates the results + // smartRoutingMapProvider.getOverlappingRanges() + // partitionKeyRangeCache.getOverlappingRanges() is as expected + const validateOverlappingRanges = async function(queryRanges: any, expectedResults: any, errorExpected?: any) { + try { + errorExpected = errorExpected || false; + await validateSmartOverlappingRanges(queryRanges, expectedResults, errorExpected); + await validatePartitionKeyRangeCacheOverlappingRanges(queryRanges, expectedResults, errorExpected); + } catch (err) { + throw err; + } + }; + + // Validates the results of both + // smartRoutingMapProvider.getOverlappingRanges() + // partitionKeyRangeCache.getOverlappingRanges() is the same for both queryRanges1, queryRanges2 + const assertOverlappingRangesAreEqual = async function(queryRanges1: any, queryRanges2: any) { + try { + await assertProviderOverlappingRangesAreEqual(smartRoutingMapProvider, queryRanges1, queryRanges2); + await assertProviderOverlappingRangesAreEqual(partitionKeyRangeCache as any, queryRanges1, queryRanges2); + await assertBothProvidersResultsEqual(queryRanges1); + } catch (err) { + throw err; + } + }; + + // Validates the results + // provider.getOverlappingRanges() is the same on both queryRanges1, queryRanges2 + const assertProviderOverlappingRangesAreEqual = async function( + provider: SmartRoutingMapProvider, + queryRanges1: any, + queryRanges2: any + ) { + let results1: any; + let results2: any; + let err1: any; + let err2: any; + try { + results1 = await provider.getOverlappingRanges(containerLink, queryRanges1); + } catch (err) { + err1 = err; + } + try { + results2 = await provider.getOverlappingRanges(containerLink, queryRanges2); + } catch (err) { + err2 = err; + } + assert.equal(err1, err2); + assert.deepEqual(results1, results2); + }; + + // Validates the results + // provider.getOverlappingRanges() is as expected + const validateProviderOverlappingRanges = async function( + provider: SmartRoutingMapProvider, + queryRanges: any, + expectedResults: any, + errorExpected?: any + ) { + errorExpected = errorExpected || false; + try { + const results = await provider.getOverlappingRanges(containerLink, queryRanges); + assert.deepEqual(results, expectedResults); + } catch (err) { + if (errorExpected) { + assert.notEqual(err, undefined); + return; + } else { + throw err; + } + } + }; + + // validates that the results of + // smartRoutingMapProvider.getOverlappingRanges() is as expected + const validateSmartOverlappingRanges = async function(queryRanges: any, expectedResults: any, errorExpected: any) { + try { + await validateProviderOverlappingRanges(smartRoutingMapProvider, queryRanges, expectedResults, errorExpected); + } catch (err) { + throw err; + } + }; + + // validates that the results of + // partitionKeyRangeCache.getOverlappingRanges() is as expected + const validatePartitionKeyRangeCacheOverlappingRanges = async function( + queryRanges: any, + expectedResults: any, + errorExpected: any + ) { + try { + await validateProviderOverlappingRanges( + partitionKeyRangeCache as any, + queryRanges, + expectedResults, + errorExpected + ); + } catch (err) { + throw err; + } + }; +}); diff --git a/sdk/cosmosdb/cosmos/src/typings/create-hmac.d.ts b/sdk/cosmosdb/cosmos/src/typings/create-hmac.d.ts new file mode 100644 index 000000000000..b60841a8867e --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/typings/create-hmac.d.ts @@ -0,0 +1,7 @@ +/// + +declare module "create-hmac" { + import { createHmac } from "crypto"; + + export = createHmac; +} diff --git a/sdk/cosmosdb/cosmos/src/typings/polyfill.d.ts b/sdk/cosmosdb/cosmos/src/typings/polyfill.d.ts new file mode 100644 index 000000000000..4018e67aee4d --- /dev/null +++ b/sdk/cosmosdb/cosmos/src/typings/polyfill.d.ts @@ -0,0 +1,5 @@ +declare module "binary-search-bounds" { + // tslint:disable-next-line:variable-name + const _bs: any; + export = _bs; +} diff --git a/sdk/cosmosdb/cosmos/test-pr.js b/sdk/cosmosdb/cosmos/test-pr.js new file mode 100644 index 000000000000..ea540c013a34 --- /dev/null +++ b/sdk/cosmosdb/cosmos/test-pr.js @@ -0,0 +1 @@ +// test PR validation diff --git a/sdk/cosmosdb/cosmos/ts-test.js b/sdk/cosmosdb/cosmos/ts-test.js new file mode 100644 index 000000000000..b1c3976c3d60 --- /dev/null +++ b/sdk/cosmosdb/cosmos/ts-test.js @@ -0,0 +1,33 @@ +const execa = require("execa"); + +let versions = ["3.0", "3.1"]; + +if (!process.env.SKIP_LATEST) { + versions.push("latest"); +} + +async function exec(cmd) { + const command = execa.shell(cmd, { cwd: "./ts-test" }); + command.stderr.pipe(process.stderr); + command.stdout.pipe(process.stdout); + return command; +} + +(async () => { + try { + console.log("Running typescript consumer test againast", versions); + await exec("npm init -y"); + console.log("Setting up typescript consumer project"); + await exec("npm install --save ./.."); + console.log("Installing @azure/cosmos as a file dependency"); + for (const version of versions) { + console.log(`Compling with typescript@${version}`); + await exec(`npx -p typescript@${version} tsc ./test.ts --allowSyntheticDefaultImports true`); + } + process.exit(0); + } catch (error) { + console.log("Typescript consumer test failed!"); + console.log(error); + process.exit(1); + } +})(); diff --git a/sdk/cosmosdb/cosmos/ts-test/test.ts b/sdk/cosmosdb/cosmos/ts-test/test.ts new file mode 100644 index 000000000000..b63fc13122da --- /dev/null +++ b/sdk/cosmosdb/cosmos/ts-test/test.ts @@ -0,0 +1,3 @@ +import * as Cosmos from "@azure/cosmos"; + +console.log(Object.keys(Cosmos)); diff --git a/sdk/cosmosdb/cosmos/tsconfig.json b/sdk/cosmosdb/cosmos/tsconfig.json new file mode 100644 index 000000000000..36522c6ebae4 --- /dev/null +++ b/sdk/cosmosdb/cosmos/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "importHelpers": true, + "noImplicitAny": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "outDir": "./lib", + "preserveConstEnums": true, + "removeComments": false, + "target": "es6", + "sourceMap": true, + "newLine": "LF", + "resolveJsonModule": true + }, + "include": ["./src/**/*", "./test/**/*.spec.ts", "./samples/MultiRegionWrite/**.ts"], + "exclude": ["node_modules", "samples"] +} diff --git a/sdk/cosmosdb/cosmos/tsconfig.prod.json b/sdk/cosmosdb/cosmos/tsconfig.prod.json new file mode 100644 index 000000000000..3a3efd6ae6ba --- /dev/null +++ b/sdk/cosmosdb/cosmos/tsconfig.prod.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "module": "es6" + } +} diff --git a/sdk/cosmosdb/cosmos/tslint.json b/sdk/cosmosdb/cosmos/tslint.json new file mode 100644 index 000000000000..19c995a42ee2 --- /dev/null +++ b/sdk/cosmosdb/cosmos/tslint.json @@ -0,0 +1,15 @@ +{ + "extends": ["tslint:recommended", "tslint-config-prettier"], + "exclude": "./node_modules", + "rules": { + "interface-name": false, + "no-string-literal": false, + "object-literal-sort-keys": false, + "member-ordering": false, // TODO: might want to look at this eventually... + "no-floating-promises": true, + "import-blacklist": [true, "assert", "util"] + }, + "linterOptions": { + "exclude": ["*.json"] + } +} diff --git a/sdk/cosmosdb/cosmos/webpack.config.js b/sdk/cosmosdb/cosmos/webpack.config.js new file mode 100644 index 000000000000..d43093b43b5a --- /dev/null +++ b/sdk/cosmosdb/cosmos/webpack.config.js @@ -0,0 +1,17 @@ +var path = require("path"); +var webpack = require("webpack"); + +module.exports = (env, argv) => ({ + entry: "./lib/src/index.js", + node: { + buffer: true, + net: "mock", + tls: "mock" + }, + output: { + filename: "azurecosmos.js", + path: path.resolve(__dirname, "lib", "dist"), + library: "CosmosClient" + }, + devtool: argv.mode === "production" ? "source-map" : "inline-source-map" +});